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 5c9c2fdb..2e5734ac 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 @@ -130,7 +130,7 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable { "vatBusiness", "vatReverseCharge", "defaultPrefix" /* TODO: do we want that updatable? */) - .createPermission(custom("new-debitor")).grantedTo("global", ADMIN).pop() + .createPermission(custom("new-debitor")).grantedTo("global", ADMIN) .importRootEntityAliasProxy("debitorRel", HsOfficeRelationshipEntity.class, fetchedBySql(""" @@ -139,9 +139,9 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable { WHERE r.relType = 'ACCOUNTING' AND r.relHolderUuid = ${REF}.debitorRelUuid """), dependsOnColumn("debitorRelUuid")) - .createPermission(ALL).grantedTo("debitorRel", OWNER).pop() - .createPermission(EDIT).grantedTo("debitorRel", ADMIN).pop() - .createPermission(VIEW).grantedTo("debitorRel", TENANT).pop() + .createPermission(ALL).grantedTo("debitorRel", OWNER) + .createPermission(EDIT).grantedTo("debitorRel", ADMIN) + .createPermission(VIEW).grantedTo("debitorRel", TENANT) .importEntityAlias("refundBankAccount", HsOfficeBankAccountEntity.class, fetchedBySql(""" diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerDetailsEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerDetailsEntity.java index 55b30148..468d82ab 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerDetailsEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerDetailsEntity.java @@ -2,7 +2,9 @@ package net.hostsharing.hsadminng.hs.office.partner; import lombok.*; import net.hostsharing.hsadminng.errors.DisplayName; +import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity; import net.hostsharing.hsadminng.persistence.HasUuid; +import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; @@ -10,6 +12,11 @@ import jakarta.persistence.*; import java.time.LocalDate; 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.Role.*; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.fetchedBySql; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.stringify.Stringify.stringify; @Entity @@ -55,6 +62,41 @@ public class HsOfficePartnerDetailsEntity implements HasUuid, Stringifyable { return registrationNumber != null ? registrationNumber : birthName != null ? birthName : birthday != null ? birthday.toString() - : dateOfDeath != null ? dateOfDeath.toString() : ""; + : dateOfDeath != null ? dateOfDeath.toString() + : ""; + } + + + public static RbacView rbac() { + return rbacViewFor("partnerDetails", HsOfficePartnerDetailsEntity.class) + .withIdentityView(RbacView.SQL.query(""" + SELECT partner_iv.idName || '-details' + FROM hs_office_partner_details AS partnerDetails + JOIN hs_office_partner partner ON partner.detailsUuid = partnerDetails.uuid + JOIN hs_office_partner_iv partner_iv ON partner_iv.uuid = partner.uuid + """)) + .withUpdatableColumns( + "registrationOffice", + "registrationNumber", + "birthPlace", + "birthName", + "birthday", + "dateOfDeath") + .createPermission(custom("new-partner-details")).grantedTo("global", ADMIN) + + .importRootEntityAliasProxy("partnerRel", HsOfficeRelationshipEntity.class, + fetchedBySql(""" + SELECT partnerRel.* + FROM hs_office_relationship AS partnerRel + JOIN hs_office_partner AS partner + ON partner.detailsUuid = ${ref}.uuid + WHERE partnerRel.uuid = partner.partnerRoleUuid + """), + dependsOnColumn("partnerRoleUuid")) + + // The grants are defined in HsOfficePartnerEntity.rbac() + // because they have to be changed when its partnerRel changes, + // not when anything in partner details changes. + ; } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntity.java index b4afb080..e8742e16 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntity.java @@ -7,12 +7,15 @@ import net.hostsharing.hsadminng.persistence.HasUuid; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; +import net.hostsharing.hsadminng.rbac.rbacdef.RbacViewMermaidFlowchart; +import net.hostsharing.hsadminng.rbac.rbacdef.RbacViewPostgresGenerator; import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; import org.hibernate.annotations.NotFound; import org.hibernate.annotations.NotFoundAction; import jakarta.persistence.*; +import java.io.IOException; import java.util.Optional; import java.util.UUID; @@ -79,23 +82,36 @@ public class HsOfficePartnerEntity implements Stringifyable, HasUuid { public static RbacView rbac() { return rbacViewFor("partner", HsOfficePartnerEntity.class) .withIdentityView(RbacView.SQL.query(""" - SELECT partner.partnerNumber - || ':' || (SELECT idName FROM hs_office_person_iv p WHERE p.uuid = partner.personuuid) - || '-' || (SELECT idName FROM hs_office_contact_iv c WHERE c.uuid = partner.contactuuid) - FROM hs_office_partner AD partner - $idName$) + SELECT partner.partnerNumber + || ':' || (SELECT idName FROM hs_office_person_iv p WHERE p.uuid = partner.personuuid) + || '-' || (SELECT idName FROM hs_office_contact_iv c WHERE c.uuid = partner.contactuuid) + FROM hs_office_partner AD partner """)) .withUpdatableColumns( "partnerRoleUuid", "personUuid", "contactUuid") - .createPermission(custom("new-partner")).grantedTo("global", ADMIN).pop() + .createPermission(custom("new-partner")).grantedTo("global", ADMIN) .importRootEntityAliasProxy("partnerRel", HsOfficeRelationshipEntity.class, fetchedBySql("SELECT * FROM hs_office_relationship AS r WHERE r.uuid = ${ref}.partnerRoleUuid"), dependsOnColumn("partnerRelUuid")) - .createPermission(ALL).grantedTo("partnerRel", ADMIN).pop() - .createPermission(EDIT).grantedTo("partnerRel", AGENT).pop() - .createPermission(VIEW).grantedTo("partnerRel", TENANT).pop(); + .createPermission(ALL).grantedTo("partnerRel", ADMIN) + .createPermission(EDIT).grantedTo("partnerRel", AGENT) + .createPermission(VIEW).grantedTo("partnerRel", TENANT) + + .importSubEntityAlias("partnerDetails", HsOfficePartnerDetailsEntity.class, + fetchedBySql("SELECT * FROM hs_office_partner_details AS d WHERE d.uuid = ${ref}.detailsUuid"), + dependsOnColumn("detailsUuid")) + .createPermission("partnerDetails", ALL).grantedTo("partnerRel", ADMIN) + .createPermission("partnerDetails", EDIT).grantedTo("partnerRel", AGENT) + .createPermission("partnerDetails", VIEW).grantedTo("partnerRel", AGENT); } + + public static void main(String[] args) throws IOException { + final RbacView rbac = HsOfficePartnerEntity.rbac(); + new RbacViewMermaidFlowchart(rbac).generateToMarkdownFile(); + new RbacViewPostgresGenerator(rbac).generateToChangeLog("233-hs-office-partner-rbac.sql"); + } + } 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 5317b882..f8ef6388 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -4,7 +4,6 @@ import java.util.function.Consumer; import lombok.EqualsAndHashCode; import lombok.Getter; -import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity; import net.hostsharing.hsadminng.persistence.HasUuid; import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; @@ -89,6 +88,10 @@ public class RbacView { return createPermission(rootEntityAlias, permission); } + public RbacPermissionDefinition createPermission(final String entityAliasName, final Permission permission) { + return createPermission(findEntityAlias(entityAliasName), permission); + } + private RbacPermissionDefinition createPermission(final EntityAlias entityAlias, final Permission permission) { final RbacPermissionDefinition permDef = new RbacPermissionDefinition(entityAlias, permission, true); permDefs.add(permDef); @@ -107,21 +110,31 @@ public class RbacView { if ( rootEntityAliasProxy != null ) { throw new IllegalStateException("there is already an entityAliasProxy: " + rootEntityAliasProxy); } - rootEntityAliasProxy = importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum); + rootEntityAliasProxy = importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, false); + return this; + } + + public RbacView importSubEntityAlias( + final String aliasName, final Class entityClass, + final SQL fetchSql, final Column dependsOnColum) { + importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, true); return this; } public RbacView importEntityAlias( - final String aliasName, final Class entityClass, final SQL fetchSql, final Column dependsOnColum) { - importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum); + final String aliasName, final Class entityClass, + final SQL fetchSql, final Column dependsOnColum) { + importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, false); return this; } - private EntityAlias importEntityAliasImpl(final String aliasName, final Class entityClass, final SQL fetchSql, final Column dependsOnColum) { - final var entityAlias = new EntityAlias(aliasName, entityClass, fetchSql, dependsOnColum); + private EntityAlias importEntityAliasImpl( + final String aliasName, final Class entityClass, + final SQL fetchSql, final Column dependsOnColum, boolean asSubEntity) { + final var entityAlias = new EntityAlias(aliasName, entityClass, fetchSql, dependsOnColum, asSubEntity); entityAliases.put(aliasName, entityAlias); try { - importAsAlias(aliasName, rbacDefinition(entityClass)); + importAsAlias(aliasName, rbacDefinition(entityClass), asSubEntity); } catch ( final ReflectiveOperationException exc) { throw new RuntimeException("cannot import entity: " + entityClass, exc); } @@ -133,11 +146,13 @@ public class RbacView { return (RbacView) entityClass.getMethod("rbac").invoke(null); } - private RbacView importAsAlias(final String aliasName, final RbacView importedRbacView) { - final var mapper = new AliasNameMapper(importedRbacView, aliasName); + private RbacView importAsAlias(final String aliasName, final RbacView importedRbacView, final boolean asSubEntity) { + final var mapper = new AliasNameMapper(importedRbacView, aliasName, + asSubEntity ? entityAliases.keySet() : null); importedRbacView.getEntityAliases().values().stream() .filter(entityAlias -> !importedRbacView.isRootEntityAlias(entityAlias)) .filter(entityAlias -> !entityAlias.isGlobal()) + .filter(entityAlias -> !asSubEntity || !entityAliases.containsKey(entityAlias.aliasName)) .forEach(entityAlias -> { final String mappedAliasName = mapper.map(entityAlias.aliasName); entityAliases.put(mappedAliasName, new EntityAlias(mappedAliasName, entityAlias.entityClass)); @@ -302,26 +317,15 @@ public class RbacView { final Permission permission; final boolean toCreate; - public RbacPermissionDefinition(final EntityAlias entityAlias, final Permission permission, final boolean toCreate) { + private RbacPermissionDefinition(final EntityAlias entityAlias, final Permission permission, final boolean toCreate) { this.entityAlias = entityAlias; this.permission = permission; this.toCreate = toCreate; } - public RbacView pop() { - return RbacView.this; - } - - public RbacPermissionDefinition withIncomingSuperRole( - final Class hsOfficeRelationshipEntityClass, - final Role owner) { - - return this; - } - - public RbacPermissionDefinition grantedTo(final String entityAlias, final Role role) { + public RbacView grantedTo(final String entityAlias, final Role role) { findOrCreateGrantDef(this, findRbacRole(entityAlias, role) ).toCreate(); - return this; + return RbacView.this; } @Override @@ -441,14 +445,14 @@ public class RbacView { .orElseGet(() -> new RbacGrantDefinition(subRoleDefinition, superRoleDefinition)); } - record EntityAlias(String aliasName, Class entityClass, SQL fetchSql, Column dependsOnColum) { + record EntityAlias(String aliasName, Class entityClass, SQL fetchSql, Column dependsOnColum, boolean isSubEntity) { public EntityAlias(final String aliasName) { - this(aliasName, null, null, null); + this(aliasName, null, null, null, false); } public EntityAlias(final String aliasName, final Class entityClass) { - this(aliasName, entityClass, null, null); + this(aliasName, entityClass, null, null, false); } boolean isGlobal() { @@ -459,8 +463,6 @@ public class RbacView { return entityClass == null; } - - private String withoutEntitySuffix(final String simpleEntityName) { return simpleEntityName.substring(0, simpleEntityName.length()-"Entity".length()); } @@ -507,14 +509,27 @@ public class RbacView { public static class SQL { + /** + * DSL methid to specify an SQL SELECT expression which fetches the related entity, + * using the reference `${ref}` of the root entity. + * `${ref}` is going to be replaced by either `NEW` or `OLD` of the trigger function. + * `into ...` will be added with a variable name prefixed with either `new` or `old`. + * + * @param sql an SQL SELECT expression (not ending with ';) + * @return the wrapped SQL expression + */ public static SQL fetchedBySql(final String sql) { + validateExpression(sql); return new SQL(sql); } + /** Generic DSL method to specify an SQL SELECT expression. + * + * @param sql an SQL SELECT expression (not ending with ';) + * @return the wrapped SQL expression + */ public static SQL query(final String sql) { - if (sql.matches(";[ \t]*$")) { - throw new IllegalArgumentException("SQL expression must not end with ';': " + sql); - } + validateExpression(sql); return new SQL(sql); } @@ -523,6 +538,12 @@ public class RbacView { private SQL(final String sql) { this.sql = sql; } + + private static void validateExpression(final String sql) { + if (sql.matches(";[ \t]*$")) { + throw new IllegalArgumentException("SQL expression must not end with ';': " + sql); + } + } } public static class Column { @@ -543,18 +564,21 @@ public class RbacView { private final RbacView importedRbacView; private final String outerAliasName; - AliasNameMapper(final RbacView importedRbacView, final String outerAliasName) { + private final Set outerAliasNames; + + AliasNameMapper(final RbacView importedRbacView, final String outerAliasName, final Set outerAliasNames) { this.importedRbacView = importedRbacView; this.outerAliasName = outerAliasName; + this.outerAliasNames = (outerAliasNames == null) ? Collections.emptySet() : outerAliasNames; } String map(final String originalAliasName) { + if (outerAliasNames.contains(originalAliasName) || originalAliasName.equals("global")) { + return originalAliasName; + } if (originalAliasName.equals(importedRbacView.rootEntityAlias.aliasName) ) { return outerAliasName; } - if (originalAliasName.equals("global") ) { - return originalAliasName; - } return outerAliasName + "." + originalAliasName; } } 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 113bea26..fcc8a83f 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java @@ -2,6 +2,7 @@ package net.hostsharing.hsadminng.rbac.rbacdef; import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; +import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerDetailsEntity; import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity; import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity; import org.apache.commons.lang3.StringUtils; @@ -16,6 +17,7 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinitio public class RbacViewMermaidFlowchart { public static final String HOSTSHARING_ORANGE = "#dd4901"; + public static final String HOSTSHARING_ORANGE_LIGHT = "#feb28c"; public static final String HOSTSHARING_LIGHTBLUE = "#99bcdb"; private final RbacView rbacDef; private final StringWriter flowchart = new StringWriter(); @@ -37,7 +39,9 @@ public class RbacViewMermaidFlowchart { } private void renderEntitySubgraph(final RbacView.EntityAlias entity) { - final var color = rbacDef.isRootEntityAlias(entity) ? HOSTSHARING_ORANGE : HOSTSHARING_LIGHTBLUE; + final var color = rbacDef.isRootEntityAlias(entity) ? HOSTSHARING_ORANGE + : entity.isSubEntity() ? HOSTSHARING_ORANGE_LIGHT + : HOSTSHARING_LIGHTBLUE; flowchart.writeLn(""" subgraph %{aliasName}["`**%{aliasName}**`"] direction TB @@ -142,7 +146,7 @@ public class RbacViewMermaidFlowchart { return flowchart.toString(); } - void generateToMarkdownFile() throws IOException { + public void generateToMarkdownFile() throws IOException { final Path path = Paths.get("doc", rbacDef.getRootEntityAlias().simpleName() + ".md"); Files.writeString( path, @@ -164,6 +168,7 @@ public class RbacViewMermaidFlowchart { new RbacViewMermaidFlowchart(HsOfficeBankAccountEntity.rbac()).generateToMarkdownFile(); new RbacViewMermaidFlowchart(HsOfficeRelationshipEntity.rbac()).generateToMarkdownFile(); new RbacViewMermaidFlowchart(HsOfficePartnerEntity.rbac()).generateToMarkdownFile(); + new RbacViewMermaidFlowchart(HsOfficePartnerDetailsEntity.rbac()).generateToMarkdownFile(); new RbacViewMermaidFlowchart(HsOfficeDebitorEntity.rbac()).generateToMarkdownFile(); } } 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 f27e238f..4298af4b 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java @@ -24,8 +24,9 @@ public class RbacViewPostgresGenerator { liqibaseTagPrefix = rbacDef.getRootEntityAlias().entityClass().getSimpleName(); plPgSql.writeLn(""" --liquibase formatted sql - -- generated at: %{timestamp} + -- This code generated was by ${generator} at %{timestamp}. """ + .replace("${generator}", getClass().getSimpleName()) .replace("%{timestamp}", LocalDateTime.now().toString())); new RolesGrantsAndPermissionsGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql); @@ -46,6 +47,16 @@ public class RbacViewPostgresGenerator { System.out.println(outputPath.toAbsolutePath()); } + public void generateToChangeLog(final String fileName) throws IOException { + final Path outputPath = Path.of("src/main/resources/db/changelog", fileName); + Files.writeString( + outputPath, + toString(), + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING); + System.out.println(outputPath.toAbsolutePath()); + } + public static void main(String[] args) throws IOException { generatePostgres(HsOfficeRelationshipEntity.rbac()); generatePostgres(HsOfficePartnerEntity.rbac()); diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java index cfe64710..0ae74bd7 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java @@ -98,6 +98,7 @@ class RolesGrantsAndPermissionsGenerator { createRolesWithGrantsSql(plPgSql, REFERRER); plPgSql.writeLn(); + // TODO: we need to group and sort the grants, similar to the Flowchart generator rbacGrants.forEach(g -> plPgSql.writeLn(generateGrant(g))); plPgSql.writeLn("return NEW;"); @@ -123,10 +124,14 @@ class RolesGrantsAndPermissionsGenerator { return "createPermissions(${entityRef}.uuid, array ['${perm}']" .replace("${entityRef}", rbacDef.isRootEntityAlias(permDef.entityAlias) ? ref.name() - : "not implemented yet" ) // TODO + : refVarName(ref, permDef.entityAlias)) .replace("${perm}", permDef.permission.permission()); } + private String refVarName(final PostgresTriggerReference ref, final RbacView.EntityAlias entityAlias) { + return ref.name().toLowerCase() + capitalize(entityAlias.aliasName()); + } + private String getRawTableName(final Class entityClass) { return withoutRvSuffix(entityClass.getAnnotation(Table.class).name()); }