From 9806bcd78fa23e7d1b3d0e536983dd965329fafd Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 23 Apr 2024 10:42:24 +0200 Subject: [PATCH] conditional insert permission grant (so far just exactly 1 unique for each table) (#48) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/48 Reviewed-by: Timotheus Pokorra --- build.gradle | 2 +- .../hs/booking/item/HsBookingItemEntity.java | 7 +- .../HsOfficeCoopAssetsTransactionEntity.java | 3 +- .../HsOfficeCoopSharesTransactionEntity.java | 3 +- .../office/debitor/HsOfficeDebitorEntity.java | 10 +-- .../membership/HsOfficeMembershipEntity.java | 3 +- .../relation/HsOfficeRelationEntity.java | 8 ++- .../HsOfficeSepaMandateEntity.java | 7 +- .../rbac/rbacdef/InsertTriggerGenerator.java | 25 ++++++-- .../hsadminng/rbac/rbacdef/RbacView.java | 64 ++++++++++++++----- .../RbacViewMermaidFlowchartGenerator.java | 5 ++ .../rbac/test/dom/TestDomainEntity.java | 3 +- .../rbac/test/pac/TestPackageEntity.java | 3 +- .../5063-hs-office-debitor-rbac.md | 10 --- .../5073-hs-office-sepamandate-rbac.md | 10 --- .../5073-hs-office-sepamandate-rbac.sql | 5 +- .../6013-hs-booking-item-rbac.md | 20 ------ .../6013-hs-booking-item-rbac.sql | 8 +-- ...fficePartnerRepositoryIntegrationTest.java | 2 - ...ficeRelationRepositoryIntegrationTest.java | 2 - 20 files changed, 111 insertions(+), 89 deletions(-) diff --git a/build.gradle b/build.gradle index 45b75734..bd48e3ac 100644 --- a/build.gradle +++ b/build.gradle @@ -174,7 +174,7 @@ project.tasks.processResources.dependsOn processSpring project.tasks.compileJava.dependsOn processSpring // Rename javax to jakarta in OpenApi generated java files because -// io.openapiprocessor.openapi-processor 2022.2 does not yet support the openapiprocessor useSpringBoot3 config option. +// io.openapiprocessor.openapi-processor 2022.5 does not yet support the openapiprocessor useSpringBoot3 config option. // TODO.impl: Upgrade to io.openapiprocessor.openapi-processor >= 2024.2 // and use either `bean-validation: true` in api-mapping.yaml or `useSpringBoot3 true` (not sure where exactly). task openApiGenerate(type: Copy) { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java index aad7d836..3d948ef2 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java @@ -35,10 +35,13 @@ import java.util.Map; import java.util.UUID; import static java.util.Optional.ofNullable; +import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.lowerInclusiveFromPostgresDateRange; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.upperInclusiveFromPostgresDateRange; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingCase; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.DELETE; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT; @@ -148,12 +151,12 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject { .withRestrictedViewOrderBy(SQL.expression("validity")) .withUpdatableColumns("version", "caption", "validity", "resources") - .importEntityAlias("debitor", HsOfficeDebitorEntity.class, + .importEntityAlias("debitor", HsOfficeDebitorEntity.class, usingDefaultCase(), dependsOnColumn("debitorUuid"), directlyFetchedByDependsOnColumn(), NOT_NULL) - .importEntityAlias("debitorRel", HsOfficeRelationEntity.class, + .importEntityAlias("debitorRel", HsOfficeRelationEntity.class, usingCase(DEBITOR), dependsOnColumn("debitorUuid"), fetchedBySql(""" SELECT ${columns} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntity.java index 4ec6685d..2cf4f089 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntity.java @@ -22,6 +22,7 @@ import java.util.UUID; import static java.util.Optional.ofNullable; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.SELECT; @@ -125,7 +126,7 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, RbacO return rbacViewFor("coopAssetsTransaction", HsOfficeCoopAssetsTransactionEntity.class) .withIdentityView(RbacView.SQL.projection("reference")) .withUpdatableColumns("comment") - .importEntityAlias("membership", HsOfficeMembershipEntity.class, + .importEntityAlias("membership", HsOfficeMembershipEntity.class, usingDefaultCase(), dependsOnColumn("membershipUuid"), directlyFetchedByDependsOnColumn(), NOT_NULL) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntity.java index 8604ec16..c886170e 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntity.java @@ -20,6 +20,7 @@ import java.util.UUID; import static java.util.Optional.ofNullable; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.SELECT; @@ -119,7 +120,7 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, RbacO return rbacViewFor("coopSharesTransaction", HsOfficeCoopSharesTransactionEntity.class) .withIdentityView(SQL.projection("reference")) .withUpdatableColumns("comment") - .importEntityAlias("membership", HsOfficeMembershipEntity.class, + .importEntityAlias("membership", HsOfficeMembershipEntity.class, usingDefaultCase(), dependsOnColumn("membershipUuid"), directlyFetchedByDependsOnColumn(), NOT_NULL) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntity.java index 51df906f..33e6f2e8 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntity.java @@ -36,7 +36,9 @@ import static jakarta.persistence.CascadeType.MERGE; import static jakarta.persistence.CascadeType.PERSIST; import static jakarta.persistence.CascadeType.REFRESH; import static java.util.Optional.ofNullable; +import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingCase; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NULLABLE; @@ -171,23 +173,21 @@ public class HsOfficeDebitorEntity implements RbacObject, Stringifyable { "defaultPrefix" /* TODO.spec: do we want that updatable? */) .toRole("global", ADMIN).grantPermission(INSERT) - .importRootEntityAliasProxy("debitorRel", HsOfficeRelationEntity.class, - // TODO.spec: do we need a distinct case for DEBITOR-Relation? - usingDefaultCase(), + .importRootEntityAliasProxy("debitorRel", HsOfficeRelationEntity.class, usingCase(DEBITOR), directlyFetchedByDependsOnColumn(), dependsOnColumn("debitorRelUuid")) .createPermission(DELETE).grantedTo("debitorRel", OWNER) .createPermission(UPDATE).grantedTo("debitorRel", ADMIN) .createPermission(SELECT).grantedTo("debitorRel", TENANT) - .importEntityAlias("refundBankAccount", HsOfficeBankAccountEntity.class, + .importEntityAlias("refundBankAccount", HsOfficeBankAccountEntity.class, usingDefaultCase(), dependsOnColumn("refundBankAccountUuid"), directlyFetchedByDependsOnColumn(), NULLABLE) .toRole("refundBankAccount", ADMIN).grantRole("debitorRel", AGENT) .toRole("debitorRel", AGENT).grantRole("refundBankAccount", REFERRER) - .importEntityAlias("partnerRel", HsOfficeRelationEntity.class, + .importEntityAlias("partnerRel", HsOfficeRelationEntity.class, usingDefaultCase(), dependsOnColumn("debitorRelUuid"), fetchedBySql(""" SELECT ${columns} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntity.java index d031389d..67050ccc 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntity.java @@ -38,6 +38,7 @@ import static net.hostsharing.hsadminng.mapper.PostgresDateRange.lowerInclusiveF import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.upperInclusiveFromPostgresDateRange; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.DELETE; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT; @@ -156,7 +157,7 @@ public class HsOfficeMembershipEntity implements RbacObject, Stringifyable { .withRestrictedViewOrderBy(SQL.projection("validity")) .withUpdatableColumns("validity", "membershipFeeBillable", "status") - .importEntityAlias("partnerRel", HsOfficeRelationEntity.class, + .importEntityAlias("partnerRel", HsOfficeRelationEntity.class, usingDefaultCase(), dependsOnColumn("partnerUuid"), fetchedBySql(""" SELECT ${columns} 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 2bc9c452..e8e90702 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 @@ -19,6 +19,8 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.*; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.CaseDef.inCaseOf; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.CaseDef.inOtherCases; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingCase; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR; @@ -94,15 +96,15 @@ public class HsOfficeRelationEntity implements RbacObject, Stringifyable { .withRestrictedViewOrderBy(SQL.expression( "(select idName from hs_office_person_iv p where p.uuid = target.holderUuid)")) .withUpdatableColumns("contactUuid") - .importEntityAlias("anchorPerson", HsOfficePersonEntity.class, + .importEntityAlias("anchorPerson", HsOfficePersonEntity.class, usingDefaultCase(), dependsOnColumn("anchorUuid"), directlyFetchedByDependsOnColumn(), NOT_NULL) - .importEntityAlias("holderPerson", HsOfficePersonEntity.class, + .importEntityAlias("holderPerson", HsOfficePersonEntity.class, usingDefaultCase(), dependsOnColumn("holderUuid"), directlyFetchedByDependsOnColumn(), NOT_NULL) - .importEntityAlias("contact", HsOfficeContactEntity.class, + .importEntityAlias("contact", HsOfficeContactEntity.class, usingDefaultCase(), dependsOnColumn("contactUuid"), directlyFetchedByDependsOnColumn(), NOT_NULL) 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 a4344abe..ad3bf25a 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 @@ -18,8 +18,11 @@ import java.io.IOException; import java.time.LocalDate; import java.util.UUID; +import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.*; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingCase; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*; @@ -107,7 +110,7 @@ public class HsOfficeSepaMandateEntity implements Stringifyable, RbacObject { .withRestrictedViewOrderBy(expression("validity")) .withUpdatableColumns("reference", "agreement", "validity") - .importEntityAlias("debitorRel", HsOfficeRelationEntity.class, + .importEntityAlias("debitorRel", HsOfficeRelationEntity.class, usingCase(DEBITOR), dependsOnColumn("debitorUuid"), fetchedBySql(""" SELECT ${columns} @@ -116,7 +119,7 @@ public class HsOfficeSepaMandateEntity implements Stringifyable, RbacObject { WHERE debitor.uuid = ${REF}.debitorUuid """), NOT_NULL) - .importEntityAlias("bankAccount", HsOfficeBankAccountEntity.class, + .importEntityAlias("bankAccount", HsOfficeBankAccountEntity.class, usingDefaultCase(), dependsOnColumn("bankAccountUuid"), directlyFetchedByDependsOnColumn(), NOT_NULL) 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 7ef34252..66ef1481 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java @@ -50,7 +50,7 @@ public class InsertTriggerGenerator { begin call defineContext('create INSERT INTO ${rawSubTableName} permissions for the related ${rawSuperTableName} rows'); - FOR row IN SELECT * FROM ${rawSuperTableName} + FOR row IN SELECT * FROM ${rawSuperTableName}${typeCondition} LOOP call grantPermissionToRole( createPermission(row.uuid, 'INSERT', '${rawSubTableName}'), @@ -61,7 +61,10 @@ public class InsertTriggerGenerator { """, with("rawSubTableName", rbacDef.getRootEntityAlias().getRawTableName()), with("rawSuperTableName", superRoleDef.getEntityAlias().getRawTableName()), - with("rawSuperRoleDescriptor", toRoleDescriptor(superRoleDef, "row")) + with("rawSuperRoleDescriptor", toRoleDescriptor(superRoleDef, "row")), + with("typeCondition", superRoleDef.getEntityAlias().isCaseDependent() + ? "\n\t\t\tWHERE type = '${case}'".replace("${case}", superRoleDef.getEntityAlias().usingCase().value) + : "") ); }); } @@ -77,9 +80,9 @@ public class InsertTriggerGenerator { language plpgsql strict as $$ begin - call grantPermissionToRole( + ${typeConditionIf}call grantPermissionToRole( createPermission(NEW.uuid, 'INSERT', '${rawSubTableName}'), - ${rawSuperRoleDescriptor}); + ${rawSuperRoleDescriptor});${typeConditionEndIf} return NEW; end; $$; @@ -91,7 +94,14 @@ public class InsertTriggerGenerator { """, with("rawSubTableName", rbacDef.getRootEntityAlias().getRawTableName()), with("rawSuperTableName", superRoleDef.getEntityAlias().getRawTableName()), - with("rawSuperRoleDescriptor", toRoleDescriptor(superRoleDef, NEW.name())) + with("rawSuperRoleDescriptor", toRoleDescriptor(superRoleDef, NEW.name())), + with("typeConditionIf", + superRoleDef.getEntityAlias().isCaseDependent() + ? "if NEW.type = '${case}' then\n\t\t".replace("${case}", superRoleDef.getEntityAlias().usingCase().value) + : ""), + with("typeConditionEndIf", superRoleDef.getEntityAlias().isCaseDependent() + ? "\n\tend if;" + : "") ); }); } @@ -241,7 +251,10 @@ public class InsertTriggerGenerator { private static BinaryOperator singleton() { return (x, y) -> { - throw new IllegalStateException("only a single INSERT permission grant allowed"); + if ( !x.equals(y) ) { + throw new IllegalStateException("only a single INSERT permission grant allowed"); + } + return x; }; } 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 4be78f1f..b9b556a9 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -18,7 +18,9 @@ import java.util.function.Consumer; import java.util.stream.Collectors; import static java.lang.reflect.Modifier.isStatic; +import static java.util.Arrays.asList; import static java.util.Arrays.stream; +import static java.util.Collections.max; import static java.util.Optional.ofNullable; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL; @@ -325,6 +327,9 @@ public class RbacView { * A JPA entity class extending RbacObject which also implements an `rbac` method returning * its RBAC specification. * + * @param usingCase + * Only use this case value for a switch within the rbac rules. + * * @param fetchSql * An SQL SELECT statement which fetches the referenced row. Use `${REF}` to speficiy the * newly created or updated row (will be replaced by NEW/OLD from the trigger method). @@ -342,19 +347,29 @@ public class RbacView { * a JPA entity class extending RbacObject */ public RbacView importEntityAlias( - final String aliasName, final Class entityClass, + final String aliasName, final Class entityClass, final ColumnValue usingCase, final Column dependsOnColum, final SQL fetchSql, final Nullable nullable) { - importEntityAliasImpl(aliasName, entityClass, usingDefaultCase(), fetchSql, dependsOnColum, false, nullable); + importEntityAliasImpl(aliasName, entityClass, usingCase, fetchSql, dependsOnColum, false, nullable); return this; } private EntityAlias importEntityAliasImpl( - final String aliasName, final Class entityClass, final ColumnValue forCase, + final String aliasName, final Class entityClass, final ColumnValue usingCase, final SQL fetchSql, final Column dependsOnColum, boolean asSubEntity, final Nullable nullable) { - final var entityAlias = new EntityAlias(aliasName, entityClass, fetchSql, dependsOnColum, asSubEntity, nullable); - entityAliases.put(aliasName, entityAlias); + + final var entityAlias = ofNullable(entityAliases.get(aliasName)) + .orElseGet(() -> { + final var ea = new EntityAlias(aliasName, entityClass, usingCase, fetchSql, dependsOnColum, asSubEntity, nullable); + entityAliases.put(aliasName, ea); + return ea; + }); + try { - importAsAlias(aliasName, rbacDefinition(entityClass), forCase, asSubEntity); + // TODO.rbac: this only works for directly recursive RBAC definitions, not for indirect recursion + final var rbacDef = entityClass == rootEntityAlias.entityClass + ? this + : rbacDefinition(entityClass); + importAsAlias(aliasName, rbacDef, usingCase, asSubEntity); } catch (final ReflectiveOperationException exc) { throw new RuntimeException("cannot import entity: " + entityClass, exc); } @@ -369,7 +384,7 @@ public class RbacView { private RbacView importAsAlias(final String aliasName, final RbacView importedRbacView, final ColumnValue forCase, final boolean asSubEntity) { final var mapper = new AliasNameMapper(importedRbacView, aliasName, asSubEntity ? entityAliases.keySet() : null); - importedRbacView.getEntityAliases().values().stream() + copyOf(importedRbacView.getEntityAliases().values()).stream() .filter(entityAlias -> !importedRbacView.isRootEntityAlias(entityAlias)) .filter(entityAlias -> !entityAlias.isGlobal()) .filter(entityAlias -> !asSubEntity || !entityAliases.containsKey(entityAlias.aliasName)) @@ -377,10 +392,10 @@ public class RbacView { final String mappedAliasName = mapper.map(entityAlias.aliasName); entityAliases.put(mappedAliasName, new EntityAlias(mappedAliasName, entityAlias.entityClass)); }); - importedRbacView.getRoleDefs().forEach(roleDef -> { + copyOf(importedRbacView.getRoleDefs()).forEach(roleDef -> { new RbacRoleDefinition(findEntityAlias(mapper.map(roleDef.entityAlias.aliasName)), roleDef.role); }); - importedRbacView.getGrantDefs().forEach(grantDef -> { + copyOf(importedRbacView.getGrantDefs()).forEach(grantDef -> { if ( grantDef.grantType() == RbacGrantDefinition.GrantType.ROLE_TO_ROLE && (grantDef.forCases == null || grantDef.matchesCase(forCase)) ) { final var importedGrantDef = findOrCreateGrantDef( @@ -411,6 +426,10 @@ public class RbacView { return this; } + private static List copyOf(final Collection eas) { + return eas.stream().toList(); + } + private void verifyVersionColumnExists() { if (stream(rootEntityAlias.entityClass.getDeclaredFields()) .noneMatch(f -> f.getAnnotation(Version.class) != null)) { @@ -615,6 +634,13 @@ public class RbacView { return this; } + public long level() { + return max(asList( + superRoleDef != null ? superRoleDef.entityAlias.level() : 0, + subRoleDef != null ? subRoleDef.entityAlias.level() : 0, + permDef != null ? permDef.entityAlias.level() : 0)); + } + public enum GrantType { ROLE_TO_USER, ROLE_TO_ROLE, @@ -854,14 +880,14 @@ public class RbacView { return distinctGrantDef; } - record EntityAlias(String aliasName, Class entityClass, SQL fetchSql, Column dependsOnColum, boolean isSubEntity, Nullable nullable) { + record EntityAlias(String aliasName, Class entityClass, ColumnValue usingCase, SQL fetchSql, Column dependsOnColum, boolean isSubEntity, Nullable nullable) { public EntityAlias(final String aliasName) { - this(aliasName, null, null, null, false, null); + this(aliasName, null, null, null, null, false, null); } public EntityAlias(final String aliasName, final Class entityClass) { - this(aliasName, entityClass, null, null, false, null); + this(aliasName, entityClass, null, null, null, false, null); } boolean isGlobal() { @@ -873,7 +899,6 @@ public class RbacView { } @NotNull - @Override public SQL fetchSql() { if (fetchSql == null) { return SQL.noop(); @@ -914,6 +939,14 @@ public class RbacView { } return dependsOnColum.column; } + + long level() { + return aliasName.chars().filter(ch -> ch == '.').count() + 1; + } + + boolean isCaseDependent() { + return usingCase != null && usingCase.value != null; + } } public static String withoutRvSuffix(final String tableName) { @@ -1074,10 +1107,9 @@ public class RbacView { return new ColumnValue(null); } - public static ColumnValue usingCase(final String value) { - return new ColumnValue(value); + public static > ColumnValue usingCase(final E value) { + return new ColumnValue(value.name()); } - public final String value; private ColumnValue(final String value) { 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 96a956e5..3522a629 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchartGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchartGenerator.java @@ -15,6 +15,9 @@ public class RbacViewMermaidFlowchartGenerator { public static final String HOSTSHARING_LIGHT_ORANGE = "#feb28c"; public static final String HOSTSHARING_DARK_BLUE = "#274d6e"; public static final String HOSTSHARING_LIGHT_BLUE = "#99bcdb"; + + // TODO.rbac: implement level limit for all renderable items and remove items which not part of a grant + private static final long MAX_LEVEL_TO_RENDER = 3; private final RbacView rbacDef; private final CaseDef forCase; @@ -56,6 +59,7 @@ public class RbacViewMermaidFlowchartGenerator { flowchart.indented( () -> { rbacDef.getEntityAliases().values().stream() + .filter(e -> e.level() <= MAX_LEVEL_TO_RENDER) .filter(e -> e.aliasName().startsWith(entity.aliasName() + ":")) .forEach(this::renderEntitySubgraph); @@ -106,6 +110,7 @@ public class RbacViewMermaidFlowchartGenerator { private void renderGrants(final RbacView.RbacGrantDefinition.GrantType grantType, final String comment) { final var grantsOfRequestedType = rbacDef.getGrantDefs().stream() + .filter(g -> g.level() <= MAX_LEVEL_TO_RENDER) .filter(g -> g.grantType() == grantType) .filter(this::isToBeRenderedInThisGraph) .toList(); diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/test/dom/TestDomainEntity.java b/src/main/java/net/hostsharing/hsadminng/rbac/test/dom/TestDomainEntity.java index 38610de3..167618ad 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/test/dom/TestDomainEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/test/dom/TestDomainEntity.java @@ -14,6 +14,7 @@ import java.io.IOException; import java.util.UUID; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*; @@ -48,7 +49,7 @@ public class TestDomainEntity implements RbacObject { .withIdentityView(SQL.projection("name")) .withUpdatableColumns("version", "packageUuid", "description") - .importEntityAlias("package", TestPackageEntity.class, + .importEntityAlias("package", TestPackageEntity.class, usingDefaultCase(), dependsOnColumn("packageUuid"), directlyFetchedByDependsOnColumn(), NOT_NULL) diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageEntity.java b/src/main/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageEntity.java index c338e38e..c7161064 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/test/pac/TestPackageEntity.java @@ -14,6 +14,7 @@ import java.io.IOException; import java.util.UUID; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*; @@ -49,7 +50,7 @@ public class TestPackageEntity implements RbacObject { .withIdentityView(SQL.projection("name")) .withUpdatableColumns("version", "customerUuid", "description") - .importEntityAlias("customer", TestCustomerEntity.class, + .importEntityAlias("customer", TestCustomerEntity.class, usingDefaultCase(), dependsOnColumn("customerUuid"), directlyFetchedByDependsOnColumn(), NOT_NULL) diff --git a/src/main/resources/db/changelog/5-hs-office/506-debitor/5063-hs-office-debitor-rbac.md b/src/main/resources/db/changelog/5-hs-office/506-debitor/5063-hs-office-debitor-rbac.md index 57ce3e73..d6e546cf 100644 --- a/src/main/resources/db/changelog/5-hs-office/506-debitor/5063-hs-office-debitor-rbac.md +++ b/src/main/resources/db/changelog/5-hs-office/506-debitor/5063-hs-office-debitor-rbac.md @@ -149,16 +149,6 @@ role:debitorRel.holderPerson:ADMIN -.-> role:debitorRel.holderPerson:REFERRER role:global:ADMIN -.-> role:debitorRel.contact:OWNER role:debitorRel.contact:OWNER -.-> role:debitorRel.contact:ADMIN role:debitorRel.contact:ADMIN -.-> role:debitorRel.contact:REFERRER -role:global:ADMIN -.-> role:debitorRel:OWNER -role:debitorRel:OWNER -.-> role:debitorRel:ADMIN -role:debitorRel:ADMIN -.-> role:debitorRel:AGENT -role:debitorRel:AGENT -.-> role:debitorRel:TENANT -role:debitorRel.contact:ADMIN -.-> role:debitorRel:TENANT -role:debitorRel:TENANT -.-> role:debitorRel.anchorPerson:REFERRER -role:debitorRel:TENANT -.-> role:debitorRel.holderPerson:REFERRER -role:debitorRel:TENANT -.-> role:debitorRel.contact:REFERRER -role:debitorRel.anchorPerson:ADMIN -.-> role:debitorRel:OWNER -role:debitorRel.holderPerson:ADMIN -.-> role:debitorRel:AGENT role:global:ADMIN -.-> role:refundBankAccount:OWNER role:refundBankAccount:OWNER -.-> role:refundBankAccount:ADMIN role:refundBankAccount:ADMIN -.-> role:refundBankAccount:REFERRER diff --git a/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5073-hs-office-sepamandate-rbac.md b/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5073-hs-office-sepamandate-rbac.md index e3528f7f..7791348c 100644 --- a/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5073-hs-office-sepamandate-rbac.md +++ b/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5073-hs-office-sepamandate-rbac.md @@ -108,16 +108,6 @@ role:debitorRel.holderPerson:ADMIN -.-> role:debitorRel.holderPerson:REFERRER role:global:ADMIN -.-> role:debitorRel.contact:OWNER role:debitorRel.contact:OWNER -.-> role:debitorRel.contact:ADMIN role:debitorRel.contact:ADMIN -.-> role:debitorRel.contact:REFERRER -role:global:ADMIN -.-> role:debitorRel:OWNER -role:debitorRel:OWNER -.-> role:debitorRel:ADMIN -role:debitorRel:ADMIN -.-> role:debitorRel:AGENT -role:debitorRel:AGENT -.-> role:debitorRel:TENANT -role:debitorRel.contact:ADMIN -.-> role:debitorRel:TENANT -role:debitorRel:TENANT -.-> role:debitorRel.anchorPerson:REFERRER -role:debitorRel:TENANT -.-> role:debitorRel.holderPerson:REFERRER -role:debitorRel:TENANT -.-> role:debitorRel.contact:REFERRER -role:debitorRel.anchorPerson:ADMIN -.-> role:debitorRel:OWNER -role:debitorRel.holderPerson:ADMIN -.-> role:debitorRel:AGENT role:global:ADMIN -.-> role:bankAccount:OWNER role:bankAccount:OWNER -.-> role:bankAccount:ADMIN role:bankAccount:ADMIN -.-> role:bankAccount:REFERRER diff --git a/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5073-hs-office-sepamandate-rbac.sql b/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5073-hs-office-sepamandate-rbac.sql index 9f126a22..839c29f6 100644 --- a/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5073-hs-office-sepamandate-rbac.sql +++ b/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5073-hs-office-sepamandate-rbac.sql @@ -115,6 +115,7 @@ do language plpgsql $$ call defineContext('create INSERT INTO hs_office_sepamandate permissions for the related hs_office_relation rows'); FOR row IN SELECT * FROM hs_office_relation + WHERE type = 'DEBITOR' LOOP call grantPermissionToRole( createPermission(row.uuid, 'INSERT', 'hs_office_sepamandate'), @@ -131,9 +132,11 @@ create or replace function hs_office_sepamandate_hs_office_relation_insert_tf() language plpgsql strict as $$ begin - call grantPermissionToRole( + if NEW.type = 'DEBITOR' then + call grantPermissionToRole( createPermission(NEW.uuid, 'INSERT', 'hs_office_sepamandate'), hsOfficeRelationADMIN(NEW)); + end if; return NEW; end; $$; diff --git a/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.md b/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.md index 5cc8616f..9f94aaa5 100644 --- a/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.md +++ b/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.md @@ -216,16 +216,6 @@ role:debitor.debitorRel.holderPerson:ADMIN -.-> role:debitor.debitorRel.holderPe role:global:ADMIN -.-> role:debitor.debitorRel.contact:OWNER role:debitor.debitorRel.contact:OWNER -.-> role:debitor.debitorRel.contact:ADMIN role:debitor.debitorRel.contact:ADMIN -.-> role:debitor.debitorRel.contact:REFERRER -role:global:ADMIN -.-> role:debitor.debitorRel:OWNER -role:debitor.debitorRel:OWNER -.-> role:debitor.debitorRel:ADMIN -role:debitor.debitorRel:ADMIN -.-> role:debitor.debitorRel:AGENT -role:debitor.debitorRel:AGENT -.-> role:debitor.debitorRel:TENANT -role:debitor.debitorRel.contact:ADMIN -.-> role:debitor.debitorRel:TENANT -role:debitor.debitorRel:TENANT -.-> role:debitor.debitorRel.anchorPerson:REFERRER -role:debitor.debitorRel:TENANT -.-> role:debitor.debitorRel.holderPerson:REFERRER -role:debitor.debitorRel:TENANT -.-> role:debitor.debitorRel.contact:REFERRER -role:debitor.debitorRel.anchorPerson:ADMIN -.-> role:debitor.debitorRel:OWNER -role:debitor.debitorRel.holderPerson:ADMIN -.-> role:debitor.debitorRel:AGENT role:global:ADMIN -.-> role:debitor.refundBankAccount:OWNER role:debitor.refundBankAccount:OWNER -.-> role:debitor.refundBankAccount:ADMIN role:debitor.refundBankAccount:ADMIN -.-> role:debitor.refundBankAccount:REFERRER @@ -262,16 +252,6 @@ role:debitorRel.holderPerson:ADMIN -.-> role:debitorRel.holderPerson:REFERRER role:global:ADMIN -.-> role:debitorRel.contact:OWNER role:debitorRel.contact:OWNER -.-> role:debitorRel.contact:ADMIN role:debitorRel.contact:ADMIN -.-> role:debitorRel.contact:REFERRER -role:global:ADMIN -.-> role:debitorRel:OWNER -role:debitorRel:OWNER -.-> role:debitorRel:ADMIN -role:debitorRel:ADMIN -.-> role:debitorRel:AGENT -role:debitorRel:AGENT -.-> role:debitorRel:TENANT -role:debitorRel.contact:ADMIN -.-> role:debitorRel:TENANT -role:debitorRel:TENANT -.-> role:debitorRel.anchorPerson:REFERRER -role:debitorRel:TENANT -.-> role:debitorRel.holderPerson:REFERRER -role:debitorRel:TENANT -.-> role:debitorRel.contact:REFERRER -role:debitorRel.anchorPerson:ADMIN -.-> role:debitorRel:OWNER -role:debitorRel.holderPerson:ADMIN -.-> role:debitorRel:AGENT role:debitorRel:AGENT ==> role:bookingItem:OWNER role:bookingItem:OWNER ==> role:bookingItem:ADMIN role:debitorRel:AGENT ==> role:bookingItem:ADMIN diff --git a/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.sql b/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.sql index b2add620..5b40e779 100644 --- a/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.sql +++ b/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.sql @@ -111,7 +111,7 @@ do language plpgsql $$ call defineContext('create INSERT INTO hs_booking_item permissions for the related hs_office_relation rows'); FOR row IN SELECT * FROM hs_office_relation - WHERE type in ('DEBITOR') -- TODO.rbac: currently manually patched, needs to be generated + WHERE type = 'DEBITOR' LOOP call grantPermissionToRole( createPermission(row.uuid, 'INSERT', 'hs_booking_item'), @@ -128,11 +128,11 @@ create or replace function hs_booking_item_hs_office_relation_insert_tf() language plpgsql strict as $$ begin - if NEW.type = 'DEBITOR' then -- TODO.rbac: currently manually patched, needs to be generated - call grantPermissionToRole( + if NEW.type = 'DEBITOR' then + call grantPermissionToRole( createPermission(NEW.uuid, 'INSERT', 'hs_booking_item'), hsOfficeRelationADMIN(NEW)); - end if; + end if; return NEW; end; $$; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java index 7e09519c..a26eda11 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java @@ -141,8 +141,6 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean .map(s -> s.replace("hs_office_", "")) .containsExactlyInAnyOrder(distinct(from( initialGrantNames, - // TODO.rbac: this grant should only be created for DEBITOR-Relationships, thus the RBAC DSL needs to support conditional grants - "{ grant perm:relation#HostsharingeG-with-PARTNER-EBess:INSERT>sepamandate to role:relation#HostsharingeG-with-PARTNER-EBess:ADMIN by system and assume }", // permissions on partner "{ grant perm:partner#P-20032:DELETE to role:relation#HostsharingeG-with-PARTNER-EBess:OWNER by system and assume }", 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 8e632c21..9c251466 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 @@ -131,8 +131,6 @@ class HsOfficeRelationRepositoryIntegrationTest extends ContextBasedTestWithClea "hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:TENANT")); assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(Array.fromFormatted( initialGrantNames, - // TODO.rbac: 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-REPRESENTATIVE-BesslerBert:INSERT>hs_office_sepamandate to role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:ADMIN by system and assume }", "{ grant perm:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:DELETE to role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:OWNER by system and assume }", "{ grant role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:OWNER to role:global#global:ADMIN by system and assume }",