introduce referrer role and support explict columns for restricted view

This commit is contained in:
Michael Hoennig 2024-03-24 11:01:45 +01:00
parent fbe2204d72
commit c9f7d8ec2d
13 changed files with 202 additions and 178 deletions

View File

@ -94,4 +94,17 @@ public class RbacGrantController implements RbacGrantsApi {
return ResponseEntity.noContent().build();
}
// TODO: implement an endpoint to create a Mermaid flowchart with all grants of a given user
// @GetMapping(
// path = "/api/rbac/users/{userUuid}/grants",
// produces = {"text/vnd.mermaid"})
// @Transactional(readOnly = true)
// public ResponseEntity<String> allGrantsOfUserAsMermaid(
// @RequestHeader(name = "current-user") String currentUser,
// @RequestHeader(name = "assumed-roles", required = false) String assumedRoles) {
// final var graph = RbacGrantsDiagramService.allGrantsToUser(currentUser);
// return ResponseEntity.ok(graph);
// }
}

View File

@ -1,5 +1,5 @@
package net.hostsharing.hsadminng.rbac.rbacrole;
public enum RbacRoleType {
owner, admin, agent, tenant, guest
owner, admin, agent, tenant, guest, referrer
}

View File

@ -0,0 +1,20 @@
--liquibase formatted sql
-- ============================================================================
-- TABLE-COLUMNS-FUNCTION
--changeset table-columns-function:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
create or replace function columnsNames( tableName text )
returns text
stable
language 'plpgsql' as $$
declare columns text[];
begin
columns := (select array(select column_name::text
from information_schema.columns
where table_name = tableName));
return array_to_string(columns, ', ');
end; $$
--//

View File

@ -164,7 +164,7 @@ end; $$;
*/
create type RbacRoleType as enum ('owner', 'admin', 'agent', 'tenant', 'guest');
create type RbacRoleType as enum ('owner', 'admin', 'agent', 'tenant', 'guest', 'referrer');
create table RbacRole
(
@ -373,10 +373,12 @@ create table RbacPermission
uuid uuid primary key references RbacReference (uuid) on delete cascade,
objectUuid uuid not null references RbacObject,
op RbacOp not null,
opTableName varchar(60),
unique (objectUuid, op)
opTableName varchar(60)
);
ALTER TABLE RbacPermission
ADD CONSTRAINT RbacPermission_uc UNIQUE NULLS NOT DISTINCT (objectUuid, op, opTableName);
call create_journal('RbacPermission');
create or replace function createPermission(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null)
@ -395,7 +397,10 @@ begin
raise exception 'forOpTableName must only be specified for ops: [INSERT]'; -- currently no other
end if;
permissionUuid = (select uuid from RbacPermission where objectUuid = forObjectUuid and op = forOp and opTableName = forOpTableName);
permissionUuid := (
select uuid from RbacPermission
where objectUuid = forObjectUuid
and op = forOp and opTableName is not distinct from forOpTableName);
if (permissionUuid is null) then
insert into RbacReference ("type")
values ('RbacPermission')
@ -466,8 +471,44 @@ select uuid
and p.op = forOp
and p.opTableName = forOpTableName
$$;
create or replace function getPermissionId(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null)
returns uuid
stable -- leakproof
language plpgsql as $$
declare
permissionUuid uuid;
begin
select uuid into permissionUuid
from RbacPermission 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 rbac-base-duplicate-role-grant-exception:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
create or replace procedure raiseDuplicateRoleGrantException(subRoleId uuid, superRoleId uuid)
language plpgsql as $$
declare
subRoleIdName text;
superRoleIdName text;
begin
select roleIdName from rbacRole_ev where uuid=subRoleId into subRoleIdName;
select roleIdName from rbacRole_ev where uuid=superRoleId into superRoleIdName;
raise exception '[400] Duplicate role grant detected: role % (%) already granted to % (%)', subRoleId, subRoleIdName, superRoleId, superRoleIdName;
end;
$$;
--//
-- ============================================================================
--changeset rbac-base-GRANTS:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
@ -634,7 +675,7 @@ begin
perform assertReferenceType('subRoleId (descendant)', subRoleId, 'RbacRole');
if isGranted(subRoleId, superRoleId) then
raise exception '[400] Cyclic role grant detected between % and %', subRoleId, superRoleId;
call raiseDuplicateRoleGrantException(subRoleId, superRoleId);
end if;
insert
@ -650,6 +691,11 @@ declare
superRoleId uuid;
subRoleId uuid;
begin
-- FIXME: maybe separate method grantRoleToRoleIfNotNull(...)?
if superRole.objectUuid is null or subRole.objectuuid is null then
return;
end if;
superRoleId := findRoleId(superRole);
subRoleId := findRoleId(subRole);
@ -657,7 +703,7 @@ begin
perform assertReferenceType('subRoleId (descendant)', subRoleId, 'RbacRole');
if isGranted(subRoleId, superRoleId) then
raise exception '[400] Cyclic role grant detected between % and %', subRoleId, superRoleId;
call raiseDuplicateRoleGrantException(subRoleId, superRoleId);
end if;
insert
@ -672,6 +718,7 @@ declare
superRoleId uuid;
subRoleId uuid;
begin
if ( superRoleId is null ) then return; end if;
superRoleId := findRoleId(superRole);
if ( subRoleId is null ) then return; end if;
subRoleId := findRoleId(subRole);
@ -680,7 +727,7 @@ begin
perform assertReferenceType('subRoleId (descendant)', subRoleId, 'RbacRole');
if isGranted(subRoleId, superRoleId) then
raise exception '[400] Cyclic role grant detected between % and %', subRoleId, superRoleId;
call raiseDuplicateRoleGrantException(subRoleId, superRoleId);
end if;
insert
@ -704,11 +751,39 @@ begin
if (isGranted(superRoleId, subRoleId)) then
delete from RbacGrants where ascendantUuid = superRoleId and descendantUuid = subRoleId;
else
raise exception 'cannot revoke role % (%) from % (% because it is not granted',
raise exception 'cannot revoke role % (%) from % (%) because it is not granted',
subRole, subRoleId, superRole, superRoleId;
end if;
end; $$;
create or replace procedure revokePermissionFromRole(permissionId UUID, superRole RbacRoleDescriptor)
language plpgsql as $$
declare
superRoleId uuid;
permissionOp text;
objectTable text;
objectUuid uuid;
begin
superRoleId := findRoleId(superRole);
perform assertReferenceType('superRoleId (ascendant)', superRoleId, 'RbacRole');
perform assertReferenceType('permission (descendant)', permissionId, 'RbacPermission');
if (isGranted(superRoleId, permissionId)) then
delete from RbacGrants where ascendantUuid = superRoleId and descendantUuid = permissionId;
else
select p.op, o.objectTable, o.uuid
from rbacGrants g
join rbacPermission p on p.uuid=g.descendantUuid
join rbacobject 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 rbac-base-QUERY-ACCESSIBLE-OBJECT-UUIDS:1 endDelimiter:--//
-- ----------------------------------------------------------------------------

View File

@ -56,14 +56,17 @@ begin
roleTypeToAssume = split_part(roleNameParts, '#', 3);
objectUuidToAssume = findObjectUuidByIdName(objectTableToAssume, objectNameToAssume);
if objectUuidToAssume is null then
raise exception '[401] object % cannot be found in table %', objectNameToAssume, objectTableToAssume;
end if;
select uuid as roleuuidToAssume
select uuid
from RbacRole r
where r.objectUuid = objectUuidToAssume
and r.roleType = roleTypeToAssume
into roleUuidToAssume;
if roleUuidToAssume is null then
raise exception '[403] role % not accessible for user %', roleName, currentSubjects();
raise exception '[403] role % does not exist or is not accessible for user %', roleName, currentUser();
end if;
if not isGranted(currentUserUuid, roleUuidToAssume) then
raise exception '[403] user % has no permission to assume role %', currentUser(), roleName;

View File

@ -31,13 +31,13 @@ create or replace function createRoleWithGrants(
called on null input
language plpgsql as $$
declare
roleUuid uuid;
subRoleDesc RbacRoleDescriptor;
superRoleDesc RbacRoleDescriptor;
subRoleUuid uuid;
superRoleUuid uuid;
userUuid uuid;
grantedByRoleUuid uuid;
roleUuid uuid;
subRoleDesc RbacRoleDescriptor;
superRoleDesc RbacRoleDescriptor;
subRoleUuid uuid;
superRoleUuid uuid;
userUuid uuid;
userGrantsByRoleUuid uuid;
begin
roleUuid := createRole(roleDescriptor);
@ -58,14 +58,15 @@ begin
end loop;
if cardinality(userUuids) > 0 then
-- direct grants to users need a grantedByRole which can revoke the grant
if grantedByRole is null then
grantedByRoleUuid := roleUuid;
userGrantsByRoleUuid := roleUuid; -- FIXME: or do we want to require an explicit userGrantsByRoleUuid?
else
grantedByRoleUuid := getRoleId(grantedByRole);
userGrantsByRoleUuid := getRoleId(grantedByRole);
end if;
foreach userUuid in array userUuids
loop
call grantRoleToUserUnchecked(grantedByRoleUuid, roleUuid, userUuid);
call grantRoleToUserUnchecked(userGrantsByRoleUuid, roleUuid, userUuid);
end loop;
end if;

View File

@ -73,6 +73,7 @@ begin
return roleDescriptor('%2$s', entity.uuid, 'tenant', assumed);
end; $f$;
-- TODO: remove guest role
create or replace function %1$sGuest(entity %2$s, assumed boolean = true)
returns RbacRoleDescriptor
language plpgsql
@ -81,6 +82,14 @@ begin
return roleDescriptor('%2$s', entity.uuid, 'guest', assumed);
end; $f$;
create or replace function %1$sReferrer(entity %2$s)
returns RbacRoleDescriptor
language plpgsql
strict as $f$
begin
return roleDescriptor('%2$s', entity.uuid, 'referrer');
end; $f$;
$sql$, prefix, targetTable);
execute sql;
end; $$;
@ -148,12 +157,16 @@ end; $$;
--changeset rbac-generators-RESTRICTED-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
create or replace procedure generateRbacRestrictedView(targetTable text, orderBy text, columnUpdates text = null)
create or replace procedure generateRbacRestrictedView(targetTable text, orderBy text, columnUpdates text = null, columnNames text = '*')
language plpgsql as $$
declare
sql text;
newColumns text;
begin
targetTable := lower(targetTable);
if columnNames = '*' then
columnNames := columnsNames(targetTable);
end if;
/*
Creates a restricted view based on the 'SELECT' permission of the current subject.
@ -175,20 +188,21 @@ begin
/**
Instead of insert trigger function for the restricted view.
*/
newColumns := 'new.' || replace(columnNames, ',', ', new.');
sql := format($sql$
create or replace function %1$sInsert()
returns trigger
language plpgsql as $f$
declare
newTargetRow %1$s;
begin
insert
into %1$s
values (new.*)
returning * into newTargetRow;
return newTargetRow;
end; $f$;
$sql$, targetTable);
create or replace function %1$sInsert()
returns trigger
language plpgsql as $f$
declare
newTargetRow %1$s;
begin
insert
into %1$s (%2$s)
values (%3$s)
returning * into newTargetRow;
return newTargetRow;
end; $f$;
$sql$, targetTable, columnNames, newColumns);
execute sql;
/*

View File

@ -118,9 +118,32 @@ select 'global', (select uuid from RbacObject where objectTable = 'global'), 'ad
$$;
begin transaction;
call defineContext('creating global admin role', null, null, null);
select createRole(globalAdmin());
call defineContext('creating global admin role', null, null, null);
select createRole(globalAdmin());
commit;
--//
-- ============================================================================
--changeset rbac-global-GUEST-ROLE:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
A global guest role.
*/
create or replace function globalGuest(assumed boolean = true)
returns RbacRoleDescriptor
returns null on null input
stable -- leakproof
language sql as $$
select 'global', (select uuid from RbacObject where objectTable = 'global'), 'guest'::RbacRoleType, assumed;
$$;
begin transaction;
call defineContext('creating global guest role', null, null, null);
select createRole(globalGuest());
commit;
--//
-- ============================================================================
--changeset rbac-global-ADMIN-USERS:1 context:dev,tc endDelimiter:--//

View File

@ -38,8 +38,6 @@ begin
SELECT * FROM test_package p
WHERE p.uuid= NEW.packageUuid
INTO newPackage;
assert newPackage.uuid is not null, format('newPackage must not be null for NEW.packageUuid = %s', NEW.packageUuid);
perform createRoleWithGrants(
testDomainOwner(NEW),
@ -91,41 +89,12 @@ create or replace procedure updateRbacRulesForTestDomain(
NEW test_domain
)
language plpgsql as $$
declare
oldPackage test_package;
newPackage test_package;
begin
call enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM test_package p
WHERE p.uuid= OLD.packageUuid
INTO oldPackage;
assert oldPackage.uuid is not null, format('oldPackage must not be null for OLD.packageUuid = %s', OLD.packageUuid);
SELECT * FROM test_package p
WHERE p.uuid= NEW.packageUuid
INTO newPackage;
assert newPackage.uuid is not null, format('newPackage must not be null for NEW.packageUuid = %s', NEW.packageUuid);
if NEW.packageUuid <> OLD.packageUuid then
call revokePermissionFromRole(getPermissionId(OLD.uuid, 'INSERT'), testPackageAdmin(oldPackage));
call revokeRoleFromRole(testDomainOwner(OLD), testPackageAdmin(oldPackage));
call grantRoleToRole(testDomainOwner(NEW), testPackageAdmin(newPackage));
call revokeRoleFromRole(testPackageTenant(oldPackage), testDomainOwner(OLD));
call grantRoleToRole(testPackageTenant(newPackage), testDomainOwner(NEW));
call revokeRoleFromRole(testPackageTenant(oldPackage), testDomainAdmin(OLD));
call grantRoleToRole(testPackageTenant(newPackage), testDomainAdmin(NEW));
if NEW.packageUuid is distinct from OLD.packageUuid then
delete from rbacgrants g where g.grantedbytriggerof = OLD.uuid;
call buildRbacSystemForTestDomain(NEW);
end if;
call leaveTriggerForObjectUuid(NEW.uuid);
end; $$;
/*

View File

@ -38,14 +38,10 @@ begin
call enterTriggerForObjectUuid(NEW.uuid);
select * from hs_office_person as p where p.uuid = NEW.holderUuid INTO newHolderPerson;
assert newHolderPerson.uuid is not null, format('newHolderPerson must not be null for NEW.holderUuid = %s', NEW.holderUuid);
select * from hs_office_person as p where p.uuid = NEW.anchorUuid INTO newAnchorPerson;
assert newAnchorPerson.uuid is not null, format('newAnchorPerson must not be null for NEW.anchorUuid = %s', NEW.anchorUuid);
select * from hs_office_contact as c where c.uuid = NEW.contactUuid INTO newContact;
assert newContact.uuid is not null, format('newContact must not be null for NEW.contactUuid = %s', NEW.contactUuid);
perform createRoleWithGrants(
hsOfficeRelationOwner(NEW),
@ -73,13 +69,13 @@ begin
hsOfficeRelationTenant(NEW),
permissions => array['SELECT'],
incomingSuperRoles => array[
hsOfficeRelationAgent(NEW),
hsOfficeContactAdmin(newContact),
hsOfficeRelationAgent(NEW),
hsOfficePersonAdmin(newHolderPerson)],
outgoingSubRoles => array[
hsOfficePersonReferrer(newHolderPerson),
hsOfficeContactReferrer(newContact),
hsOfficePersonReferrer(newAnchorPerson)]
hsOfficePersonReferrer(newAnchorPerson),
hsOfficePersonReferrer(newHolderPerson)]
);
call leaveTriggerForObjectUuid(NEW.uuid);
@ -118,48 +114,12 @@ create or replace procedure updateRbacRulesForHsOfficeRelation(
NEW hs_office_relation
)
language plpgsql as $$
declare
oldHolderPerson hs_office_person;
newHolderPerson hs_office_person;
oldAnchorPerson hs_office_person;
newAnchorPerson hs_office_person;
oldContact hs_office_contact;
newContact hs_office_contact;
begin
call enterTriggerForObjectUuid(NEW.uuid);
select * from hs_office_person as p where p.uuid = OLD.holderUuid INTO oldHolderPerson;
assert oldHolderPerson.uuid is not null, format('oldHolderPerson must not be null for OLD.holderUuid = %s', OLD.holderUuid);
select * from hs_office_person as p where p.uuid = NEW.holderUuid INTO newHolderPerson;
assert newHolderPerson.uuid is not null, format('newHolderPerson must not be null for NEW.holderUuid = %s', NEW.holderUuid);
select * from hs_office_person as p where p.uuid = OLD.anchorUuid INTO oldAnchorPerson;
assert oldAnchorPerson.uuid is not null, format('oldAnchorPerson must not be null for OLD.anchorUuid = %s', OLD.anchorUuid);
select * from hs_office_person as p where p.uuid = NEW.anchorUuid INTO newAnchorPerson;
assert newAnchorPerson.uuid is not null, format('newAnchorPerson must not be null for NEW.anchorUuid = %s', NEW.anchorUuid);
select * from hs_office_contact as c where c.uuid = OLD.contactUuid INTO oldContact;
assert oldContact.uuid is not null, format('oldContact must not be null for OLD.contactUuid = %s', OLD.contactUuid);
select * from hs_office_contact as c where c.uuid = NEW.contactUuid INTO newContact;
assert newContact.uuid is not null, format('newContact must not be null for NEW.contactUuid = %s', NEW.contactUuid);
if NEW.contactUuid <> OLD.contactUuid then
call revokeRoleFromRole(hsOfficeRelationTenant(OLD), hsOfficeContactAdmin(oldContact));
call grantRoleToRole(hsOfficeRelationTenant(NEW), hsOfficeContactAdmin(newContact));
call revokeRoleFromRole(hsOfficeContactReferrer(oldContact), hsOfficeRelationTenant(OLD));
call grantRoleToRole(hsOfficeContactReferrer(newContact), hsOfficeRelationTenant(NEW));
if NEW.contactUuid is distinct from OLD.contactUuid then
delete from rbacgrants g where g.grantedbytriggerof = OLD.uuid;
call buildRbacSystemForHsOfficeRelation(NEW);
end if;
call leaveTriggerForObjectUuid(NEW.uuid);
end; $$;
/*

View File

@ -57,8 +57,8 @@ begin
hsOfficeSepaMandateAgent(NEW),
incomingSuperRoles => array[hsOfficeSepaMandateAdmin(NEW)],
outgoingSubRoles => array[
hsOfficeBankAccountReferrer(newBankAccount),
hsOfficeRelationAgent(newDebitorRel)]
hsOfficeRelationAgent(newDebitorRel),
hsOfficeBankAccountReferrer(newBankAccount)]
);
perform createRoleWithGrants(

View File

@ -41,7 +41,6 @@ begin
FROM hs_office_relation AS partnerRel
WHERE ${debitorRel}.anchorUuid = partnerRel.holderUuid
INTO newPartnerRel;
assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.partnerRelUuid = %s', NEW.partnerRelUuid);
SELECT *
FROM hs_office_relation AS r
@ -53,7 +52,6 @@ begin
FROM hs_office_relation AS r
WHERE r.type = 'DEBITOR' AND r.holderUuid = NEW.debitorRelUuid
INTO newRefundBankAccount;
assert newRefundBankAccount.uuid is not null, format('newRefundBankAccount must not be null for NEW.refundBankAccountUuid = %s', NEW.refundBankAccountUuid);
call grantRoleToRole(hsOfficeBankAccountReferrer(newRefundBankAccount), hsOfficeRelationAgent(newDebitorRel));
call grantRoleToRole(hsOfficeRelationAdmin(newDebitorRel), hsOfficeRelationAdmin(newPartnerRel));
@ -101,66 +99,12 @@ create or replace procedure updateRbacRulesForHsOfficeDebitor(
NEW hs_office_debitor
)
language plpgsql as $$
declare
oldPartnerRel hs_office_relation;
newPartnerRel hs_office_relation;
oldDebitorRel hs_office_relation;
newDebitorRel hs_office_relation;
oldRefundBankAccount hs_office_bankaccount;
newRefundBankAccount hs_office_bankaccount;
begin
call enterTriggerForObjectUuid(NEW.uuid);
SELECT *
FROM hs_office_relation AS partnerRel
WHERE ${debitorRel}.anchorUuid = partnerRel.holderUuid
INTO oldPartnerRel;
assert oldPartnerRel.uuid is not null, format('oldPartnerRel must not be null for OLD.partnerRelUuid = %s', OLD.partnerRelUuid);
SELECT *
FROM hs_office_relation AS partnerRel
WHERE ${debitorRel}.anchorUuid = partnerRel.holderUuid
INTO newPartnerRel;
assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.partnerRelUuid = %s', NEW.partnerRelUuid);
SELECT *
FROM hs_office_relation AS r
WHERE r.type = 'DEBITOR' AND r.holderUuid = OLD.debitorRelUuid
INTO oldDebitorRel;
assert oldDebitorRel.uuid is not null, format('oldDebitorRel must not be null for OLD.debitorRelUuid = %s', OLD.debitorRelUuid);
SELECT *
FROM hs_office_relation AS r
WHERE r.type = 'DEBITOR' AND r.holderUuid = NEW.debitorRelUuid
INTO newDebitorRel;
assert newDebitorRel.uuid is not null, format('newDebitorRel must not be null for NEW.debitorRelUuid = %s', NEW.debitorRelUuid);
SELECT *
FROM hs_office_relation AS r
WHERE r.type = 'DEBITOR' AND r.holderUuid = OLD.debitorRelUuid
INTO oldRefundBankAccount;
assert oldRefundBankAccount.uuid is not null, format('oldRefundBankAccount must not be null for OLD.refundBankAccountUuid = %s', OLD.refundBankAccountUuid);
SELECT *
FROM hs_office_relation AS r
WHERE r.type = 'DEBITOR' AND r.holderUuid = NEW.debitorRelUuid
INTO newRefundBankAccount;
assert newRefundBankAccount.uuid is not null, format('newRefundBankAccount must not be null for NEW.refundBankAccountUuid = %s', NEW.refundBankAccountUuid);
if NEW.refundBankAccountUuid <> OLD.refundBankAccountUuid then
call revokeRoleFromRole(hsOfficeRelationAgent(oldDebitorRel), hsOfficeBankAccountAdmin(oldRefundBankAccount));
call grantRoleToRole(hsOfficeRelationAgent(newDebitorRel), hsOfficeBankAccountAdmin(newRefundBankAccount));
call revokeRoleFromRole(hsOfficeBankAccountReferrer(oldRefundBankAccount), hsOfficeRelationAgent(oldDebitorRel));
call grantRoleToRole(hsOfficeBankAccountReferrer(newRefundBankAccount), hsOfficeRelationAgent(newDebitorRel));
if NEW.refundBankAccountUuid is distinct from OLD.refundBankAccountUuid then
delete from rbacgrants g where g.grantedbytriggerof = OLD.uuid;
call buildRbacSystemForHsOfficeDebitor(NEW);
end if;
call leaveTriggerForObjectUuid(NEW.uuid);
end; $$;
/*

View File

@ -11,6 +11,8 @@ databaseChangeLog:
file: db/changelog/005-uuid-ossp-extension.sql
- include:
file: db/changelog/006-numeric-hash-functions.sql
- include:
file: db/changelog/007-table-columns.sql
- include:
file: db/changelog/009-check-environment.sql
- include: