From 4f25bf14967f1a6027a6d0a982c49901e0388458 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 2 Apr 2024 10:09:28 +0200 Subject: [PATCH] WIP: rbac-generator-with-conditional-grants --- .../relation/HsOfficeRelationEntity.java | 10 ++- .../hsadminng/rbac/rbacdef/RbacView.java | 17 ++++- .../RbacViewMermaidFlowchartGenerator.java | 4 +- .../5033-hs-office-relation-rbac.md | 6 +- .../5033-hs-office-relation-rbac.sql | 63 +++++------------ ...ficeRelationRepositoryIntegrationTest.java | 67 ++++++++++++++++++- 6 files changed, 109 insertions(+), 58 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntity.java index 21c82852..a46ab2d9 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntity.java @@ -104,18 +104,16 @@ public class HsOfficeRelationEntity implements RbacObject, Stringifyable { .createRole(OWNER, (with) -> { with.owningUser(CREATOR); with.incomingSuperRole(GLOBAL, ADMIN); - // TODO: if type=REPRESENTATIIVE - // with.incomingSuperRole("holderPerson", ADMIN); + with.incomingSuperRole("holderPerson", ADMIN).where("${REF}.type = 'REPRESENTATIVE'"); with.permission(DELETE); }) .createSubRole(ADMIN, (with) -> { - with.incomingSuperRole("anchorPerson", ADMIN); - // TODO: if type=REPRESENTATIIVE - // with.outgoingSuperRole("anchorPerson", OWNER); + with.incomingSuperRole("anchorPerson", ADMIN).where("${REF}.type <> 'REPRESENTATIVE'"); + with.outgoingSubRole("anchorPerson", OWNER).where("${REF}.type = 'REPRESENTATIVE'"); with.permission(UPDATE); }) .createSubRole(AGENT, (with) -> { - with.incomingSuperRole("holderPerson", ADMIN); + with.incomingSuperRole("holderPerson", ADMIN).where("${REF}.type <> 'REPRESENTATIVE'"); }) .createSubRole(TENANT, (with) -> { with.incomingSuperRole("contact", ADMIN); diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java index cb048455..a09b270b 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -496,10 +496,13 @@ public class RbacView { private final RbacPermissionDefinition permDef; private boolean assumed = true; private boolean toCreate = false; + private String sqlWhere; @Override public String toString() { - final var arrow = isAssumed() ? " --> " : " -- // --> "; + final var arrow = isConditional() + ? (isAssumed() ? " -- ?? --> " : " -- ?//? --> ") + : (isAssumed() ? " --> " : " -- // --> "); return switch (grantType()) { case ROLE_TO_USER -> userDef.toString() + arrow + subRoleDef.toString(); case ROLE_TO_ROLE -> superRoleDef + arrow + subRoleDef; @@ -546,6 +549,10 @@ public class RbacView { return assumed; } + boolean isConditional() { + return sqlWhere != null; + } + boolean isToCreate() { return toCreate; } @@ -567,8 +574,14 @@ public class RbacView { .orElse(false); } - public void unassumed() { + public RbacGrantDefinition unassumed() { this.assumed = false; + return this; + } + + public RbacGrantDefinition where(final String sqlWhere) { + this.sqlWhere = sqlWhere; + return this; } public enum GrantType { diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchartGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchartGenerator.java index c6e775c9..4b6e1c14 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchartGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchartGenerator.java @@ -109,7 +109,9 @@ public class RbacViewMermaidFlowchartGenerator { private String grantDef(final RbacView.RbacGrantDefinition grant) { final var arrow = (grant.isToCreate() ? " ==>" : " -.->") - + (grant.isAssumed() ? " " : "|XX| "); + + (grant.isConditional() + ? (grant.isAssumed() ? " |??| " : "|?XX?| ") + : (grant.isAssumed() ? " " : "|XX| ")); return switch (grant.grantType()) { case ROLE_TO_USER -> // TODO: other user types not implemented yet diff --git a/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.md b/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.md index 3f45379b..be9f89b7 100644 --- a/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.md +++ b/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.md @@ -82,10 +82,12 @@ role:global:ADMIN -.-> role:contact:OWNER role:contact:OWNER -.-> role:contact:ADMIN role:contact:ADMIN -.-> role:contact:REFERRER role:global:ADMIN ==> role:relation:OWNER +role:holderPerson:ADMIN ==> |??| role:relation:OWNER role:relation:OWNER ==> role:relation:ADMIN -role:anchorPerson:ADMIN ==> role:relation:ADMIN +role:anchorPerson:ADMIN ==> |??| role:relation:ADMIN +role:relation:ADMIN ==> |??| role:anchorPerson:OWNER role:relation:ADMIN ==> role:relation:AGENT -role:holderPerson:ADMIN ==> role:relation:AGENT +role:holderPerson:ADMIN ==> |??| role:relation:AGENT role:relation:AGENT ==> role:relation:TENANT role:contact:ADMIN ==> role:relation:TENANT role:relation:TENANT ==> role:anchorPerson:REFERRER diff --git a/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.sql b/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.sql index ae783ff6..aca2eb93 100644 --- a/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.sql +++ b/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.sql @@ -57,16 +57,12 @@ begin perform createRoleWithGrants( hsOfficeRelationADMIN(NEW), permissions => array['UPDATE'], - incomingSuperRoles => array[ - hsOfficePersonADMIN(newAnchorPerson), - hsOfficeRelationOWNER(NEW)] + incomingSuperRoles => array[hsOfficeRelationOWNER(NEW)] ); perform createRoleWithGrants( hsOfficeRelationAGENT(NEW), - incomingSuperRoles => array[ - hsOfficePersonADMIN(newHolderPerson), - hsOfficeRelationADMIN(NEW)] + incomingSuperRoles => array[hsOfficeRelationADMIN(NEW)] ); perform createRoleWithGrants( @@ -81,6 +77,19 @@ begin hsOfficePersonREFERRER(newHolderPerson)] ); + if NEW.type <> 'REPRESENTATIVE' then + call grantRoleToRole(hsOfficeRelationADMIN(NEW), hsOfficePersonADMIN(newAnchorPerson)); + end if; + if NEW.type <> 'REPRESENTATIVE' then + call grantRoleToRole(hsOfficeRelationAGENT(NEW), hsOfficePersonADMIN(newHolderPerson)); + end if; + if NEW.type = 'REPRESENTATIVE' then + call grantRoleToRole(hsOfficePersonOWNER(newAnchorPerson), hsOfficeRelationADMIN(NEW)); + end if; + if NEW.type = 'REPRESENTATIVE' then + call grantRoleToRole(hsOfficeRelationOWNER(NEW), hsOfficePersonADMIN(newHolderPerson)); + end if; + call leaveTriggerForObjectUuid(NEW.uuid); end; $$; @@ -117,48 +126,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 WHERE 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 WHERE 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 WHERE 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 WHERE 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 WHERE 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 WHERE 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; $$; /* diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java index 91a7965e..2813bcf6 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java @@ -103,6 +103,69 @@ class HsOfficeRelationRepositoryIntegrationTest extends ContextBasedTestWithClea final var initialRoleNames = distinctRoleNamesOf(rawRoleRepo.findAll()); final var initialGrantNames = distinctGrantDisplaysOf(rawGrantRepo.findAll()); + // when + attempt(em, () -> { + final var givenAnchorPerson = personRepo.findPersonByOptionalNameLike("Bessler").stream() + .filter(p -> p.getPersonType() == UNINCORPORATED_FIRM) + .findFirst().orElseThrow(); + final var givenHolderPerson = personRepo.findPersonByOptionalNameLike("Bert").stream() + .filter(p -> p.getPersonType() == NATURAL_PERSON) + .findFirst().orElseThrow(); + final var givenContact = contactRepo.findContactByOptionalLabelLike("fourth contact").stream() + .findFirst().orElseThrow(); + final var newRelation = HsOfficeRelationEntity.builder() + .anchor(givenAnchorPerson) + .holder(givenHolderPerson) + .type(HsOfficeRelationType.SUBSCRIBER) + .mark("dummy") + .contact(givenContact) + .build(); + return toCleanup(relationRepo.save(newRelation)); + }); + + // then + assertThat(distinctRoleNamesOf(rawRoleRepo.findAll())).containsExactlyInAnyOrder(Array.from( + initialRoleNames, + "hs_office_relation#ErbenBesslerMelBessler-with-SUBSCRIBER-BesslerBert:OWNER", + "hs_office_relation#ErbenBesslerMelBessler-with-SUBSCRIBER-BesslerBert:ADMIN", + "hs_office_relation#ErbenBesslerMelBessler-with-SUBSCRIBER-BesslerBert:AGENT", + "hs_office_relation#ErbenBesslerMelBessler-with-SUBSCRIBER-BesslerBert:TENANT")); + assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(Array.fromFormatted( + initialGrantNames, + // TODO: this grant should only be created for DEBITOR-Relationships, thus the RBAC DSL needs to support conditional grants + "{ grant perm:hs_office_relation#ErbenBesslerMelBessler-with-SUBSCRIBER-BesslerBert:INSERT>hs_office_sepamandate to role:hs_office_relation#ErbenBesslerMelBessler-with-SUBSCRIBER-BesslerBert:ADMIN by system and assume }", + + "{ grant perm:hs_office_relation#ErbenBesslerMelBessler-with-SUBSCRIBER-BesslerBert:DELETE to role:hs_office_relation#ErbenBesslerMelBessler-with-SUBSCRIBER-BesslerBert:OWNER by system and assume }", + "{ grant role:hs_office_relation#ErbenBesslerMelBessler-with-SUBSCRIBER-BesslerBert:OWNER to role:global#global:ADMIN by system and assume }", + "{ grant role:hs_office_relation#ErbenBesslerMelBessler-with-SUBSCRIBER-BesslerBert:OWNER to user:superuser-alex@hostsharing.net by hs_office_relation#ErbenBesslerMelBessler-with-SUBSCRIBER-BesslerBert:OWNER and assume }", + + "{ grant perm:hs_office_relation#ErbenBesslerMelBessler-with-SUBSCRIBER-BesslerBert:UPDATE to role:hs_office_relation#ErbenBesslerMelBessler-with-SUBSCRIBER-BesslerBert:ADMIN by system and assume }", + "{ grant role:hs_office_relation#ErbenBesslerMelBessler-with-SUBSCRIBER-BesslerBert:ADMIN to role:hs_office_relation#ErbenBesslerMelBessler-with-SUBSCRIBER-BesslerBert:OWNER by system and assume }", + "{ grant role:hs_office_relation#ErbenBesslerMelBessler-with-SUBSCRIBER-BesslerBert:ADMIN to role:hs_office_person#ErbenBesslerMelBessler:ADMIN by system and assume }", + + "{ grant role:hs_office_relation#ErbenBesslerMelBessler-with-SUBSCRIBER-BesslerBert:AGENT to role:hs_office_person#BesslerBert:ADMIN by system and assume }", + "{ grant role:hs_office_relation#ErbenBesslerMelBessler-with-SUBSCRIBER-BesslerBert:AGENT to role:hs_office_relation#ErbenBesslerMelBessler-with-SUBSCRIBER-BesslerBert:ADMIN by system and assume }", + + "{ grant perm:hs_office_relation#ErbenBesslerMelBessler-with-SUBSCRIBER-BesslerBert:SELECT to role:hs_office_relation#ErbenBesslerMelBessler-with-SUBSCRIBER-BesslerBert:TENANT by system and assume }", + "{ grant role:hs_office_relation#ErbenBesslerMelBessler-with-SUBSCRIBER-BesslerBert:TENANT to role:hs_office_relation#ErbenBesslerMelBessler-with-SUBSCRIBER-BesslerBert:AGENT by system and assume }", + "{ grant role:hs_office_person#BesslerBert:REFERRER to role:hs_office_relation#ErbenBesslerMelBessler-with-SUBSCRIBER-BesslerBert:TENANT by system and assume }", + "{ grant role:hs_office_person#ErbenBesslerMelBessler:REFERRER to role:hs_office_relation#ErbenBesslerMelBessler-with-SUBSCRIBER-BesslerBert:TENANT by system and assume }", + "{ grant role:hs_office_contact#fourthcontact:REFERRER to role:hs_office_relation#ErbenBesslerMelBessler-with-SUBSCRIBER-BesslerBert:TENANT by system and assume }", + + // SUBSCRIBER holder person -> (represented) anchor person + "{ grant role:hs_office_relation#ErbenBesslerMelBessler-with-SUBSCRIBER-BesslerBert:TENANT to role:hs_office_contact#fourthcontact:ADMIN by system and assume }", + + null) + ); + } + + @Test + public void createsAndGrantsRolesForTypeRepresentative() { + // given + context("superuser-alex@hostsharing.net"); + final var initialRoleNames = distinctRoleNamesOf(rawRoleRepo.findAll()); + final var initialGrantNames = distinctGrantDisplaysOf(rawGrantRepo.findAll()); + // when attempt(em, () -> { final var givenAnchorPerson = personRepo.findPersonByOptionalNameLike("Bessler").stream() @@ -140,9 +203,9 @@ class HsOfficeRelationRepositoryIntegrationTest extends ContextBasedTestWithClea "{ grant perm:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:UPDATE to role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:ADMIN by system and assume }", "{ grant role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:ADMIN to role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:OWNER by system and assume }", - "{ grant role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:ADMIN to role:hs_office_person#ErbenBesslerMelBessler:ADMIN by system and assume }", + "{ grant role:hs_office_person#ErbenBesslerMelBessler:OWNER to role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:ADMIN by system and assume }", - "{ grant role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:AGENT to role:hs_office_person#BesslerBert:ADMIN by system and assume }", + "{ grant role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:OWNER to role:hs_office_person#BesslerBert:ADMIN by system and assume }", "{ grant role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:AGENT to role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:ADMIN by system and assume }", "{ grant perm:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:SELECT to role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:TENANT by system and assume }",