bae13d5503
Co-authored-by: Michael Hoennig <michael@hoennig.de> Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/202 Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
901 lines
31 KiB
PL/PgSQL
901 lines
31 KiB
PL/PgSQL
--liquibase formatted sql
|
|
|
|
-- ============================================================================
|
|
--changeset michael.hoennig:rbac-base-REFERENCE endDelimiter:--//
|
|
-- ----------------------------------------------------------------------------
|
|
|
|
create type rbac.ReferenceType as enum ('rbac.subject', 'rbac.role', 'rbac.permission');
|
|
|
|
create table rbac.reference
|
|
(
|
|
uuid uuid unique default uuid_generate_v4(),
|
|
type rbac.ReferenceType not null
|
|
);
|
|
|
|
create or replace function rbac.assertReferenceType(argument varchar, referenceId uuid, expectedType rbac.ReferenceType)
|
|
returns rbac.ReferenceType
|
|
language plpgsql as $$
|
|
declare
|
|
actualType rbac.ReferenceType;
|
|
begin
|
|
if referenceId is null then
|
|
raise exception '% must be a % and not null', argument, expectedType;
|
|
end if;
|
|
|
|
actualType = (select type from rbac.reference where uuid = referenceId);
|
|
if (actualType <> expectedType) then
|
|
raise exception '% must reference a %, but got a %', argument, expectedType, actualType;
|
|
end if;
|
|
return expectedType;
|
|
end; $$;
|
|
--//
|
|
|
|
-- ============================================================================
|
|
--changeset michael.hoennig:rbac-base-SUBJECT runOnChange:true validCheckSum:ANY endDelimiter:--//
|
|
-- ----------------------------------------------------------------------------
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (SELECT FROM information_schema.tables
|
|
WHERE table_schema = 'rbac' AND table_name = 'subject') THEN
|
|
|
|
CREATE TABLE rbac.subject
|
|
(
|
|
uuid uuid primary key references rbac.reference (uuid) on delete cascade,
|
|
name varchar(63) not null unique
|
|
);
|
|
|
|
CALL base.create_journal('rbac.subject');
|
|
END IF;
|
|
END
|
|
$$;
|
|
|
|
create or replace function rbac.create_subject(subjectName varchar)
|
|
returns uuid
|
|
returns null on null input
|
|
language plpgsql as $$
|
|
declare
|
|
stableUuidNamespace uuid;
|
|
subjectUuid uuid;
|
|
begin
|
|
stableUuidNamespace := '6ba7b810-9dad-11d1-80b4-00c04fd430c8'::uuid;
|
|
subjectUuid := uuid_generate_v5(stableUuidNamespace, subjectName);
|
|
insert
|
|
into rbac.reference (uuid, type)
|
|
values (subjectUuid, 'rbac.subject');
|
|
insert
|
|
into rbac.subject (uuid, name)
|
|
values (subjectUuid, subjectName);
|
|
return subjectUuid;
|
|
end;
|
|
$$;
|
|
|
|
create or replace function rbac.create_subject(refUuid uuid, subjectName varchar)
|
|
returns uuid
|
|
called on null input
|
|
language plpgsql as $$
|
|
begin
|
|
insert
|
|
into rbac.reference as r (uuid, type)
|
|
values (coalesce(refUuid, uuid_generate_v4()), 'rbac.subject')
|
|
returning r.uuid into refUuid;
|
|
insert
|
|
into rbac.subject (uuid, name)
|
|
values (refUuid, subjectName);
|
|
return refUuid;
|
|
end;
|
|
$$;
|
|
|
|
create or replace function rbac.find_subject_id(subjectName varchar)
|
|
returns uuid
|
|
returns null on null input
|
|
language sql as $$
|
|
select uuid from rbac.subject where name = subjectName
|
|
$$;
|
|
--//
|
|
|
|
-- ============================================================================
|
|
--changeset michael.hoennig:rbac-base-OBJECT endDelimiter:--//
|
|
-- ----------------------------------------------------------------------------
|
|
/*
|
|
|
|
*/
|
|
create table rbac.object
|
|
(
|
|
uuid uuid primary key default uuid_generate_v4(),
|
|
serialId serial, -- TODO.perf: only needed for reverse deletion of temp test data
|
|
objectTable varchar(64) not null,
|
|
unique (objectTable, uuid)
|
|
);
|
|
|
|
call base.create_journal('rbac.object');
|
|
|
|
--//
|
|
|
|
|
|
-- ============================================================================
|
|
--changeset michael.hoennig:rbac-base-GENERATE-RELATED-OBJECT endDelimiter:--//
|
|
-- ----------------------------------------------------------------------------
|
|
|
|
/*
|
|
Inserts related rbac.object for use in the BEFORE INSERT TRIGGERs on the business objects.
|
|
*/
|
|
create or replace function rbac.insert_related_object()
|
|
returns trigger
|
|
language plpgsql
|
|
strict as $$
|
|
declare
|
|
objectUuid uuid;
|
|
tableSchemaAndName text;
|
|
begin
|
|
tableSchemaAndName := base.combine_table_schema_and_name(TG_TABLE_SCHEMA, TG_TABLE_NAME);
|
|
if TG_OP = 'INSERT' then
|
|
if NEW.uuid is null then
|
|
insert
|
|
into rbac.object (objectTable)
|
|
values (tableSchemaAndName)
|
|
returning uuid into objectUuid;
|
|
NEW.uuid = objectUuid;
|
|
else
|
|
insert
|
|
into rbac.object (uuid, objectTable)
|
|
values (NEW.uuid, tableSchemaAndName)
|
|
returning uuid into objectUuid;
|
|
end if;
|
|
return NEW;
|
|
else
|
|
raise exception 'invalid usage of TRIGGER AFTER INSERT';
|
|
end if;
|
|
end; $$;
|
|
|
|
/*
|
|
Deletes related rbac.object for use in the BEFORE DELETE TRIGGERs on the business objects.
|
|
Through cascades all related rbac roles and grants are going to be deleted as well.
|
|
*/
|
|
create or replace function rbac.delete_related_rbac_rules_tf()
|
|
returns trigger
|
|
language plpgsql
|
|
strict as $$
|
|
begin
|
|
if TG_OP = 'DELETE' then
|
|
delete from rbac.object where rbac.object.uuid = old.uuid;
|
|
else
|
|
raise exception 'invalid usage of TRIGGER BEFORE DELETE';
|
|
end if;
|
|
return old;
|
|
end; $$;
|
|
--//
|
|
|
|
|
|
-- ============================================================================
|
|
--changeset michael.hoennig:rbac-base-ROLE endDelimiter:--//
|
|
-- ----------------------------------------------------------------------------
|
|
|
|
create type rbac.RoleType as enum ('OWNER', 'ADMIN', 'AGENT', 'TENANT', 'GUEST', 'REFERRER');
|
|
|
|
create table rbac.role
|
|
(
|
|
uuid uuid primary key references rbac.reference (uuid) on delete cascade initially deferred, -- initially deferred
|
|
objectUuid uuid not null references rbac.object (uuid) initially deferred,
|
|
roleType rbac.RoleType not null,
|
|
unique (objectUuid, roleType)
|
|
);
|
|
|
|
call base.create_journal('rbac.role');
|
|
--//
|
|
|
|
|
|
-- ============================================================================
|
|
--changeset michael.hoennig:rbac-base-ROLE-DESCRIPTOR endDelimiter:--//
|
|
-- ----------------------------------------------------------------------------
|
|
|
|
create type rbac.RoleDescriptor as
|
|
(
|
|
objectTable varchar(63), -- for human readability and easier debugging
|
|
objectUuid uuid,
|
|
roleType rbac.RoleType,
|
|
assumed boolean
|
|
);
|
|
|
|
create or replace function rbac.assumed()
|
|
returns boolean
|
|
stable -- leakproof
|
|
language sql as $$
|
|
select true;
|
|
$$;
|
|
|
|
create or replace function rbac.unassumed()
|
|
returns boolean
|
|
stable -- leakproof
|
|
language sql as $$
|
|
select false;
|
|
$$;
|
|
|
|
create or replace function rbac.roleDescriptorOf(
|
|
objectTable varchar(63), objectUuid uuid, roleType rbac.RoleType,
|
|
assumed boolean = true) -- just for DSL readability, belongs actually to the grant
|
|
returns rbac.RoleDescriptor
|
|
returns null on null input
|
|
stable -- leakproof
|
|
language sql as $$
|
|
select objectTable, objectUuid, roleType::rbac.RoleType, assumed;
|
|
$$;
|
|
|
|
create or replace function rbac.createRole(roleDescriptor rbac.RoleDescriptor)
|
|
returns uuid
|
|
returns null on null input
|
|
language plpgsql as $$
|
|
declare
|
|
referenceId uuid;
|
|
begin
|
|
insert
|
|
into rbac.reference (type)
|
|
values ('rbac.role')
|
|
returning uuid into referenceId;
|
|
insert
|
|
into rbac.role (uuid, objectUuid, roleType)
|
|
values (referenceId, roleDescriptor.objectUuid, roleDescriptor.roleType);
|
|
return referenceId;
|
|
end;
|
|
$$;
|
|
--//
|
|
|
|
|
|
-- ============================================================================
|
|
--changeset michael.hoennig:rbac-base-IDNAME-FUNCTIONS endDelimiter:--//
|
|
-- ----------------------------------------------------------------------------
|
|
create or replace function rbac.findObjectUuidByIdName(objectTable varchar, objectIdName varchar)
|
|
returns uuid
|
|
returns null on null input
|
|
language plpgsql as $$
|
|
declare
|
|
sql varchar;
|
|
uuid uuid;
|
|
begin
|
|
objectTable := base.pureIdentifier(objectTable);
|
|
objectIdName := base.pureIdentifier(objectIdName);
|
|
sql := format('select * from %s_uuid_by_id_name(%L);', objectTable, objectIdName);
|
|
begin
|
|
execute sql into uuid;
|
|
exception
|
|
when others then
|
|
raise exception 'function %_uuid_by_id_name(''%'') failed: %, SQLSTATE: %. If the function itself could not be found, add identity view support to %\nSQL:%',
|
|
objectTable, objectIdName, SQLERRM, SQLSTATE, objectTable, sql;
|
|
end;
|
|
if uuid is null then
|
|
raise exception 'SQL returned null: %', sql;
|
|
else
|
|
return uuid;
|
|
end if;
|
|
end ; $$;
|
|
|
|
create or replace function rbac.findIdNameByObjectUuid(objectTable varchar, objectUuid uuid)
|
|
returns varchar
|
|
returns null on null input
|
|
language plpgsql as $$
|
|
declare
|
|
sql varchar;
|
|
idName varchar;
|
|
begin
|
|
objectTable := base.pureIdentifier(objectTable);
|
|
sql := format('select * from %s_id_name_by_uuid(%L::uuid);', objectTable, objectUuid);
|
|
begin
|
|
execute sql into idName;
|
|
exception
|
|
when others then
|
|
raise exception 'function %_id_name_by_uuid(''%'') failed: %, SQLSTATE: %. If the function itself could not be found, add identity view support to %',
|
|
objectTable, objectUuid, SQLERRM, SQLSTATE, objectTable;
|
|
end;
|
|
return idName;
|
|
end ; $$;
|
|
--//
|
|
|
|
|
|
-- ============================================================================
|
|
--changeset michael.hoennig:rbac-base-ROLE-FUNCTIONS endDelimiter:--//
|
|
-- ----------------------------------------------------------------------------
|
|
|
|
create or replace procedure rbac.deleteRole(roleUUid uuid)
|
|
language plpgsql as $$
|
|
begin
|
|
--raise exception '% deleting role uuid %', rbac.currentSubjectOrAssumedRolesUuids(), roleUUid;
|
|
delete from rbac.role where uuid = roleUUid;
|
|
end;
|
|
$$;
|
|
|
|
create or replace function rbac.findRoleId(roleIdName varchar)
|
|
returns uuid
|
|
returns null on null input
|
|
language plpgsql as $$
|
|
declare
|
|
roleParts text;
|
|
roleTypeFromRoleIdName rbac.RoleType;
|
|
objectNameFromRoleIdName text;
|
|
objectTableFromRoleIdName text;
|
|
objectUuidOfRole uuid;
|
|
roleUuid uuid;
|
|
begin
|
|
-- TODO.refa: extract function rbac.toRoleDescriptor(roleIdName varchar) + find other occurrences
|
|
roleParts = overlay(roleIdName placing '#' from length(roleIdName) + 1 - strpos(reverse(roleIdName), ':'));
|
|
objectTableFromRoleIdName = split_part(roleParts, '#', 1);
|
|
objectNameFromRoleIdName = split_part(roleParts, '#', 2);
|
|
roleTypeFromRoleIdName = split_part(roleParts, '#', 3);
|
|
objectUuidOfRole = rbac.findObjectUuidByIdName(objectTableFromRoleIdName, objectNameFromRoleIdName);
|
|
|
|
select uuid
|
|
from rbac.role
|
|
where objectUuid = objectUuidOfRole
|
|
and roleType = roleTypeFromRoleIdName
|
|
into roleUuid;
|
|
return roleUuid;
|
|
end; $$;
|
|
|
|
create or replace function rbac.findRoleId(roleDescriptor rbac.RoleDescriptor)
|
|
returns uuid
|
|
returns null on null input
|
|
language sql as $$
|
|
select uuid from rbac.role where objectUuid = roleDescriptor.objectUuid and roleType = roleDescriptor.roleType;
|
|
$$;
|
|
|
|
create or replace function rbac.getRoleId(roleDescriptor rbac.RoleDescriptor)
|
|
returns uuid
|
|
language plpgsql as $$
|
|
declare
|
|
roleUuid uuid;
|
|
begin
|
|
assert roleDescriptor is not null, 'roleDescriptor must not be null';
|
|
|
|
roleUuid := rbac.findRoleId(roleDescriptor);
|
|
if (roleUuid is null) then
|
|
raise exception 'rbac.role "%#%.%" not found', roleDescriptor.objectTable, roleDescriptor.objectUuid, roleDescriptor.roleType;
|
|
end if;
|
|
return roleUuid;
|
|
end;
|
|
$$;
|
|
--//
|
|
|
|
|
|
-- ============================================================================
|
|
--changeset michael.hoennig:rbac-base-BEFORE-DELETE-ROLE-TRIGGER endDelimiter:--//
|
|
-- ----------------------------------------------------------------------------
|
|
|
|
/*
|
|
rbac.role BEFORE DELETE TRIGGER function which deletes all related roles.
|
|
*/
|
|
create or replace function rbac.delete_grants_of_role_tf()
|
|
returns trigger
|
|
language plpgsql
|
|
strict as $$
|
|
begin
|
|
if TG_OP = 'DELETE' then
|
|
delete from rbac.grant g where old.uuid in (g.grantedbyroleuuid, g.ascendantuuid, g.descendantuuid);
|
|
else
|
|
raise exception 'invalid usage of TRIGGER BEFORE DELETE';
|
|
end if;
|
|
return old;
|
|
end; $$;
|
|
|
|
/*
|
|
Installs the rbac.role BEFORE DELETE TRIGGER.
|
|
*/
|
|
create trigger delete_grants_of_role_tg
|
|
before delete
|
|
on rbac.role
|
|
for each row
|
|
execute procedure rbac.delete_grants_of_role_tf();
|
|
--//
|
|
|
|
|
|
-- ============================================================================
|
|
--changeset michael.hoennig:rbac-base-BEFORE-DELETE-OBJECT-TRIGGER endDelimiter:--//
|
|
-- ----------------------------------------------------------------------------
|
|
|
|
/*
|
|
rbac.object BEFORE DELETE TRIGGER function which deletes all related roles.
|
|
*/
|
|
create or replace function rbac.delete_roles_of_object_tf()
|
|
returns trigger
|
|
language plpgsql
|
|
strict as $$
|
|
begin
|
|
if TG_OP = 'DELETE' then
|
|
delete from rbac.permission p where p.objectuuid = old.uuid;
|
|
delete from rbac.role r where r.objectUuid = old.uuid;
|
|
else
|
|
raise exception 'invalid usage of TRIGGER BEFORE DELETE';
|
|
end if;
|
|
return old;
|
|
end; $$;
|
|
|
|
/*
|
|
Installs the rbac.role BEFORE DELETE TRIGGER.
|
|
*/
|
|
create trigger delete_roles_of_object_tg
|
|
before delete
|
|
on rbac.object
|
|
for each row
|
|
execute procedure rbac.delete_roles_of_object_tf();
|
|
--//
|
|
|
|
|
|
-- ============================================================================
|
|
--changeset michael.hoennig:rbac-base-PERMISSION endDelimiter:--//
|
|
-- ----------------------------------------------------------------------------
|
|
create domain rbac.RbacOp as varchar(6)
|
|
check (
|
|
VALUE = 'DELETE'
|
|
or VALUE = 'UPDATE'
|
|
or VALUE = 'SELECT'
|
|
or VALUE = 'INSERT'
|
|
or VALUE = 'ASSUME'
|
|
);
|
|
|
|
create table rbac.permission
|
|
(
|
|
uuid uuid primary key references rbac.reference (uuid) on delete cascade,
|
|
objectUuid uuid not null references rbac.object,
|
|
op rbac.RbacOp not null,
|
|
opTableName varchar(60)
|
|
);
|
|
-- TODO.perf: check if these indexes are really useful
|
|
create index on rbac.permission (objectUuid, op);
|
|
create index on rbac.permission (opTableName, op);
|
|
|
|
ALTER TABLE rbac.permission
|
|
ADD CONSTRAINT unique_including_null_values UNIQUE NULLS NOT DISTINCT (objectUuid, op, opTableName);
|
|
|
|
call base.create_journal('rbac.permission');
|
|
|
|
create or replace function rbac.createPermission(forObjectUuid uuid, forOp rbac.RbacOp, forOpTableName text = null)
|
|
returns uuid
|
|
language plpgsql as $$
|
|
declare
|
|
permissionUuid uuid;
|
|
begin
|
|
if (forObjectUuid is null) then
|
|
raise exception 'forObjectUuid must not be null';
|
|
end if;
|
|
if (forOp = 'INSERT' and forOpTableName is null) then
|
|
raise exception 'INSERT permissions needs forOpTableName';
|
|
end if;
|
|
if (forOp <> 'INSERT' and forOpTableName is not null) then
|
|
raise exception 'forOpTableName must only be specified for ops: [INSERT]'; -- currently no other
|
|
end if;
|
|
|
|
permissionUuid := (
|
|
select uuid from rbac.permission
|
|
where objectUuid = forObjectUuid
|
|
and op = forOp and opTableName is not distinct from forOpTableName);
|
|
if (permissionUuid is null) then
|
|
insert into rbac.reference ("type")
|
|
values ('rbac.permission')
|
|
returning uuid into permissionUuid;
|
|
begin
|
|
insert into rbac.permission (uuid, objectUuid, op, opTableName)
|
|
values (permissionUuid, forObjectUuid, forOp, forOpTableName);
|
|
exception
|
|
when others then
|
|
raise exception 'insert into rbac.permission (uuid, objectUuid, op, opTableName)
|
|
values (%, %, %, %);', permissionUuid, forObjectUuid, forOp, forOpTableName;
|
|
end;
|
|
end if;
|
|
return permissionUuid;
|
|
end; $$;
|
|
|
|
create or replace function rbac.findEffectivePermissionId(forObjectUuid uuid, forOp rbac.RbacOp, forOpTableName text = null)
|
|
returns uuid
|
|
returns null on null input
|
|
stable -- leakproof
|
|
language sql as $$
|
|
select uuid
|
|
from rbac.permission p
|
|
where p.objectUuid = forObjectUuid
|
|
and (forOp = 'SELECT' or p.op = forOp) -- all other rbac.RbacOp include 'SELECT'
|
|
and p.opTableName = forOpTableName
|
|
$$;
|
|
|
|
create or replace function rbac.findPermissionId(forObjectUuid uuid, forOp rbac.RbacOp, forOpTableName text = null)
|
|
returns uuid
|
|
returns null on null input
|
|
stable -- leakproof
|
|
language sql as $$
|
|
select uuid
|
|
from rbac.permission p
|
|
where p.objectUuid = forObjectUuid
|
|
and p.op = forOp
|
|
and p.opTableName = forOpTableName
|
|
$$;
|
|
|
|
create or replace function rbac.getPermissionId(forObjectUuid uuid, forOp rbac.RbacOp, forOpTableName text = null)
|
|
returns uuid
|
|
stable -- leakproof
|
|
language plpgsql as $$
|
|
declare
|
|
permissionUuid uuid;
|
|
begin
|
|
select uuid into permissionUuid
|
|
from rbac.permission p
|
|
where p.objectUuid = forObjectUuid
|
|
and p.op = forOp
|
|
and forOpTableName is null or p.opTableName = forOpTableName;
|
|
assert permissionUuid is not null,
|
|
format('permission %s %s for object UUID %s cannot be found', forOp, forOpTableName, forObjectUuid);
|
|
return permissionUuid;
|
|
end; $$;
|
|
--//
|
|
|
|
|
|
-- ============================================================================
|
|
--changeset michael.hoennig:rbac-base-duplicate-role-grant-exception endDelimiter:--//
|
|
-- ----------------------------------------------------------------------------
|
|
|
|
create or replace procedure rbac.raiseDuplicateRoleGrantException(subRoleId uuid, superRoleId uuid)
|
|
language plpgsql as $$
|
|
declare
|
|
subRoleIdName text;
|
|
superRoleIdName text;
|
|
begin
|
|
select roleIdName from rbac.role_ev where uuid=subRoleId into subRoleIdName;
|
|
select roleIdName from rbac.role_ev where uuid=superRoleId into superRoleIdName;
|
|
raise exception '[400] Duplicate role grant detected: role % (%) already granted to % (%)', subRoleId, subRoleIdName, superRoleId, superRoleIdName;
|
|
end;
|
|
$$;
|
|
--//
|
|
|
|
|
|
-- ============================================================================
|
|
--changeset michael.hoennig:rbac-base-GRANTS endDelimiter:--//
|
|
-- ----------------------------------------------------------------------------
|
|
/*
|
|
Table to store grants / role- or permission assignments to subjects or roles.
|
|
*/
|
|
create table rbac.grant
|
|
(
|
|
uuid uuid primary key default uuid_generate_v4(),
|
|
grantedByTriggerOf uuid references rbac.object (uuid) on delete cascade initially deferred ,
|
|
grantedByRoleUuid uuid references rbac.role (uuid),
|
|
ascendantUuid uuid references rbac.reference (uuid),
|
|
descendantUuid uuid references rbac.reference (uuid),
|
|
assumed boolean not null default true, -- auto assumed (true) vs. needs assumeRoles (false)
|
|
unique (ascendantUuid, descendantUuid),
|
|
constraint rbacGrant_createdBy check ( grantedByRoleUuid is null or grantedByTriggerOf is null) );
|
|
create index on rbac.grant (ascendantUuid);
|
|
create index on rbac.grant (descendantUuid);
|
|
|
|
call base.create_journal('rbac.grant');
|
|
create or replace function rbac.findGrantees(grantedId uuid)
|
|
returns setof rbac.reference
|
|
returns null on null input
|
|
language sql as $$
|
|
with recursive grants as (
|
|
select descendantUuid, ascendantUuid
|
|
from rbac.grant
|
|
where descendantUuid = grantedId
|
|
union all
|
|
select g.descendantUuid, g.ascendantUuid
|
|
from rbac.grant g
|
|
inner join grants on grants.ascendantUuid = g.descendantUuid
|
|
)
|
|
select ref.*
|
|
from grants
|
|
join rbac.reference ref on ref.uuid = grants.ascendantUuid;
|
|
$$;
|
|
|
|
create or replace function rbac.isGranted(granteeIds uuid[], grantedId uuid)
|
|
returns bool
|
|
returns null on null input
|
|
language sql as $$
|
|
with recursive grants as (
|
|
select descendantUuid, ascendantUuid
|
|
from rbac.grant
|
|
where descendantUuid = grantedId
|
|
union all
|
|
select "grant".descendantUuid, "grant".ascendantUuid
|
|
from rbac.grant "grant"
|
|
inner join grants recur on recur.ascendantUuid = "grant".descendantUuid
|
|
)
|
|
select exists (
|
|
select true
|
|
from grants
|
|
where ascendantUuid = any(granteeIds)
|
|
) or grantedId = any(granteeIds);
|
|
$$;
|
|
|
|
create or replace function rbac.isGranted(granteeId uuid, grantedId uuid)
|
|
returns bool
|
|
returns null on null input
|
|
language sql as $$
|
|
select * from rbac.isGranted(array[granteeId], grantedId);
|
|
$$;
|
|
create or replace function rbac.isPermissionGrantedToSubject(permissionId uuid, subjectId uuid)
|
|
returns BOOL
|
|
stable -- leakproof
|
|
language sql as $$
|
|
with recursive grants as (
|
|
select descendantUuid, ascendantUuid
|
|
from rbac.grant
|
|
where descendantUuid = permissionId
|
|
union all
|
|
select g.descendantUuid, g.ascendantUuid
|
|
from rbac.grant g
|
|
inner join grants on grants.ascendantUuid = g.descendantUuid
|
|
)
|
|
select exists(
|
|
select true
|
|
from grants
|
|
where ascendantUuid = subjectId
|
|
);
|
|
$$;
|
|
|
|
create or replace function rbac.hasInsertPermission(objectUuid uuid, tableName text )
|
|
returns BOOL
|
|
stable -- leakproof
|
|
language plpgsql as $$
|
|
declare
|
|
permissionUuid uuid;
|
|
begin
|
|
permissionUuid = rbac.findPermissionId(objectUuid, 'INSERT'::rbac.RbacOp, tableName);
|
|
return permissionUuid is not null;
|
|
end;
|
|
$$;
|
|
|
|
create or replace function rbac.hasGlobalRoleGranted(forAscendantUuid uuid)
|
|
returns bool
|
|
stable -- leakproof
|
|
language sql as $$
|
|
select exists(
|
|
select r.uuid
|
|
from rbac.grant as g
|
|
join rbac.role as r on r.uuid = g.descendantuuid
|
|
join rbac.object as o on o.uuid = r.objectuuid
|
|
where g.ascendantuuid = forAscendantUuid
|
|
and o.objecttable = 'rbac.global'
|
|
);
|
|
$$;
|
|
|
|
create or replace procedure rbac.grantPermissionToRole(permissionUuid uuid, roleUuid uuid)
|
|
language plpgsql as $$
|
|
begin
|
|
perform rbac.assertReferenceType('roleId (ascendant)', roleUuid, 'rbac.role');
|
|
perform rbac.assertReferenceType('permissionId (descendant)', permissionUuid, 'rbac.permission');
|
|
|
|
insert
|
|
into rbac.grant (grantedByTriggerOf, ascendantUuid, descendantUuid, assumed)
|
|
values (rbac.currentTriggerObjectUuid(), roleUuid, permissionUuid, true)
|
|
on conflict do nothing; -- allow granting multiple times
|
|
end;
|
|
$$;
|
|
|
|
create or replace procedure rbac.grantPermissionToRole(permissionUuid uuid, roleDesc rbac.RoleDescriptor)
|
|
language plpgsql as $$
|
|
begin
|
|
call rbac.grantPermissionToRole(permissionUuid, rbac.findRoleId(roleDesc));
|
|
end;
|
|
$$;
|
|
|
|
create or replace procedure rbac.grantRoleToRole(subRoleId uuid, superRoleId uuid, doAssume bool = true)
|
|
language plpgsql as $$
|
|
begin
|
|
perform rbac.assertReferenceType('superRoleId (ascendant)', superRoleId, 'rbac.role');
|
|
perform rbac.assertReferenceType('subRoleId (descendant)', subRoleId, 'rbac.role');
|
|
|
|
if rbac.isGranted(subRoleId, superRoleId) then
|
|
call rbac.raiseDuplicateRoleGrantException(subRoleId, superRoleId);
|
|
end if;
|
|
|
|
insert
|
|
into rbac.grant (grantedByTriggerOf, ascendantuuid, descendantUuid, assumed)
|
|
values (rbac.currentTriggerObjectUuid(), superRoleId, subRoleId, doAssume)
|
|
on conflict do nothing; -- allow granting multiple times
|
|
end; $$;
|
|
|
|
|
|
create or replace procedure rbac.grantRoleToRole(subRole rbac.RoleDescriptor, superRole rbac.RoleDescriptor, doAssume bool = true)
|
|
language plpgsql as $$
|
|
declare
|
|
superRoleId uuid;
|
|
subRoleId uuid;
|
|
begin
|
|
-- TODO.refa: maybe separate method rbac.grantRoleToRoleIfNotNull(...) for NULLABLE references
|
|
if superRole.objectUuid is null or subRole.objectuuid is null then
|
|
return;
|
|
end if;
|
|
|
|
superRoleId := rbac.findRoleId(superRole);
|
|
subRoleId := rbac.findRoleId(subRole);
|
|
|
|
perform rbac.assertReferenceType('superRoleId (ascendant)', superRoleId, 'rbac.role');
|
|
perform rbac.assertReferenceType('subRoleId (descendant)', subRoleId, 'rbac.role');
|
|
|
|
if rbac.isGranted(subRoleId, superRoleId) then
|
|
call rbac.raiseDuplicateRoleGrantException(subRoleId, superRoleId);
|
|
end if;
|
|
|
|
insert
|
|
into rbac.grant (grantedByTriggerOf, ascendantuuid, descendantUuid, assumed)
|
|
values (rbac.currentTriggerObjectUuid(), superRoleId, subRoleId, doAssume)
|
|
on conflict do nothing; -- allow granting multiple times
|
|
end; $$;
|
|
|
|
create or replace procedure rbac.revokeRoleFromRole(subRole rbac.RoleDescriptor, superRole rbac.RoleDescriptor)
|
|
language plpgsql as $$
|
|
declare
|
|
superRoleId uuid;
|
|
subRoleId uuid;
|
|
begin
|
|
superRoleId := rbac.findRoleId(superRole);
|
|
subRoleId := rbac.findRoleId(subRole);
|
|
|
|
perform rbac.assertReferenceType('superRoleId (ascendant)', superRoleId, 'rbac.role');
|
|
perform rbac.assertReferenceType('subRoleId (descendant)', subRoleId, 'rbac.role');
|
|
|
|
if (rbac.isGranted(superRoleId, subRoleId)) then
|
|
delete from rbac.grant where ascendantUuid = superRoleId and descendantUuid = subRoleId;
|
|
else
|
|
raise exception 'cannot revoke role % (%) from % (%) because it is not granted',
|
|
subRole, subRoleId, superRole, superRoleId;
|
|
end if;
|
|
end; $$;
|
|
|
|
create or replace procedure rbac.revokePermissionFromRole(permissionId UUID, superRole rbac.RoleDescriptor)
|
|
language plpgsql as $$
|
|
declare
|
|
superRoleId uuid;
|
|
permissionOp text;
|
|
objectTable text;
|
|
objectUuid uuid;
|
|
begin
|
|
superRoleId := rbac.findRoleId(superRole);
|
|
|
|
perform rbac.assertReferenceType('superRoleId (ascendant)', superRoleId, 'rbac.role');
|
|
perform rbac.assertReferenceType('permission (descendant)', permissionId, 'rbac.permission');
|
|
|
|
if (rbac.isGranted(superRoleId, permissionId)) then
|
|
delete from rbac.grant where ascendantUuid = superRoleId and descendantUuid = permissionId;
|
|
else
|
|
select p.op, o.objectTable, o.uuid
|
|
from rbac.grant g
|
|
join rbac.permission p on p.uuid=g.descendantUuid
|
|
join rbac.object o on o.uuid=p.objectUuid
|
|
where g.uuid=permissionId
|
|
into permissionOp, objectTable, objectUuid;
|
|
|
|
raise exception 'cannot revoke permission % (% on %#% (%) from % (%)) because it is not granted',
|
|
permissionId, permissionOp, objectTable, objectUuid, permissionId, superRole, superRoleId;
|
|
end if;
|
|
end; $$;
|
|
|
|
-- ============================================================================
|
|
--changeset michael.hoennig:rbac-base-QUERY-ACCESSIBLE-OBJECT-UUIDS runOnChange=true endDelimiter:--//
|
|
-- ----------------------------------------------------------------------------
|
|
/*
|
|
|
|
*/
|
|
create or replace function rbac.queryAccessibleObjectUuidsOfSubjectIds(
|
|
requiredOp rbac.RbacOp,
|
|
forObjectTable varchar,
|
|
subjectIds uuid[],
|
|
maxObjects integer = 8000)
|
|
returns setof uuid
|
|
returns null on null input
|
|
language plpgsql as $$
|
|
declare
|
|
foundRows bigint;
|
|
begin
|
|
return query
|
|
WITH RECURSIVE grants AS (
|
|
SELECT descendantUuid, ascendantUuid, 1 AS level
|
|
FROM rbac.grant
|
|
WHERE assumed
|
|
AND ascendantUuid = any(subjectIds)
|
|
UNION ALL
|
|
SELECT g.descendantUuid, g.ascendantUuid, grants.level + 1 AS level
|
|
FROM rbac.grant g
|
|
INNER JOIN grants ON grants.descendantUuid = g.ascendantUuid
|
|
WHERE g.assumed
|
|
),
|
|
granted AS (
|
|
SELECT DISTINCT descendantUuid
|
|
FROM grants
|
|
)
|
|
SELECT DISTINCT perm.objectUuid
|
|
FROM granted
|
|
JOIN rbac.permission perm ON granted.descendantUuid = perm.uuid
|
|
JOIN rbac.object obj ON obj.uuid = perm.objectUuid
|
|
WHERE (requiredOp = 'SELECT' OR perm.op = requiredOp)
|
|
AND obj.objectTable = forObjectTable
|
|
LIMIT maxObjects+1;
|
|
|
|
foundRows = base.lastRowCount();
|
|
if foundRows > maxObjects then
|
|
raise exception '[400] Too many accessible objects, limit is %, found %.', maxObjects, foundRows
|
|
using
|
|
errcode = 'P0003',
|
|
hint = 'Please assume a sub-role and try again.';
|
|
end if;
|
|
end;
|
|
$$;
|
|
--//
|
|
|
|
-- ============================================================================
|
|
--changeset michael.hoennig:rbac-base-QUERY-GRANTED-PERMISSIONS endDelimiter:--//
|
|
-- ----------------------------------------------------------------------------
|
|
/*
|
|
Returns all permissions accessible to the given subject UUID (subject or role).
|
|
*/
|
|
create or replace function rbac.queryPermissionsGrantedToSubjectId(subjectId uuid)
|
|
returns setof rbac.permission
|
|
strict
|
|
language sql as $$
|
|
with recursive grants as (
|
|
select descendantUuid, ascendantUuid
|
|
from rbac.grant
|
|
where ascendantUuid = subjectId
|
|
union all
|
|
select g.descendantUuid, g.ascendantUuid
|
|
from rbac.grant g
|
|
inner join grants on grants.descendantUuid = g.ascendantUuid
|
|
)
|
|
select perm.*
|
|
from rbac.permission perm
|
|
where perm.uuid in (
|
|
select descendantUuid
|
|
from grants
|
|
);
|
|
$$;
|
|
|
|
--//
|
|
|
|
-- ============================================================================
|
|
--changeset michael.hoennig:rbac-base-QUERY-SUBJECTS-WITH-PERMISSION-FOR-OBJECT endDelimiter:--//
|
|
-- ----------------------------------------------------------------------------
|
|
/*
|
|
Returns all subject UUIDs which have any permission for the given object UUID.
|
|
*/
|
|
|
|
create or replace function rbac.queryAllRbacSubjectsWithPermissionsFor(objectId uuid)
|
|
returns setof rbac.subject
|
|
returns null on null input
|
|
language sql as $$
|
|
select *
|
|
from rbac.subject
|
|
where uuid in (
|
|
-- @formatter:off
|
|
with recursive grants as (
|
|
select descendantUuid, ascendantUuid
|
|
from rbac.grant
|
|
where descendantUuid = objectId
|
|
union all
|
|
select "grant".descendantUuid, "grant".ascendantUuid
|
|
from rbac.grant "grant"
|
|
inner join grants recur on recur.ascendantUuid = "grant".descendantUuid
|
|
)
|
|
-- @formatter:on
|
|
select ascendantUuid
|
|
from grants);
|
|
$$;
|
|
--//
|
|
|
|
|
|
-- ============================================================================
|
|
--changeset michael.hoennig:rbac-base-PGSQL-ROLES runOnChange:true validCheckSum:ANY context:!external-db endDelimiter:--//
|
|
-- ----------------------------------------------------------------------------
|
|
|
|
do $$
|
|
begin
|
|
if '${HSADMINNG_POSTGRES_ADMIN_USERNAME}'='admin' then
|
|
if not exists (select from pg_catalog.pg_roles where rolname = 'admin') then
|
|
create role admin;
|
|
end if;
|
|
grant all privileges on all tables in schema public to admin;
|
|
end if;
|
|
|
|
if '${HSADMINNG_POSTGRES_RESTRICTED_USERNAME}'='restricted' then
|
|
if not exists (select from pg_catalog.pg_roles where rolname = 'restricted') then
|
|
create role restricted;
|
|
end if;
|
|
|
|
grant all privileges on all tables in schema public to restricted;
|
|
end if;
|
|
end $$;
|
|
--//
|