From 266cd16b52d60e4d193afc571f3bdf0624ee2efb Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 15 Mar 2024 06:18:02 +0100 Subject: [PATCH] new case for insert permission trigger generator: indirect role check (via relation) --- .../HsOfficeSepaMandateEntity.java | 23 +++- .../rbac/rbacdef/InsertTriggerGenerator.java | 130 +++++++++++++----- .../hsadminng/rbac/rbacdef/StringWriter.java | 14 ++ .../rbacgrant/RbacGrantsDiagramService.java | 23 ++-- .../253-hs-office-sepamandate-rbac.md | 4 +- .../253-hs-office-sepamandate-rbac.sql | 98 ++++++++++--- 6 files changed, 225 insertions(+), 67 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntity.java index a048fe4d..02c96ad1 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntity.java @@ -103,8 +103,19 @@ public class HsOfficeSepaMandateEntity implements Stringifyable, HasUuid { .withRestrictedViewOrderBy(expression("validity")) .withUpdatableColumns("reference", "agreement", "validity") - .importEntityAlias("debitorRel", HsOfficeRelationshipEntity.class, dependsOnColumn("debitorRelUuid")) - .importEntityAlias("bankAccount", HsOfficeBankAccountEntity.class, dependsOnColumn("bankAccountUuid")) + .importEntityAlias("debitorRel", HsOfficeRelationshipEntity.class, + dependsOnColumn("debitorUuid"), + fetchedBySql(""" + SELECT debitorRel.* + FROM hs_office_relationship debitorRel + JOIN hs_office_debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid + WHERE debitor.uuid = ${REF}.debitorUuid + """) + ) + .importEntityAlias("bankAccount", HsOfficeBankAccountEntity.class, + dependsOnColumn("bankAccountUuid"), + autoFetched() + ) .createRole(OWNER, (with) -> { with.owningUser(CREATOR); @@ -116,14 +127,16 @@ public class HsOfficeSepaMandateEntity implements Stringifyable, HasUuid { }) .createSubRole(AGENT, (with) -> { with.outgoingSubRole("bankAccount", REFERRER); - with.outgoingSubRole("debitor", AGENT); + with.outgoingSubRole("debitorRel", AGENT); }) .createSubRole(REFERRER, (with) -> { with.incomingSuperRole("bankAccount", ADMIN); - with.incomingSuperRole("debitor", AGENT); + with.incomingSuperRole("debitorRel", AGENT); with.outgoingSubRole("debitorRel", TENANT); with.permission(SELECT); - }); + }) + + .toRole("debitorRel", ADMIN).grantPermission("sepaMandate", INSERT); } public static void main(String[] args) throws IOException { diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java index 8bdd18ae..a29deab1 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java @@ -4,6 +4,7 @@ import java.util.Optional; import java.util.function.BinaryOperator; import java.util.stream.Stream; +import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.NEW; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.PERM_TO_ROLE; import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with; @@ -91,49 +92,23 @@ public class InsertTriggerGenerator { """, with("rawSubTableName", rbacDef.getRootEntityAlias().getRawTableName()), with("rawSuperTableName", superRoleDef.getEntityAlias().getRawTableName()), - with("rawSuperRoleDescriptor", toRoleDescriptor(superRoleDef, PostgresTriggerReference.NEW.name())) + with("rawSuperRoleDescriptor", toRoleDescriptor(superRoleDef, NEW.name())) ); }); } private void generateInsertCheckTrigger(final StringWriter plPgSql) { - plPgSql.writeLn(""" - /** - Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable}. - */ - create or replace function ${rawSubTable}_insert_permission_missing_tf() - returns trigger - language plpgsql as $$ - begin - raise exception '[403] insert into ${rawSubTable} not allowed for current subjects % (%)', - currentSubjects(), currentSubjectsUuids(); - end; $$; - """, - with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName())); getOptionalInsertGrant().ifPresentOrElse(g -> { if (!g.getSuperRoleDef().getEntityAlias().isGlobal()) { - plPgSql.writeLn( - """ - create trigger ${rawSubTable}_insert_permission_check_tg - before insert on ${rawSubTable} - for each row - when ( not hasInsertPermission(NEW.${referenceColumn}, 'INSERT', '${rawSubTable}') ) - execute procedure ${rawSubTable}_insert_permission_missing_tf(); - """, - with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()), - with("referenceColumn", g.getSuperRoleDef().getEntityAlias().dependsOnColumName())); + if (rbacDef.isRootEntityAlias(g.getSuperRoleDef().getEntityAlias())) { + generateInsertPermissionTriggerAllowByDirectRole(plPgSql, g); + } else { + generateInsertPermissionTriggerAllowByIndirectRole(plPgSql, g); + } } else { switch (g.getSuperRoleDef().getRole()) { case ADMIN -> { - plPgSql.writeLn( - """ - create trigger ${rawSubTable}_insert_permission_check_tg - before insert on ${rawSubTable} - for each row - when ( not isGlobalAdmin() ) - execute procedure ${rawSubTable}_insert_permission_missing_tf(); - """, - with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName())); + generateInsertPermissionTriggerAllowOnlyGlobalAdmin(plPgSql); } case GUEST -> { // no permission check trigger generated, as anybody can insert rows into this table @@ -146,7 +121,8 @@ public class InsertTriggerGenerator { } }, () -> { - plPgSql.writeLn(""" + plPgSql.writeLn(""" + -- FIXME: Where is this case necessary? create trigger ${rawSubTable}_insert_permission_check_tg before insert on ${rawSubTable} for each row @@ -159,6 +135,92 @@ public class InsertTriggerGenerator { }); } + private void generateInsertPermissionTriggerAllowByDirectRole(final StringWriter plPgSql, final RbacView.RbacGrantDefinition g) { + plPgSql.writeLn(""" + /** + Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable}. + */ + create or replace function ${rawSubTable}_insert_permission_missing_tf() + returns trigger + language plpgsql as $$ + begin + raise exception '[403] insert into ${rawSubTable} not allowed for current subjects % (%)', + currentSubjects(), currentSubjectsUuids(); + end; $$; + + create trigger ${rawSubTable}_insert_permission_check_tg + before insert on ${rawSubTable} + for each row + when ( not hasInsertPermission(NEW.${referenceColumn}, 'INSERT', '${rawSubTable}') ) + execute procedure ${rawSubTable}_insert_permission_missing_tf(); + """, + with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()), + with("referenceColumn", g.getSuperRoleDef().getEntityAlias().dependsOnColumName())); + } + + private void generateInsertPermissionTriggerAllowByIndirectRole( + final StringWriter plPgSql, + final RbacView.RbacGrantDefinition g) { + plPgSql.writeLn(""" + /** + Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable}. + */ + create or replace function ${rawSubTable}_insert_permission_missing_tf() + returns trigger + language plpgsql as $$ + begin + if ( not hasInsertPermission( + ( SELECT ${varName}.uuid FROM + """, + with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()), + with("varName", g.getSuperRoleDef().getEntityAlias().aliasName())); + plPgSql.indented(3, () -> { + plPgSql.writeLn( + "(" + g.getSuperRoleDef().getEntityAlias().fetchSql().sql + ") AS ${varName}", + with("varName", g.getSuperRoleDef().getEntityAlias().aliasName()), + with("ref", NEW.name())); + }); + plPgSql.writeLn(""" + + ), 'INSERT', '${rawSubTable}') ) then + raise exception + '[403] insert into ${rawSubTable} not allowed for current subjects % (%)', + currentSubjects(), currentSubjectsUuids(); + end if; + return NEW; + end; $$; + + create trigger ${rawSubTable}_insert_permission_check_tg + before insert on ${rawSubTable} + for each row + execute procedure ${rawSubTable}_insert_permission_missing_tf(); + + """, + with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName())); + } + + private void generateInsertPermissionTriggerAllowOnlyGlobalAdmin(final StringWriter plPgSql) { + plPgSql.writeLn(""" + /** + Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable}. + */ + create or replace function ${rawSubTable}_insert_permission_missing_tf() + returns trigger + language plpgsql as $$ + begin + raise exception '[403] insert into ${rawSubTable} not allowed for current subjects % (%)', + currentSubjects(), currentSubjectsUuids(); + end; $$; + + create trigger ${rawSubTable}_insert_permission_check_tg + before insert on ${rawSubTable} + for each row + when ( not isGlobalAdmin() ) + execute procedure ${rawSubTable}_insert_permission_missing_tf(); + """, + with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName())); + } + private Stream getInsertGrants() { return rbacDef.getGrantDefs().stream() .filter(g -> g.grantType() == PERM_TO_ROLE) diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/StringWriter.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/StringWriter.java index 3e065c92..5a5e0699 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/StringWriter.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/StringWriter.java @@ -38,12 +38,26 @@ public class StringWriter { --indentLevel; } + void indent(int levels) { + indentLevel += levels; + } + + void unindent(int levels) { + indentLevel -= levels; + } + void indented(final Runnable indented) { indent(); indented.run(); unindent(); } + void indented(int levels, final Runnable indented) { + indent(levels); + indented.run(); + unindent(levels); + } + boolean chopTail(final String tail) { if (string.toString().endsWith(tail)) { string.setLength(string.length() - tail.length()); diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java index 0296cd61..900c8d12 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java @@ -42,7 +42,9 @@ public class RbacGrantsDiagramService { PERMISSIONS, NOT_ASSUMED, TEST_ENTITIES, - NON_TEST_ENTITIES + NON_TEST_ENTITIES; + + public static final EnumSet ALL = EnumSet.allOf(Include.class); } @Autowired @@ -65,6 +67,10 @@ public class RbacGrantsDiagramService { private void traverseGrantsTo(final Set graph, final UUID refUuid, final EnumSet includes) { final var grants = rawGrantRepo.findByAscendingUuid(refUuid); grants.forEach(g -> { + if ( g.getDescendantIdName() == null ) { + // FIXME: what's that? + return; + } if (!includes.contains(PERMISSIONS) && g.getDescendantIdName().startsWith("perm ")) { return; } @@ -116,7 +122,7 @@ public class RbacGrantsDiagramService { ) .collect(groupingBy(RbacGrantsDiagramService::renderEntityIdName)) .entrySet().stream() - .map(entity -> "subgraph " + quoted(entity.getKey()) + renderSubgraph(entity.getKey()) + "\n\n " + .map(entity -> "subgraph " + cleanId(entity.getKey()) + renderSubgraph(entity.getKey()) + "\n\n " + entity.getValue().stream() .map(n -> renderNode(n.idName(), n.uuid()).replace("\n", "\n ")) .sorted() @@ -127,9 +133,9 @@ public class RbacGrantsDiagramService { : ""; final var grants = graph.stream() - .map(g -> quoted(g.getAscendantIdName()) + .map(g -> cleanId(g.getAscendantIdName()) + " -->" + (g.isAssumed() ? " " : "|XX| ") - + quoted(g.getDescendantIdName())) + + cleanId(g.getDescendantIdName())) .sorted() .collect(joining("\n")); @@ -151,7 +157,7 @@ public class RbacGrantsDiagramService { // } // return "[" + table + "\n" + entity + "]"; // } - return "[" + entityId + "]"; + return "[" + cleanId(entityId) + "]"; } private static String renderEntityIdName(final Node node) { @@ -170,7 +176,7 @@ public class RbacGrantsDiagramService { } private String renderNode(final String idName, final UUID uuid) { - return quoted(idName) + renderNodeContent(idName, uuid); + return cleanId(idName) + renderNodeContent(idName, uuid); } private String renderNodeContent(final String idName, final UUID uuid) { @@ -196,8 +202,9 @@ public class RbacGrantsDiagramService { } @NotNull - private static String quoted(final String idName) { - return idName.replace(" ", ":").replaceAll("@.*", ""); + private static String cleanId(final String idName) { + return idName.replace(" ", ":").replaceAll("@.*", "") + .replace("[", "").replace("]", "").replace("(", "").replace(")", "").replace(",", ""); } } diff --git a/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.md b/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.md index 5381bcd6..3d904ce9 100644 --- a/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.md +++ b/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.md @@ -1,6 +1,6 @@ ### rbac sepaMandate -This code generated was by RbacViewMermaidFlowchartGenerator at 2024-03-11T18:29:47.084556363. +This code generated was by RbacViewMermaidFlowchartGenerator at 2024-03-15T06:12:35.337470470. ```mermaid %%{init:{'flowchart':{'htmlLabels':false}}}%% @@ -77,6 +77,7 @@ subgraph sepaMandate["`**sepaMandate**`"] perm:sepaMandate:DELETE{{sepaMandate:DELETE}} perm:sepaMandate:UPDATE{{sepaMandate:UPDATE}} perm:sepaMandate:SELECT{{sepaMandate:SELECT}} + perm:sepaMandate:INSERT{{sepaMandate:INSERT}} end end @@ -174,5 +175,6 @@ role:sepaMandate:referrer ==> role:debitorRel:tenant role:sepaMandate:owner ==> perm:sepaMandate:DELETE role:sepaMandate:admin ==> perm:sepaMandate:UPDATE role:sepaMandate:referrer ==> perm:sepaMandate:SELECT +role:debitorRel:admin ==> perm:sepaMandate:INSERT ``` diff --git a/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.sql b/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.sql index aedfb28d..b5f98ca3 100644 --- a/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.sql +++ b/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.sql @@ -1,5 +1,6 @@ --liquibase formatted sql --- This code generated was by RbacViewPostgresGenerator at 2024-03-11T18:29:47.095199204. +-- This code generated was by RbacViewPostgresGenerator at 2024-03-15T06:12:35.345630060. + -- ============================================================================ --changeset hs-office-sepamandate-rbac-OBJECT:1 endDelimiter:--// @@ -34,14 +35,23 @@ declare begin call enterTriggerForObjectUuid(NEW.uuid); - SELECT * FROM hs_office_bankaccount WHERE uuid = NEW.bankAccountUuid into newBankAccount; - SELECT * FROM hs_office_relationship WHERE uuid = NEW.debitorUuid into newDebitorRel; + + SELECT * FROM hs_office_bankaccount WHERE uuid = NEW.bankAccountUuid INTO newBankAccount; + assert newBankAccount.uuid is not null, format('newBankAccount must not be null for NEW.bankAccountUuid = %s', NEW.bankAccountUuid); + + SELECT debitorRel.* + FROM hs_office_relationship debitorRel + JOIN hs_office_debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid + WHERE debitor.uuid = NEW.debitorUuid + INTO newDebitorRel; + assert newDebitorRel.uuid is not null, format('newDebitorRel must not be null for NEW.debitorUuid = %s', NEW.debitorUuid); + perform createRoleWithGrants( hsOfficeSepaMandateOwner(NEW), permissions => array['DELETE'], - userUuids => array[currentUserUuid()], - incomingSuperRoles => array[globalAdmin()] + incomingSuperRoles => array[globalAdmin()], + userUuids => array[currentUserUuid()] ); perform createRoleWithGrants( @@ -54,16 +64,16 @@ begin hsOfficeSepaMandateAgent(NEW), incomingSuperRoles => array[hsOfficeSepaMandateAdmin(NEW)], outgoingSubRoles => array[ - hsOfficeBankAccountReferrer(newBankAccount), - hsOfficeRelationshipAgent(newDebitorRel)] + hsOfficeRelationshipAgent(newDebitorRel), + hsOfficeBankAccountReferrer(newBankAccount)] ); perform createRoleWithGrants( hsOfficeSepaMandateReferrer(NEW), permissions => array['SELECT'], incomingSuperRoles => array[ - hsOfficeRelationshipAgent(newDebitorRel), hsOfficeBankAccountAdmin(newBankAccount), + hsOfficeRelationshipAgent(newDebitorRel), hsOfficeSepaMandateAgent(NEW)], outgoingSubRoles => array[hsOfficeRelationshipTenant(newDebitorRel)] ); @@ -88,13 +98,52 @@ create trigger insertTriggerForHsOfficeSepaMandate_tg after insert on hs_office_sepamandate for each row execute procedure insertTriggerForHsOfficeSepaMandate_tf(); - --// + -- ============================================================================ --changeset hs-office-sepamandate-rbac-INSERT:1 endDelimiter:--// -- ---------------------------------------------------------------------------- +/* + Creates INSERT INTO hs_office_sepamandate permissions for the related hs_office_relationship rows. + */ +do language plpgsql $$ + declare + row hs_office_relationship; + permissionUuid uuid; + roleUuid uuid; + begin + call defineContext('create INSERT INTO hs_office_sepamandate permissions for the related hs_office_relationship rows'); + + FOR row IN SELECT * FROM hs_office_relationship + LOOP + roleUuid := findRoleId(hsOfficeRelationshipAdmin(row)); + permissionUuid := createPermission(row.uuid, 'INSERT', 'hs_office_sepamandate'); + call grantPermissionToRole(permissionUuid, roleUuid); + END LOOP; + END; +$$; + +/** + Adds hs_office_sepamandate INSERT permission to specified role of new hs_office_relationship rows. +*/ +create or replace function hs_office_sepamandate_hs_office_relationship_insert_tf() + returns trigger + language plpgsql + strict as $$ +begin + call grantPermissionToRole( + hsOfficeRelationshipAdmin(NEW), + createPermission(NEW.uuid, 'INSERT', 'hs_office_sepamandate')); + return NEW; +end; $$; + +create trigger hs_office_sepamandate_hs_office_relationship_insert_tg + after insert on hs_office_relationship + for each row +execute procedure hs_office_sepamandate_hs_office_relationship_insert_tf(); + /** Checks if the user or assumed roles are allowed to insert a row to hs_office_sepamandate. */ @@ -102,19 +151,29 @@ create or replace function hs_office_sepamandate_insert_permission_missing_tf() returns trigger language plpgsql as $$ begin - raise exception '[403] insert into hs_office_sepamandate not allowed for current subjects % (%)', - currentSubjects(), currentSubjectsUuids(); + if ( not hasInsertPermission( + ( SELECT debitorRel.uuid FROM + + (SELECT debitorRel.* + FROM hs_office_relationship debitorRel + JOIN hs_office_debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid + WHERE debitor.uuid = NEW.debitorUuid + ) AS debitorRel + + ), 'INSERT', 'hs_office_sepamandate') ) then + raise exception + '[403] insert into hs_office_sepamandate not allowed for current subjects % (%)', + currentSubjects(), currentSubjectsUuids(); + end if; + return NEW; end; $$; create trigger hs_office_sepamandate_insert_permission_check_tg before insert on hs_office_sepamandate for each row - -- As there is no explicit INSERT grant specified for this table, - -- only global admins are allowed to insert any rows. - when ( not isGlobalAdmin() ) execute procedure hs_office_sepamandate_insert_permission_missing_tf(); - --// + -- ============================================================================ --changeset hs-office-sepamandate-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- @@ -125,18 +184,19 @@ create trigger hs_office_sepamandate_insert_permission_check_tg join hs_office_bankaccount ba on ba.uuid = sm.bankAccountUuid $idName$); - --// + -- ============================================================================ --changeset hs-office-sepamandate-rbac-RESTRICTED-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- call generateRbacRestrictedView('hs_office_sepamandate', - 'validity', + $orderBy$ + validity + $orderBy$, $updates$ - reference = new.reference, + reference = new.reference, agreement = new.agreement, validity = new.validity $updates$); --// -