From b2cea1e88296ab3f16d2cb05e544d7a54ce4eb66 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 5 Mar 2024 09:26:39 +0100 Subject: [PATCH] insert (into) table permission --- .../office/person/HsOfficePersonEntity.java | 4 +- .../HsOfficeRelationshipEntity.java | 2 +- .../rbac/rbacdef/InsertTriggerGenerator.java | 76 ++++++ .../rbac/rbacdef/RbacObjectGenerator.java | 28 +++ .../rbacdef/RbacRestrictedViewGenerator.java | 7 +- .../hsadminng/rbac/rbacdef/RbacView.java | 221 ++++++++++++------ .../rbacdef/RbacViewMermaidFlowchart.java | 16 +- .../rbacdef/RbacViewPostgresGenerator.java | 1 + .../hsadminng/rbac/rbacdef/StringWriter.java | 12 +- .../hsadminng/rbac/rbacdef/package-info.java | 5 + .../test/cust/TestCustomerEntity.java | 20 +- .../hsadminng/test/pac/TestPackageEntity.java | 44 +++- .../resources/db/changelog/050-rbac-base.sql | 53 ++++- .../test/cust/TestCustomerEntityTest.java | 15 +- .../test/pac/TestPackageEntityTest.java | 71 ++++++ 15 files changed, 464 insertions(+), 111 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacObjectGenerator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/package-info.java create mode 100644 src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityTest.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonEntity.java index 104b5d61..37874df3 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonEntity.java @@ -5,6 +5,7 @@ import lombok.experimental.FieldNameConstants; import net.hostsharing.hsadminng.errors.DisplayName; import net.hostsharing.hsadminng.persistence.HasUuid; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; +import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; import org.apache.commons.lang3.StringUtils; @@ -17,7 +18,6 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*; -import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.projection; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.stringify.Stringify.stringify; @@ -67,7 +67,7 @@ public class HsOfficePersonEntity implements HasUuid, Stringifyable { public static RbacView rbac() { return rbacViewFor("person", HsOfficePersonEntity.class) - .withIdentityView(projection("concat(tradeName, familyName, givenName)")) + .withIdentityView(SQL.projection("concat(tradeName, familyName, givenName)")) .withUpdatableColumns("personType", "tradeName", "givenName", "familyName") .createRole(OWNER, (with) -> { with.permission(ALL); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipEntity.java index 72185b64..bb555162 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipEntity.java @@ -85,7 +85,7 @@ public class HsOfficeRelationshipEntity implements HasUuid, Stringifyable { || '-with-' || target.relType || '-' || (select idName from hs_office_person_iv p where p.uuid = relHolderUuid) """)) - .withRestrictedViewOrderedBy(SQL.expression( + .withRestrictedViewOrderBy(SQL.expression( "(select idName from hs_office_person_iv p where p.uuid = target.relHolderUuid)")) .withUpdatableColumns("contactUuid") .importEntityAlias("anchorPerson", HsOfficePersonEntity.class, diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java new file mode 100644 index 00000000..bff58ce5 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java @@ -0,0 +1,76 @@ +package net.hostsharing.hsadminng.rbac.rbacdef; + +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.PERM_TO_ROLE; +import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with; + +public class InsertTriggerGenerator { + + private final RbacView rbacDef; + private final String liquibaseTagPrefix; + + public InsertTriggerGenerator(final RbacView rbacDef, final String liqibaseTagPrefix) { + this.rbacDef = rbacDef; + this.liquibaseTagPrefix = liqibaseTagPrefix; + } + + void generateTo(final StringWriter plPgSql) { + generateLiquibaseChangesetHeader(plPgSql); + generateGrantInsertRoleToExistingCustomers(plPgSql); + rbacDef.getGrantDefs().stream() + .filter(g -> g.isToCreate() && g.grantType() == PERM_TO_ROLE && + g.getPermDef().getPermission() == RbacView.Permission.INSERT ) + .forEach(g -> { + plPgSql.writeLn(""" + /** + Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable}. + */ + create trigger ${rawSubTable}_it + before insert + on ${rawSubTable} + for each row + when ( hasInsertPermission(NEW.${referenceColumn}, 'INSERT', '${rawSubTable}') ) + execute procedure insertNotAllowedForCurrentSubjects('${rawSubTable}'); + """, + with("rawSubTable", g.getPermDef().entityAlias.getRawTableName()), + with("referenceColumn", g.getSuperRoleDef().getEntityAlias().dependsOnColumName() )); + }); + + plPgSql.writeLn("--//"); + } + + private void generateLiquibaseChangesetHeader(final StringWriter plPgSql) { + plPgSql.writeLn(""" + -- ============================================================================ + --changeset ${liquibaseTagPrefix}-rbac-INSERT:1 endDelimiter:--// + -- ---------------------------------------------------------------------------- + """, + with("liquibaseTagPrefix", liquibaseTagPrefix)); + } + + private void generateGrantInsertRoleToExistingCustomers(final StringWriter plPgSql) { + plPgSql.writeLn(""" + /* + Creates an INSERT INTO ${rawSubTableName} permission for the related ${rawSuperTableName} row. + */ + do language plpgsql $$ + declare + row ${rawSuperTableName}; + permissionUuids uuid[]; + roleUuid uuid; + begin + FOR row IN SELECT * FROM ${rawSuperTableName} + LOOP + roleUuid := ${rawSuperRoleDescriptor}(row); + permissionUuids := createPermissions(row.uuid, array ['INSERT:${rawSubTableName}']); + call grantPermissionsToRole(roleUuid, permissionUuids); + END LOOP; + END; + $$; + """, + with("rawSubTableName", "test_package"), // TODO + with("rawSuperTableName", "test_customer"), // TODO + with("rawSuperRoleDescriptor", "testCustomerAdmin") // TODO + ); + } + +} diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacObjectGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacObjectGenerator.java new file mode 100644 index 00000000..9c1579af --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacObjectGenerator.java @@ -0,0 +1,28 @@ +package net.hostsharing.hsadminng.rbac.rbacdef; + +import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with; + +public class RbacObjectGenerator { + + private final String liquibaseTagPrefix; + private final String rawTableName; + + public RbacObjectGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) { + this.liquibaseTagPrefix = liquibaseTagPrefix; + this.rawTableName = rbacDef.getRootEntityAlias().getRawTableName(); + } + + void generateTo(final StringWriter plPgSql) { + plPgSql.writeLn(""" + + -- ============================================================================ + --changeset ${liquibaseTagPrefix}-rbac-OBJECT:1 endDelimiter:--// + -- ---------------------------------------------------------------------------- + call generateRelatedRbacObject('${rawTableName}'); + --// + + """, + with("liquibaseTagPrefix", liquibaseTagPrefix), + with("rawTableName", rawTableName)); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacRestrictedViewGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacRestrictedViewGenerator.java index 3755b20f..32f2d8e0 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacRestrictedViewGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacRestrictedViewGenerator.java @@ -2,6 +2,7 @@ package net.hostsharing.hsadminng.rbac.rbacdef; import static java.util.stream.Collectors.joining; +import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.indented; import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with; public class RbacRestrictedViewGenerator { @@ -26,16 +27,16 @@ public class RbacRestrictedViewGenerator { call generateRbacRestrictedView('${rawTableName}', '${orderBy}', $updates$ - ${updates} + ${updates} $updates$); --// """, with("liquibaseTagPrefix", liquibaseTagPrefix), with("orderBy", rbacDef.getOrderBySqlExpression().sql), - with("updates", rbacDef.getUpdatableColumns().stream() + with("updates", indented(rbacDef.getUpdatableColumns().stream() .map(c -> c + " = new." + c) - .collect(joining("\n"))), + .collect(joining(",\n")), 2)), with("rawTableName", rawTableName)); } } 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 b60a1d29..b92f8f38 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -1,18 +1,30 @@ package net.hostsharing.hsadminng.rbac.rbacdef; -import java.lang.reflect.Method; -import java.nio.file.Path; -import java.util.function.Consumer; - import lombok.EqualsAndHashCode; import lombok.Getter; +import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity; +import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity; +import net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionEntity; +import net.hostsharing.hsadminng.hs.office.coopshares.HsOfficeCoopSharesTransactionEntity; +import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; +import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity; +import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerDetailsEntity; +import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity; +import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; +import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity; +import net.hostsharing.hsadminng.hs.office.sepamandate.HsOfficeSepaMandateEntity; import net.hostsharing.hsadminng.persistence.HasUuid; import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; +import net.hostsharing.hsadminng.test.cust.TestCustomerEntity; +import net.hostsharing.hsadminng.test.pac.TestPackageEntity; import jakarta.persistence.Table; import jakarta.validation.constraints.NotNull; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.file.Path; import java.util.*; +import java.util.function.Consumer; import java.util.stream.Stream; import static java.lang.reflect.Modifier.isStatic; @@ -36,7 +48,7 @@ public class RbacView { @Override public EntityAlias put(final String key, final EntityAlias value) { - if ( containsKey(key) ) { + if (containsKey(key)) { throw new IllegalArgumentException("duplicate entityAlias: " + key); } return super.put(key, value); @@ -60,6 +72,7 @@ public class RbacView { new RbacUserReference(CREATOR); entityAliases.put("global", new EntityAlias("global")); } + public RbacView withUpdatableColumns(final String... columnNames) { Collections.addAll(updatableColumns, columnNames); return this; @@ -70,7 +83,7 @@ public class RbacView { return this; } - public RbacView withRestrictedViewOrderedBy(final SQL orderBySqlExpression) { + public RbacView withRestrictedViewOrderBy(final SQL orderBySqlExpression) { this.orderBySqlExpression = orderBySqlExpression; return this; } @@ -106,21 +119,22 @@ public class RbacView { } private RbacPermissionDefinition createPermission(final EntityAlias entityAlias, final Permission permission) { - final RbacPermissionDefinition permDef = new RbacPermissionDefinition(entityAlias, permission, true); - permDefs.add(permDef); - return permDef; + return new RbacPermissionDefinition(entityAlias, permission, null, true); } public RbacView declarePlaceholderEntityAliases(final String... aliasNames) { - for ( String alias: aliasNames ) { + for (String alias : aliasNames) { entityAliases.put(alias, new EntityAlias(alias)); } return this; } public RbacView importRootEntityAliasProxy( - final String aliasName, final Class entityClass, final SQL fetchSql, final Column dependsOnColum) { - if ( rootEntityAliasProxy != null ) { + final String aliasName, + final Class entityClass, + final SQL fetchSql, + final Column dependsOnColum) { + if (rootEntityAliasProxy != null) { throw new IllegalStateException("there is already an entityAliasProxy: " + rootEntityAliasProxy); } rootEntityAliasProxy = importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, false); @@ -155,7 +169,7 @@ public class RbacView { entityAliases.put(aliasName, entityAlias); try { importAsAlias(aliasName, rbacDefinition(entityClass), asSubEntity); - } catch ( final ReflectiveOperationException exc) { + } catch (final ReflectiveOperationException exc) { throw new RuntimeException("cannot import entity: " + entityClass, exc); } return entityAlias; @@ -178,13 +192,17 @@ public class RbacView { entityAliases.put(mappedAliasName, new EntityAlias(mappedAliasName, entityAlias.entityClass)); }); importedRbacView.getRoleDefs().forEach(roleDef -> { - new RbacRoleDefinition( findEntityAlias(mapper.map(roleDef.entityAlias.aliasName)), roleDef.role); + new RbacRoleDefinition(findEntityAlias(mapper.map(roleDef.entityAlias.aliasName)), roleDef.role); }); importedRbacView.getGrantDefs().forEach(grantDef -> { if (grantDef.grantType() == RbacGrantDefinition.GrantType.ROLE_TO_ROLE) { findOrCreateGrantDef( - findRbacRole(mapper.map(grantDef.getSubRoleDef().entityAlias.aliasName), grantDef.getSubRoleDef().getRole()), - findRbacRole(mapper.map(grantDef.getSuperRoleDef().entityAlias.aliasName), grantDef.getSuperRoleDef().getRole()) + findRbacRole( + mapper.map(grantDef.getSubRoleDef().entityAlias.aliasName), + grantDef.getSubRoleDef().getRole()), + findRbacRole( + mapper.map(grantDef.getSuperRoleDef().entityAlias.aliasName), + grantDef.getSuperRoleDef().getRole()) ); } }); @@ -199,16 +217,19 @@ public class RbacView { return new RbacExampleRole(entityAlias, role); } - - private RbacGrantDefinition grantRoleToUser(final RbacRoleDefinition roleDefinition, final RbacUserReference user) { + private RbacGrantDefinition grantRoleToUser(final RbacRoleDefinition roleDefinition, final RbacUserReference user) { return findOrCreateGrantDef(roleDefinition, user).toCreate(); } - private RbacGrantDefinition grantPermissionToRole(final RbacPermissionDefinition permDef , final RbacRoleDefinition roleDef) { + private RbacGrantDefinition grantPermissionToRole( + final RbacPermissionDefinition permDef, + final RbacRoleDefinition roleDef) { return findOrCreateGrantDef(permDef, roleDef).toCreate(); } - private RbacGrantDefinition grantSubRoleToSuperRole(final RbacRoleDefinition subRoleDefinition, final RbacRoleDefinition superRoleDefinition) { + private RbacGrantDefinition grantSubRoleToSuperRole( + final RbacRoleDefinition subRoleDefinition, + final RbacRoleDefinition superRoleDefinition) { return findOrCreateGrantDef(subRoleDefinition, superRoleDefinition).toCreate(); } @@ -220,6 +241,13 @@ public class RbacView { return entityAlias == rootEntityAliasProxy; } + public SQL getOrderBySqlExpression() { + if (orderBySqlExpression == null) { + return identityViewSqlQuery; + } + return orderBySqlExpression; + } + public void generateWithBaseFileName(final String baseFileName) { new RbacViewMermaidFlowchart(this).generateToMarkdownFile(Path.of(OUTPUT_BASEDIR, baseFileName + "-generated.md")); new RbacViewPostgresGenerator(this).generateToChangeLog(Path.of(OUTPUT_BASEDIR, baseFileName + "-generated.sql")); @@ -238,16 +266,25 @@ public class RbacView { return RbacView.this; } + public RbacView grantPermission(final String entityAliasName, final Permission perm) { + final var entityAlias = findEntityAlias(entityAliasName); + final var forTable = entityAlias.getRawTableName(); + findOrCreateGrantDef(findRbacPerm(entityAlias, perm, forTable), superRoleDef).toCreate(); + return RbacView.this; + } + } @Getter @EqualsAndHashCode public class RbacGrantDefinition { + private final RbacUserReference userDef; private final RbacRoleDefinition superRoleDef; private final RbacRoleDefinition subRoleDef; private final RbacPermissionDefinition permDef; - private boolean toCreate; + private boolean assumed = true; + private boolean toCreate = false; @Override public String toString() { @@ -295,8 +332,7 @@ public class RbacView { } boolean isAssumed() { - // TODO: not implemented yet - return true; + return assumed; } boolean isToCreate() { @@ -310,7 +346,7 @@ public class RbacView { boolean dependsOnColumn(final String columnName) { return dependsRoleDefOnColumnName(this.superRoleDef, columnName) - || dependsRoleDefOnColumnName(this.subRoleDef, columnName); + || dependsRoleDefOnColumnName(this.subRoleDef, columnName); } private Boolean dependsRoleDefOnColumnName(final RbacRoleDefinition superRoleDef, final String columnName) { @@ -320,6 +356,10 @@ public class RbacView { .orElse(false); } + public void unassumed() { + this.assumed = false; + } + public enum GrantType { ROLE_TO_USER, ROLE_TO_ROLE, @@ -352,22 +392,25 @@ public class RbacView { final EntityAlias entityAlias; final Permission permission; + final String tableName; final boolean toCreate; - private RbacPermissionDefinition(final EntityAlias entityAlias, final Permission permission, final boolean toCreate) { + private RbacPermissionDefinition(final EntityAlias entityAlias, final Permission permission, final String tableName, final boolean toCreate) { this.entityAlias = entityAlias; this.permission = permission; + this.tableName = tableName; this.toCreate = toCreate; + permDefs.add(this); } public RbacView grantedTo(final String entityAlias, final Role role) { - findOrCreateGrantDef(this, findRbacRole(entityAlias, role) ).toCreate(); + findOrCreateGrantDef(this, findRbacRole(entityAlias, role)).toCreate(); return RbacView.this; } @Override public String toString() { - return "perm:" + entityAlias.aliasName + permission; + return "perm:" + entityAlias.aliasName + permission + ofNullable(tableName).map(tn -> ":" + tn).orElse(""); } } @@ -390,26 +433,22 @@ public class RbacView { return this; } - public RbacRoleDefinition owningUser(final RbacUserReference.UserRole userRole) { - grantRoleToUser(this, findUserRef(userRole)); - return this; + public RbacGrantDefinition owningUser(final RbacUserReference.UserRole userRole) { + return grantRoleToUser(this, findUserRef(userRole)); } - public RbacRoleDefinition permission(final Permission permission) { - grantPermissionToRole( createPermission(entityAlias, permission) , this); - return this; + public RbacGrantDefinition permission(final Permission permission) { + return grantPermissionToRole(createPermission(entityAlias, permission), this); } - public RbacRoleDefinition incomingSuperRole(final String entityAlias, final Role role) { - final var incomingSuperRole = findRbacRole(entityAlias, role); - grantSubRoleToSuperRole(this, incomingSuperRole); - return this; + public RbacGrantDefinition incomingSuperRole(final String entityAlias, final Role role) { + final var incomingSuperRole = findRbacRole(entityAlias, role); + return grantSubRoleToSuperRole(this, incomingSuperRole); } - public RbacRoleDefinition outgoingSubRole(final String entityAlias, final Role role) { - final var outgoingSubRole = findRbacRole(entityAlias, role); - grantSubRoleToSuperRole(outgoingSubRole, this); - return this; + public RbacGrantDefinition outgoingSubRole(final String entityAlias, final Role role) { + final var outgoingSubRole = findRbacRole(entityAlias, role); + return grantSubRoleToSuperRole(outgoingSubRole, this); } @Override @@ -430,6 +469,7 @@ public class RbacView { } final UserRole role; + public RbacUserReference(final UserRole creator) { this.role = creator; userDefs.add(this); @@ -443,7 +483,7 @@ public class RbacView { EntityAlias findEntityAlias(final String aliasName) { final var found = entityAliases.get(aliasName); - if ( found == null ) { + if (found == null) { throw new IllegalArgumentException("entityAlias not found: " + aliasName); } return found; @@ -461,6 +501,26 @@ public class RbacView { } + RbacPermissionDefinition findRbacPerm(final EntityAlias entityAlias, final Permission perm, String tableName) { + return permDefs.stream() + .filter(p -> p.getEntityAlias() == entityAlias && p.getPermission() == perm) + .findFirst() + .orElseGet(() -> new RbacPermissionDefinition(entityAlias, perm, tableName, true)); // TODO: true => toCreate + } + + + RbacPermissionDefinition findRbacPerm(final EntityAlias entityAlias, final Permission perm) { + return findRbacPerm(entityAlias, perm, null); + } + + public RbacPermissionDefinition findRbacPerm(final String entityAliasName, final Permission perm, String tableName) { + return findRbacPerm(findEntityAlias(entityAliasName), perm, tableName); + } + + public RbacPermissionDefinition findRbacPerm(final String entityAliasName, final Permission perm) { + return findRbacPerm(findEntityAlias(entityAliasName), perm); + } + private RbacGrantDefinition findOrCreateGrantDef(final RbacRoleDefinition roleDefinition, final RbacUserReference user) { return grantDefs.stream() .filter(g -> g.subRoleDef == roleDefinition && g.userDef == user) @@ -475,7 +535,9 @@ public class RbacView { .orElseGet(() -> new RbacGrantDefinition(permDef, roleDef)); } - private RbacGrantDefinition findOrCreateGrantDef(final RbacRoleDefinition subRoleDefinition, final RbacRoleDefinition superRoleDefinition) { + private RbacGrantDefinition findOrCreateGrantDef( + final RbacRoleDefinition subRoleDefinition, + final RbacRoleDefinition superRoleDefinition) { return grantDefs.stream() .filter(g -> g.subRoleDef == subRoleDefinition && g.superRoleDef == superRoleDefinition) .findFirst() @@ -484,31 +546,32 @@ public class RbacView { record EntityAlias(String aliasName, Class entityClass, SQL fetchSql, Column dependsOnColum, boolean isSubEntity) { - public EntityAlias(final String aliasName) { - this(aliasName, null, null, null, false); - } + public EntityAlias(final String aliasName) { + this(aliasName, null, null, null, false); + } - public EntityAlias(final String aliasName, final Class entityClass) { - this(aliasName, entityClass, null, null, false); - } + public EntityAlias(final String aliasName, final Class entityClass) { + this(aliasName, entityClass, null, null, false); + } boolean isGlobal() { return aliasName().equals("global"); } boolean isPlaceholder() { - return entityClass == null; + return entityClass == null; } @NotNull @Override public SQL fetchSql() { - if ( fetchSql == null ) { + if (fetchSql == null) { return SQL.noop(); } return switch (fetchSql.part) { case SQL_QUERY -> fetchSql; - case AUTO_FETCH -> SQL.query("SELECT * FROM " + getRawTableName() + " WHERE uuid = ${ref}." + dependsOnColum.column); + case AUTO_FETCH -> + SQL.query("SELECT * FROM " + getRawTableName() + " WHERE uuid = ${ref}." + dependsOnColum.column); default -> throw new IllegalStateException("unexpected SQL definition: " + fetchSql); }; } @@ -518,7 +581,7 @@ public class RbacView { } private String withoutEntitySuffix(final String simpleEntityName) { - return simpleEntityName.substring(0, simpleEntityName.length()-"Entity".length()); + return simpleEntityName.substring(0, simpleEntityName.length() - "Entity".length()); } String simpleName() { @@ -530,12 +593,22 @@ public class RbacView { String getRawTableName() { return withoutRvSuffix(entityClass.getAnnotation(Table.class).name()); } + + String dependsOnColumName() { + if (dependsOnColum == null) { + throw new IllegalStateException( + "Entity " + aliasName + "(" + entityClass.getSimpleName() + ")" + ": please add dependsOnColum"); + } + return dependsOnColum.column; + } } + public static String withoutRvSuffix(final String tableName) { - return tableName.substring(0, tableName.length()-"_rv".length()); + return tableName.substring(0, tableName.length() - "_rv".length()); } public record Role(String roleName) { + public static final Role OWNER = new Role("owner"); public static final Role ADMIN = new Role("admin"); public static final Role AGENT = new Role("agent"); @@ -549,11 +622,13 @@ public class RbacView { @Override public boolean equals(final Object obj) { - return ((obj instanceof Role) && ((Role)obj).roleName.equals(this.roleName)); + return ((obj instanceof Role) && ((Role) obj).roleName.equals(this.roleName)); } } public record Permission(String permission) { + + public static final Permission INSERT = new Permission("insert"); public static final Permission ALL = new Permission("*"); public static final Permission EDIT = new Permission("edit"); public static final Permission VIEW = new Permission("view"); @@ -604,7 +679,8 @@ public class RbacView { return new SQL(null, Part.NOOP); } - /** Generic DSL method to specify an SQL SELECT expression. + /** + * Generic DSL method to specify an SQL SELECT expression. * * @param sql an SQL SELECT expression (not ending with ';) * @return the wrapped SQL expression @@ -614,7 +690,8 @@ public class RbacView { return new SQL(sql, Part.SQL_QUERY); } - /** Generic DSL method to specify an SQL SELECT expression by just the projection part. + /** + * Generic DSL method to specify an SQL SELECT expression by just the projection part. * * @param projection an SQL SELECT expression, the list of columns after 'SELECT' * @return the wrapped SQL projection @@ -688,10 +765,10 @@ public class RbacView { } String map(final String originalAliasName) { - if (outerAliasNames.contains(originalAliasName) || originalAliasName.equals("global")) { + if (outerAliasNames.contains(originalAliasName) || originalAliasName.equals("global")) { return originalAliasName; } - if (originalAliasName.equals(importedRbacView.rootEntityAlias.aliasName) ) { + if (originalAliasName.equals(importedRbacView.rootEntityAlias.aliasName)) { return outerAliasName; } return outerAliasName + "." + originalAliasName; @@ -700,17 +777,19 @@ public class RbacView { public static void main(String[] args) { Stream.of( - net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity.class, - net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity.class, - net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerDetailsEntity.class, - net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity.class, - net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity.class, - net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity.class, - net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionEntity.class, - net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity.class, - net.hostsharing.hsadminng.hs.office.sepamandate.HsOfficeSepaMandateEntity.class, - net.hostsharing.hsadminng.hs.office.coopshares.HsOfficeCoopSharesTransactionEntity.class, - net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity.class + TestCustomerEntity.class, + TestPackageEntity.class, + HsOfficePersonEntity.class, + HsOfficePartnerEntity.class, + HsOfficePartnerDetailsEntity.class, + HsOfficeBankAccountEntity.class, + HsOfficeDebitorEntity.class, + HsOfficeRelationshipEntity.class, + HsOfficeCoopAssetsTransactionEntity.class, + HsOfficeContactEntity.class, + HsOfficeSepaMandateEntity.class, + HsOfficeCoopSharesTransactionEntity.class, + HsOfficeMembershipEntity.class ).forEach(c -> { final Method mainMethod = Arrays.stream(c.getMethods()).filter( m -> isStatic(m.getModifiers()) && m.getName().equals("main") @@ -719,7 +798,7 @@ public class RbacView { .orElse(null); if (mainMethod != null) { try { - mainMethod.invoke(null, new Object[]{null}); + mainMethod.invoke(null, new Object[] { null }); } catch (IllegalAccessException | InvocationTargetException e) { throw new RuntimeException(e); } diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java index ef7fa3b0..27615f85 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java @@ -97,21 +97,21 @@ public class RbacViewMermaidFlowchart { renderGrants(PERM_TO_ROLE, "%% granting permissions to roles"); } - private void renderGrants(final RbacView.RbacGrantDefinition.GrantType f, final String t) { - final var userGrants = rbacDef.getGrantDefs().stream() - .filter(g -> g.grantType() == f) + private void renderGrants(final RbacView.RbacGrantDefinition.GrantType grantType, final String comment) { + final var grantsOfRequestedType = rbacDef.getGrantDefs().stream() + .filter(g -> g.grantType() == grantType) .toList(); - if ( !userGrants.isEmpty()) { + if ( !grantsOfRequestedType.isEmpty()) { flowchart.ensureSingleEmptyLine(); - flowchart.writeLn(t); - userGrants.forEach(g -> flowchart.writeLn(grantDef(g))); + flowchart.writeLn(comment); + grantsOfRequestedType.forEach(g -> flowchart.writeLn(grantDef(g))); } } private String grantDef(final RbacView.RbacGrantDefinition grant) { final var arrow = grant.isToCreate() - ? grant.isAssumed() ? " ==> " : " == // ==> " - : grant.isAssumed() ? " -.-> " : " -.- // -.-> "; + ? grant.isAssumed() ? " ==> " : " == /// ==> " + : grant.isAssumed() ? " -.-> " : " -.- /// -.-> "; return switch (grant.grantType()) { case ROLE_TO_USER -> // TODO: other user types not implemented yet diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java index 11c2dc8c..eb8f3534 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java @@ -30,6 +30,7 @@ public class RbacViewPostgresGenerator { new RbacObjectGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql); new RbacRoleDescriptorsGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql); new RolesGrantsAndPermissionsGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql); + new InsertTriggerGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql); new RbacIdentityViewGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql); new RbacRestrictedViewGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql); } 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 00f684e2..512ec72d 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/StringWriter.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/StringWriter.java @@ -68,10 +68,7 @@ public class StringWriter { return string.toString(); } - private String indented(final String text) { - if ( indentLevel == 0) { - return text; - } + public static String indented(final String text, final int indentLevel) { final var indentation = StringUtils.repeat(" ", indentLevel); final var indented = stream(text.split("\n")) .map(line -> line.trim().isBlank() ? "" : indentation + line) @@ -79,6 +76,13 @@ public class StringWriter { return indented; } + private String indented(final String text) { + if ( indentLevel == 0) { + return text; + } + return indented(text, indentLevel); + } + record VarDef(String name, String value){} private static final class VarReplacer { diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/package-info.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/package-info.java new file mode 100644 index 00000000..2a193f2f --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/package-info.java @@ -0,0 +1,5 @@ +package net.hostsharing.hsadminng.rbac.rbacdef; + +// TODO: The whole code in this package is more like a quick hack to solve an urgent problem. +// It should be re-written in PostgreSQL pl/pgsql, +// so that no Java is needed to use this RBAC system in it's full extend. diff --git a/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java b/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java index b7baf3b8..57e29475 100644 --- a/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java @@ -4,16 +4,16 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import net.hostsharing.hsadminng.persistence.HasUuid; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; -import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import jakarta.persistence.*; +import java.io.IOException; import java.util.UUID; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL; -import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.ALL; -import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.VIEW; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; @@ -24,7 +24,7 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; @Setter @NoArgsConstructor @AllArgsConstructor -public class TestCustomerEntity implements RbacObject { +public class TestCustomerEntity implements HasUuid { @Id @GeneratedValue @@ -36,22 +36,24 @@ public class TestCustomerEntity implements RbacObject { @Column(name = "adminusername") private String adminUserName; - - public static RbacView rbac() { return rbacViewFor("customer", TestCustomerEntity.class) .withIdentityView(SQL.projection("prefix")) + .withRestrictedViewOrderBy(SQL.expression("reference")) .withUpdatableColumns("reference", "prefix", "adminUserName") + .createRole(OWNER, (with) -> { with.owningUser(CREATOR); with.incomingSuperRole(GLOBAL, ADMIN); with.permission(ALL); }) - .createSubRole(ADMIN, (with) -> { - with.permission(RbacView.Permission.custom("add-package")); - }) + .createSubRole(ADMIN) .createSubRole(TENANT, (with) -> { with.permission(VIEW); }); } + + public static void main(String[] args) throws IOException { + rbac().generateWithBaseFileName("113-test-customer-rbac"); + } } diff --git a/src/main/java/net/hostsharing/hsadminng/test/pac/TestPackageEntity.java b/src/main/java/net/hostsharing/hsadminng/test/pac/TestPackageEntity.java index 8687666f..ceb88ec8 100644 --- a/src/main/java/net/hostsharing/hsadminng/test/pac/TestPackageEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/test/pac/TestPackageEntity.java @@ -4,18 +4,29 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import net.hostsharing.hsadminng.persistence.HasUuid; +import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; +import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.test.cust.TestCustomerEntity; import jakarta.persistence.*; +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.Permission.*; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.*; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; + @Entity @Table(name = "test_package_rv") @Getter @Setter @NoArgsConstructor @AllArgsConstructor -public class TestPackageEntity { +public class TestPackageEntity implements HasUuid { @Id @GeneratedValue @@ -31,4 +42,35 @@ public class TestPackageEntity { private String name; private String description; + + + public static RbacView rbac() { + return rbacViewFor("package", TestPackageEntity.class) + .withIdentityView(SQL.projection("name")) + .withUpdatableColumns("customerUuid", "description") + + .importEntityAlias("customer", TestCustomerEntity.class, + dependsOnColumn("customerUuid"), + fetchedBySql(""" + SELECT * FROM test_customer c + WHERE c.uuid= ${ref}.customerUuid + """)) + .toRole("customer", ADMIN).grantPermission("package", INSERT) + + .createRole(OWNER, (with) -> { + with.owningUser(CREATOR); + with.incomingSuperRole("customer", ADMIN).unassumed(); + with.permission(ALL); + with.permission(EDIT); + }) + .createSubRole(ADMIN) + .createSubRole(TENANT, (with) -> { + with.outgoingSubRole("customer", TENANT); + with.permission(VIEW); + }); + } + + public static void main(String[] args) throws IOException { + rbac().generateWithBaseFileName("123-test-package-rbac"); + } } diff --git a/src/main/resources/db/changelog/050-rbac-base.sql b/src/main/resources/db/changelog/050-rbac-base.sql index fe2f30ae..5e2726f7 100644 --- a/src/main/resources/db/changelog/050-rbac-base.sql +++ b/src/main/resources/db/changelog/050-rbac-base.sql @@ -378,9 +378,10 @@ create domain RbacOp as varchar(67) create table RbacPermission ( - uuid uuid primary key references RbacReference (uuid) on delete cascade, - objectUuid uuid not null references RbacObject, - op RbacOp not null, + uuid uuid primary key references RbacReference (uuid) on delete cascade, + objectUuid uuid not null references RbacObject, + op RbacOp not null, + opTableName RbacOp, unique (objectUuid, op) ); @@ -397,6 +398,37 @@ select exists( ); $$; +create or replace function createPermissions(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null) + returns uuid[] + language plpgsql as $$ +declare + permissionId uuid; +begin + if (forObjectUuid is null) then + raise exception 'forObjectUuid must not be null'; + end if; + if (forOp = 'INSERT' && forOpTableName is null) then + raise exception 'INSERT permissions needs forOpTableName'; + end if; + if (forOp <> 'INSERT' && forOpTableName is not null) then + raise exception 'forOpTableName must only be specified for ops: [INSERT]'; -- currently no other + end if; + + permissionId = (select uuid from RbacPermission where objectUuid = forObjectUuid and op = forOp and opTableName = forOpTableName); + if (permissionId is null) then + insert + into RbacReference ("type") + values ('RbacPermission') + returning uuid into permissionId; + insert + into RbacPermission (uuid, objectUuid, op, opTableName) + values (permissionId, forObjectUuid, forOp, opTableName); + end if; + return permissionId; +end; +$$; + +-- TODO: deprecated, remove and amend all usages to createPermission create or replace function createPermissions(forObjectUuid uuid, permitOps RbacOp[]) returns uuid[] language plpgsql as $$ @@ -430,7 +462,7 @@ begin end; $$; -create or replace function findPermissionId(forObjectUuid uuid, forOp RbacOp) +create or replace function findPermissionId(forObjectUuid uuid, forOp RbacOp, opTableName text = null ) returns uuid returns null on null input stable -- leakproof @@ -439,6 +471,7 @@ select uuid from RbacPermission p where p.objectUuid = forObjectUuid and p.op = forOp + and p.opTableName = opTableName $$; create or replace function findEffectivePermissionId(forObjectUuid uuid, forOp RbacOp) @@ -552,6 +585,18 @@ select exists( ); $$; +create or replace function hasInsertPermission(objectUuid uuid, forOp RbacOp, tableName text ) + returns BOOL + stable -- leakproof + language plpgsql as $$ +declare + permissionUuid uuid; +begin + permissionUuid = findPermissionId(objectUuid, forOp); + +end; +$$; + create or replace function hasGlobalRoleGranted(userUuid uuid) returns bool stable -- leakproof diff --git a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityTest.java b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityTest.java index 09aeb1f8..ab5d76a5 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityTest.java @@ -16,21 +16,20 @@ class TestCustomerEntityTest { subgraph customer["`**customer**`"] direction TB - style customer fill:#dd4901,stroke:darkblue,stroke-width:8px - + style customer fill:#dd4901,stroke:#274d6e,stroke-width:8px + subgraph customer:roles[ ] - style customer:roles fill: #dd4901 - + style customer:roles fill:#dd4901,stroke:white + role:customer:owner[[customer:owner]] role:customer:admin[[customer:admin]] role:customer:tenant[[customer:tenant]] end - + subgraph customer:permissions[ ] - style customer:permissions fill: #dd4901 - + style customer:permissions fill:#dd4901,stroke:white + perm:customer:*{{customer:*}} - perm:customer:add-package{{customer:add-package}} perm:customer:view{{customer:view}} end end diff --git a/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityTest.java b/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityTest.java new file mode 100644 index 00000000..1bb88ffb --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityTest.java @@ -0,0 +1,71 @@ +package net.hostsharing.hsadminng.test.pac; + +import net.hostsharing.hsadminng.rbac.rbacdef.RbacViewMermaidFlowchart; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class TestPackageEntityTest { + + @Test + void definesRbac() { + final var rbacFlowchart = new RbacViewMermaidFlowchart(TestPackageEntity.rbac()).toString(); + assertThat(rbacFlowchart).isEqualTo(""" + %%{init:{'flowchart':{'htmlLabels':false}}}%% + flowchart TB + + subgraph package["`**package**`"] + direction TB + style package fill:#dd4901,stroke:#274d6e,stroke-width:8px + + subgraph package:roles[ ] + style package:roles fill:#dd4901,stroke:white + + role:package:owner[[package:owner]] + role:package:admin[[package:admin]] + role:package:tenant[[package:tenant]] + end + + subgraph package:permissions[ ] + style package:permissions fill:#dd4901,stroke:white + + perm:package:insert{{package:insert}} + perm:package:*{{package:*}} + perm:package:edit{{package:edit}} + perm:package:view{{package:view}} + end + end + + subgraph customer["`**customer**`"] + direction TB + style customer fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph customer:roles[ ] + style customer:roles fill:#99bcdb,stroke:white + + role:customer:owner[[customer:owner]] + role:customer:admin[[customer:admin]] + role:customer:tenant[[customer:tenant]] + end + end + + %% granting roles to users + user:creator ==> role:package:owner + + %% granting roles to roles + role:global:admin -.-> role:customer:owner + role:customer:owner -.-> role:customer:admin + role:customer:admin -.-> role:customer:tenant + role:customer:admin == /// ==> role:package:owner + role:package:owner ==> role:package:admin + role:package:admin ==> role:package:tenant + role:package:tenant ==> role:customer:tenant + + %% granting permissions to roles + role:customer:admin ==> perm:package:insert + role:package:owner ==> perm:package:* + role:package:owner ==> perm:package:edit + role:package:tenant ==> perm:package:view + """); + } +}