From dff9803dc34635fa89ebb00982394faa955de21c Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 28 Feb 2024 13:58:55 +0100 Subject: [PATCH] add RBAC for HsOfficeSepaMandateEntity, improved DSL and Postgres-generator --- .../HsOfficeBankAccountEntity.java | 2 +- .../office/contact/HsOfficeContactEntity.java | 3 +- .../office/debitor/HsOfficeDebitorEntity.java | 12 +- .../partner/HsOfficePartnerDetailsEntity.java | 3 +- .../office/partner/HsOfficePartnerEntity.java | 16 +-- .../office/person/HsOfficePersonEntity.java | 4 +- .../HsOfficeRelationshipEntity.java | 18 +-- .../HsOfficeSepaMandateEntity.java | 39 ++++++ .../hsadminng/rbac/rbacdef/RbacView.java | 85 ++++++++++++- .../rbacdef/RbacViewMermaidFlowchart.java | 28 ++--- .../rbacdef/RbacViewPostgresGenerator.java | 18 +-- .../RolesGrantsAndPermissionsGenerator.java | 117 ++++++++++-------- .../test/cust/TestCustomerEntity.java | 3 +- 13 files changed, 227 insertions(+), 121 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountEntity.java index 7f5a0185..5655fead 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountEntity.java @@ -58,7 +58,7 @@ public class HsOfficeBankAccountEntity implements HasUuid, Stringifyable { public static RbacView rbac() { return rbacViewFor("bankAccount", HsOfficeBankAccountEntity.class) - .withIdentityView(SQL.query("target.iban || ':' || target.holder")) + .withIdentityView(SQL.projection("iban || ':' || holder")) .withUpdatableColumns("holder", "iban", "bic") .createRole(OWNER, (with) -> { with.owningUser(CREATOR); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntity.java index b9522d0d..64baa4bc 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntity.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.hibernate.annotations.GenericGenerator; @@ -61,7 +62,7 @@ public class HsOfficeContactEntity implements Stringifyable, HasUuid { public static RbacView rbac() { return rbacViewFor("contact", HsOfficeContactEntity.class) - .withIdentityView(RbacView.SQL.query("target.label")) + .withIdentityView(SQL.projection("label")) .withUpdatableColumns("label", "postalAddress", "emailAddresses", "phoneNumbers") .createRole(OWNER, (with) -> { with.owningUser(CREATOR); 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 2e5734ac..afe905b3 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 @@ -144,22 +144,22 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable { .createPermission(VIEW).grantedTo("debitorRel", TENANT) .importEntityAlias("refundBankAccount", HsOfficeBankAccountEntity.class, - fetchedBySql(""" + dependsOnColumn("bankAccountUuid"), fetchedBySql(""" SELECT * FROM hs_office_relationship AS r WHERE r.relType = 'ACCOUNTING' AND r.relHolderUuid = ${REF}.debitorRelUuid - """), - dependsOnColumn("bankAccountUuid")) + """) + ) .toRole("refundBankAccount", ADMIN).grantRole("debitorRel", AGENT) .toRole("debitorRel", AGENT).grantRole("refundBankAccount", REFERRER) .importEntityAlias("partnerRel", HsOfficeRelationshipEntity.class, - fetchedBySql(""" + dependsOnColumn("debitorRelUuid"), fetchedBySql(""" SELECT * FROM hs_office_relationship AS partnerRel WHERE ${debitorRel}.relAnchorUuid = partnerRel.relHolderUuid - """), - dependsOnColumn("debitorRelUuid")) + """) + ) .toRole("partnerRel", ADMIN).grantRole("debitorRel", ADMIN) .toRole("partnerRel", AGENT).grantRole("debitorRel", AGENT) .toRole("debitorRel", AGENT).grantRole("partnerRel", TENANT) 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 468d82ab..736e95cc 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 @@ -5,6 +5,7 @@ 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.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; @@ -69,7 +70,7 @@ public class HsOfficePartnerDetailsEntity implements HasUuid, Stringifyable { public static RbacView rbac() { return rbacViewFor("partnerDetails", HsOfficePartnerDetailsEntity.class) - .withIdentityView(RbacView.SQL.query(""" + .withIdentityView(SQL.query(""" SELECT partner_iv.idName || '-details' FROM hs_office_partner_details AS partnerDetails JOIN hs_office_partner partner ON partner.detailsUuid = partnerDetails.uuid 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 e8742e16..c2a11c0e 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,8 +7,7 @@ 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.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; import org.hibernate.annotations.NotFound; @@ -81,11 +80,11 @@ public class HsOfficePartnerEntity implements Stringifyable, HasUuid { public static RbacView rbac() { return rbacViewFor("partner", HsOfficePartnerEntity.class) - .withIdentityView(RbacView.SQL.query(""" + .withIdentityView(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 + || ':' || (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 AS partner """)) .withUpdatableColumns( "partnerRoleUuid", @@ -109,9 +108,6 @@ public class HsOfficePartnerEntity implements Stringifyable, HasUuid { } 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"); + HsOfficePartnerEntity.rbac().generateWithBaseFileName("233-hs-office-partner-rbac"); } - } 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 83c9c94c..87232918 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 @@ -16,7 +16,7 @@ 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.query; +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; @@ -66,7 +66,7 @@ public class HsOfficePersonEntity implements HasUuid, Stringifyable { public static RbacView rbac() { return rbacViewFor("person", HsOfficePersonEntity.class) - .withIdentityView(query("concat(target.tradeName, target.familyName, target.givenName)")) + .withIdentityView(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 eb433725..44f12cf5 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 @@ -79,21 +79,21 @@ public class HsOfficeRelationshipEntity implements HasUuid, Stringifyable { public static RbacView rbac() { return rbacViewFor("relationship", HsOfficeRelationshipEntity.class) - .withIdentityView(SQL.query(""" - (select idName from hs_office_person_iv p where p.uuid = target.relAnchorUuid) + .withIdentityView(SQL.projection(""" + (select idName from hs_office_person_iv p where p.uuid = relAnchorUuid) || '-with-' || target.relType || '-' - || (select idName from hs_office_person_iv p where p.uuid = target.relHolderUuid) + || (select idName from hs_office_person_iv p where p.uuid = relHolderUuid) """)) .withUpdatableColumns("contactUuid") .importEntityAlias("anchorPerson", HsOfficePersonEntity.class, - fetchedBySql("select * from hs_office_person as p where p.uuid = ${REF}.relAnchorUuid"), - dependsOnColumn("relAnchorUuid")) + dependsOnColumn("relAnchorUuid"), fetchedBySql("select * from hs_office_person as p where p.uuid = ${REF}.relAnchorUuid") + ) .importEntityAlias("holderPerson", HsOfficePersonEntity.class, - fetchedBySql("select * from hs_office_person as p where p.uuid = ${REF}.relHolderUuid"), - dependsOnColumn("relHolderUuid")) + dependsOnColumn("relHolderUuid"), fetchedBySql("select * from hs_office_person as p where p.uuid = ${REF}.relHolderUuid") + ) .importEntityAlias("contact", HsOfficeContactEntity.class, - fetchedBySql("select * from hs_office_contact as c where c.uuid = ${REF}.contactUuid"), - dependsOnColumn("contactUuid")) + dependsOnColumn("contactUuid"), fetchedBySql("select * from hs_office_contact as c where c.uuid = ${REF}.contactUuid") + ) .createRole(OWNER, (with) -> { with.owningUser(CREATOR); with.incomingSuperRole(GLOBAL, ADMIN); 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 baed26aa..aa0f925e 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 @@ -6,16 +6,26 @@ import lombok.*; import net.hostsharing.hsadminng.errors.DisplayName; import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; +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; import org.hibernate.annotations.Type; import jakarta.persistence.*; +import java.io.IOException; import java.time.LocalDate; import java.util.UUID; 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.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.*; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.stringify.Stringify.stringify; @Entity @@ -84,4 +94,33 @@ public class HsOfficeSepaMandateEntity implements Stringifyable, HasUuid { return reference; } + public static RbacView rbac() { + return rbacViewFor("sepaMandate", HsOfficeSepaMandateEntity.class) + .withIdentityView(projection("concat(tradeName, familyName, givenName)")) + .withUpdatableColumns("reference", "agreement", "validity") + + .importEntityAlias("debitorRel", HsOfficeRelationshipEntity.class, dependsOnColumn("debitorRelUuid")) + .importEntityAlias("bankAccount", HsOfficeBankAccountEntity.class, dependsOnColumn("bankAccountUuid")) + + .createRole(OWNER, (with) -> { + with.owningUser(CREATOR); + with.incomingSuperRole(GLOBAL, ADMIN); + with.outgoingSubRole("bankAccount", REFERRER); + with.permission(ALL); + }) + .createSubRole(ADMIN, (with) -> { + with.permission(EDIT); + }) + .createSubRole(AGENT, (with) -> { + with.outgoingSubRole("debitorRel", AGENT); + }) + .createSubRole(REFERRER, (with) -> { + with.incomingSuperRole("debitorRel", AGENT); + with.permission(VIEW); + }); + } + + public static void main(String[] args) throws IOException { + HsOfficeSepaMandateEntity.rbac().generateWithBaseFileName("253-hs-office-sepamandate-rbac"); + } } 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 f8ef6388..42eb80da 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -1,5 +1,6 @@ package net.hostsharing.hsadminng.rbac.rbacdef; +import java.nio.file.Path; import java.util.function.Consumer; import lombok.EqualsAndHashCode; @@ -7,17 +8,21 @@ import lombok.Getter; import net.hostsharing.hsadminng.persistence.HasUuid; import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; +import jakarta.persistence.Table; import jakarta.validation.constraints.NotNull; import java.lang.reflect.InvocationTargetException; import java.util.*; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.autoFetched; import static org.apache.commons.lang3.StringUtils.uncapitalize; @Getter public class RbacView { public static final String GLOBAL = "global"; + public static final String OUTPUT_BASEDIR = "src/main/resources/db/changelog"; + private final EntityAlias rootEntityAlias; @@ -123,11 +128,18 @@ public class RbacView { public RbacView importEntityAlias( final String aliasName, final Class entityClass, - final SQL fetchSql, final Column dependsOnColum) { + final Column dependsOnColum, final SQL fetchSql) { importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, false); return this; } + public RbacView importEntityAlias( + final String aliasName, final Class entityClass, + final Column dependsOnColum) { + importEntityAliasImpl(aliasName, entityClass, autoFetched(), dependsOnColum, false); + return this; + } + private EntityAlias importEntityAliasImpl( final String aliasName, final Class entityClass, final SQL fetchSql, final Column dependsOnColum, boolean asSubEntity) { @@ -200,6 +212,11 @@ public class RbacView { return entityAlias == rootEntityAliasProxy; } + 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")); + } + public class RbacGrantBuilder { private final RbacRoleDefinition superRoleDef; @@ -463,6 +480,18 @@ public class RbacView { return entityClass == null; } + @Override + public SQL fetchSql() { + if ( fetchSql == null ) { + return null; + } + return switch (fetchSql.part) { + case SQL_QUERY -> fetchSql; + case AUTO_FETCH -> SQL.query("SELECT * FROM " + getRawTableName(entityClass) + " WHERE uuid = ${ref}." + dependsOnColum.column); + default -> throw new IllegalStateException("unexpected SQL definition: " + fetchSql); + }; + } + private String withoutEntitySuffix(final String simpleEntityName) { return simpleEntityName.substring(0, simpleEntityName.length()-"Entity".length()); } @@ -474,6 +503,13 @@ public class RbacView { } } + public static String getRawTableName(final Class entityClass) { + return withoutRvSuffix(entityClass.getAnnotation(Table.class).name()); + } + public static String withoutRvSuffix(final String tableName) { + 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"); @@ -510,7 +546,7 @@ public class RbacView { public static class SQL { /** - * DSL methid to specify an SQL SELECT expression which fetches the related entity, + * DSL method 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`. @@ -520,7 +556,18 @@ public class RbacView { */ public static SQL fetchedBySql(final String sql) { validateExpression(sql); - return new SQL(sql); + return new SQL(sql, Part.SQL_QUERY); + } + + /** + * DSL method to specify that a related entity is to be fetched by a simple SELECT statement + * using the raw table from the @Table statement of the entity to fetch + * and the dependent column of the root entity. + * + * @return the wrapped SQL definition object + */ + public static SQL autoFetched() { + return new SQL(null, Part.AUTO_FETCH); } /** Generic DSL method to specify an SQL SELECT expression. @@ -530,13 +577,39 @@ public class RbacView { */ public static SQL query(final String sql) { validateExpression(sql); - return new SQL(sql); + return new SQL(sql, Part.SQL_QUERY); } - public final String sql; + /** 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 + */ + public static SQL projection(final String projection) { + validateProjection(projection); + return new SQL(projection, Part.SQL_PROJECTION); + } - private SQL(final String sql) { + enum Part { + SQL_QUERY, + AUTO_FETCH, SQL_PROJECTION + } + + final String sql; + final Part part; + + private SQL(final String sql, final Part part) { this.sql = sql; + this.part = part; + } + + private static void validateProjection(final String projection) { + if (projection.toUpperCase().matches("[ \t]*$SELECT[ \t]")) { + throw new IllegalArgumentException("SQL projection must not start with 'SELECT': " + projection); + } + if (projection.matches(";[ \t]*$")) { + throw new IllegalArgumentException("SQL projection must not end with ';': " + projection); + } } private static void validateExpression(final String sql) { 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 9acf5df4..1495fab1 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java @@ -1,13 +1,8 @@ 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 lombok.SneakyThrows; import org.apache.commons.lang3.StringUtils; -import java.io.IOException; import java.nio.file.*; import java.time.LocalDateTime; @@ -46,10 +41,11 @@ public class RbacViewMermaidFlowchart { flowchart.writeLn(""" subgraph %{aliasName}["`**%{aliasName}**`"] direction TB - style %{aliasName} fill:%{color},stroke:darkblue,stroke-width:8px + style %{aliasName} fill:%{fillColor},stroke:%{strokeColor},stroke-width:8px """ .replace("%{aliasName}", entity.aliasName()) - .replace("%{color}", color )); + .replace("%{fillColor}", color ) + .replace("%{strokeColor}", HOSTSHARING_DARK_BLUE )); flowchart.indented( () -> { rbacDef.getEntityAliases().values().stream() @@ -83,9 +79,9 @@ public class RbacViewMermaidFlowchart { flowchart.ensureEmptyLine(); flowchart.writeLn("subgraph " + name + "[ ]\n"); flowchart.indented(() -> { - flowchart.writeLn("style %{aliasName} fill: %{color}" + flowchart.writeLn("style %{aliasName} fill:%{fillColor},stroke:white" .replace("%{aliasName}", name) - .replace("%{color}", color)); + .replace("%{fillColor}", color)); flowchart.writeLn(); flowchart.writeLn(content); }); @@ -147,8 +143,8 @@ public class RbacViewMermaidFlowchart { return flowchart.toString(); } - public void generateToMarkdownFile() throws IOException { - final Path path = Paths.get("doc", rbacDef.getRootEntityAlias().simpleName() + ".md"); + @SneakyThrows + public void generateToMarkdownFile(final Path path) { Files.writeString( path, """ @@ -164,12 +160,4 @@ public class RbacViewMermaidFlowchart { StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); System.out.println("Markdown-File: " + path.toAbsolutePath()); } - - public static void main(String[] args) throws IOException { - 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 4298af4b..36e963c0 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java @@ -1,10 +1,7 @@ package net.hostsharing.hsadminng.rbac.rbacdef; -import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; -import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity; -import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity; +import lombok.SneakyThrows; -import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -37,7 +34,8 @@ public class RbacViewPostgresGenerator { return plPgSql.toString(); } - private static void generatePostgres(final RbacView rbac) throws IOException { + @SneakyThrows + private static void generatePostgres(final RbacView rbac) { final Path outputPath = Paths.get("doc", rbac.getRootEntityAlias().simpleName() + ".sql"); Files.writeString( outputPath, @@ -47,8 +45,8 @@ 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); + @SneakyThrows + public void generateToChangeLog(final Path outputPath) { Files.writeString( outputPath, toString(), @@ -56,10 +54,4 @@ public class RbacViewPostgresGenerator { StandardOpenOption.TRUNCATE_EXISTING); System.out.println(outputPath.toAbsolutePath()); } - - public static void main(String[] args) throws IOException { - generatePostgres(HsOfficeRelationshipEntity.rbac()); - generatePostgres(HsOfficePartnerEntity.rbac()); - generatePostgres(HsOfficeDebitorEntity.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 6801d5d4..7b8aae8d 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java @@ -2,14 +2,15 @@ package net.hostsharing.hsadminng.rbac.rbacdef; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacPermissionDefinition; -import jakarta.persistence.Table; import java.util.HashSet; +import java.util.List; import java.util.Set; import static java.util.stream.Collectors.*; import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.NEW; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.*; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.getRawTableName; import static org.apache.commons.lang3.StringUtils.capitalize; import static org.apache.commons.lang3.StringUtils.uncapitalize; @@ -141,10 +142,6 @@ class RolesGrantsAndPermissionsGenerator { return ref.name().toLowerCase() + capitalize(entityAlias.aliasName()); } - private String getRawTableName(final Class entityClass) { - return withoutRvSuffix(entityClass.getAnnotation(Table.class).name()); - } - private String roleRef(final PostgresTriggerReference rootRefVar, final RbacView.RbacRoleDefinition roleDef) { if ( roleDef == null ) { System.out.println("null"); @@ -179,51 +176,13 @@ class RolesGrantsAndPermissionsGenerator { .replace("${simpleVarName)", simpleEntityVarName) .replace("${roleSuffix}", capitalize(role.roleName()))); - final var permissionGrantsForRole = findPermissionsGrantsForRole(rbacDef.getRootEntityAlias(), role); - if (!permissionGrantsForRole.isEmpty()) { - final var permissionsForRoleInPlPgSql = permissionGrantsForRole.stream() - .map(RbacView.RbacGrantDefinition::getPermDef) - .map(RbacPermissionDefinition::getPermission) - .map(RbacView.Permission::permission) - .map(p -> "'" + p + "'") - .collect(joining(", ")); - plPgSql.indented( () -> - plPgSql.writeLn("permissions => array[" + permissionsForRoleInPlPgSql + "],\n")); - rbacGrants.removeAll(permissionGrantsForRole); - } + generatePermissionsForRole(plPgSql, role); - final var grantsToUsers = findGrantsToUserForRole(rbacDef.getRootEntityAlias(), role); - if (!grantsToUsers.isEmpty()) { - final var grantsToUsersPlPgSql = grantsToUsers.stream() - .map(RbacView.RbacGrantDefinition::getUserDef) - .map(this::toPlPgSqlReference) - .collect(joining(", ")); - plPgSql.indented(() -> - plPgSql.writeLn("userUuids => array[" + grantsToUsersPlPgSql + "],\n")); - rbacGrants.removeAll(grantsToUsers); - } + generateUserGrantsForRole(plPgSql, role); - final var incomingGrants = findIncomingSuperRolesForRole(rbacDef.getRootEntityAlias(), role); - if (!incomingGrants.isEmpty()) { - final var incomingGrantsInPlPgSql = incomingGrants.stream() - .map(RbacView.RbacGrantDefinition::getSuperRoleDef) - .map(r -> toPlPgSqlReference(NEW, r)) - .collect(joining(", ")); - plPgSql.indented(() -> - plPgSql.writeLn("incomingSuperRoles => array[" + incomingGrantsInPlPgSql + "],\n")); - rbacGrants.removeAll(incomingGrants); - } + generateIncomingSuperRolesForRole(plPgSql, role); - final var outgoingGrants = findOutgoingSuperRolesForRole(rbacDef.getRootEntityAlias(), role); - if (!outgoingGrants.isEmpty()) { - final var outgoingGrantsInPlPgSql = outgoingGrants.stream() - .map(RbacView.RbacGrantDefinition::getSuperRoleDef) - .map(r -> toPlPgSqlReference(NEW, r)) - .collect(joining(", ")); - plPgSql.indented(() -> - plPgSql.writeLn("outgoingSubRoles => array[" + outgoingGrantsInPlPgSql + "],\n")); - rbacGrants.removeAll(outgoingGrants); - } + generateOutgoingSubRolesForRole(plPgSql, role); plPgSql.chopTail(",\n"); plPgSql.writeLn(); @@ -232,6 +191,66 @@ class RolesGrantsAndPermissionsGenerator { plPgSql.writeLn(");"); } + private void generateUserGrantsForRole(final StringWriter plPgSql, final RbacView.Role role) { + final var grantsToUsers = findGrantsToUserForRole(rbacDef.getRootEntityAlias(), role); + if (!grantsToUsers.isEmpty()) { + final var arrayElements = grantsToUsers.stream() + .map(RbacView.RbacGrantDefinition::getUserDef) + .map(this::toPlPgSqlReference) + .toList(); + plPgSql.indented(() -> + plPgSql.writeLn("userUuids => array[" + joinArrayElements(arrayElements, 2) + "],\n")); + rbacGrants.removeAll(grantsToUsers); + } + } + + private void generatePermissionsForRole(final StringWriter plPgSql, final RbacView.Role role) { + final var permissionGrantsForRole = findPermissionsGrantsForRole(rbacDef.getRootEntityAlias(), role); + if (!permissionGrantsForRole.isEmpty()) { + final var arrayElements = permissionGrantsForRole.stream() + .map(RbacView.RbacGrantDefinition::getPermDef) + .map(RbacPermissionDefinition::getPermission) + .map(RbacView.Permission::permission) + .map(p -> "'" + p + "'") + .toList(); + plPgSql.indented( () -> + plPgSql.writeLn("permissions => array[" + joinArrayElements(arrayElements, 3) + "],\n")); + rbacGrants.removeAll(permissionGrantsForRole); + } + } + + private void generateIncomingSuperRolesForRole(final StringWriter plPgSql, final RbacView.Role role) { + final var incomingGrants = findIncomingSuperRolesForRole(rbacDef.getRootEntityAlias(), role); + if (!incomingGrants.isEmpty()) { + final var arraElements = incomingGrants.stream() + .map(RbacView.RbacGrantDefinition::getSuperRoleDef) + .map(r -> toPlPgSqlReference(NEW, r)) + .toList(); + plPgSql.indented(() -> + plPgSql.writeLn("incomingSuperRoles => array[" + joinArrayElements(arraElements, 1) + "],\n")); + rbacGrants.removeAll(incomingGrants); + } + } + + private void generateOutgoingSubRolesForRole(final StringWriter plPgSql, final RbacView.Role role) { + final var outgoingGrants = findOutgoingSuperRolesForRole(rbacDef.getRootEntityAlias(), role); + if (!outgoingGrants.isEmpty()) { + final var arrayElements = outgoingGrants.stream() + .map(RbacView.RbacGrantDefinition::getSubRoleDef) + .map(r -> toPlPgSqlReference(NEW, r)) + .toList(); + plPgSql.indented(() -> + plPgSql.writeLn("outgoingSubRoles => array[" + joinArrayElements(arrayElements, 1) + "],\n")); + rbacGrants.removeAll(outgoingGrants); + } + } + + private String joinArrayElements(final List arrayElements, final int singleLineLimit) { + return arrayElements.size() <= singleLineLimit + ? String.join(", ", arrayElements) + : arrayElements.stream().collect(joining(",\n\t", "\n\t", "")); + } + private Set findPermissionsGrantsForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { final var roleDef = rbacDef.findRbacRole(entityAlias, role); return rbacGrants.stream() @@ -284,10 +303,6 @@ class RolesGrantsAndPermissionsGenerator { plPgSql.writeLn(); } - private String withoutRvSuffix(final String tableName) { - return tableName.substring(0, tableName.length()-"_rv".length()); - } - private String toPlPgSqlReference(final RbacView.RbacUserReference userRef) { return switch (userRef.role) { case CREATOR -> "currentUserUuid()"; 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 80bdf940..b7baf3b8 100644 --- a/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java @@ -5,6 +5,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; 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.*; @@ -39,7 +40,7 @@ public class TestCustomerEntity implements RbacObject { public static RbacView rbac() { return rbacViewFor("customer", TestCustomerEntity.class) - .withIdentityView(RbacView.SQL.query("target.prefix")) + .withIdentityView(SQL.projection("prefix")) .withUpdatableColumns("reference", "prefix", "adminUserName") .createRole(OWNER, (with) -> { with.owningUser(CREATOR);