From 17282c857f533907df8cdd600e74824248b9bb6d Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 1 Mar 2024 12:34:02 +0100 Subject: [PATCH] first working version for UPDATE-trigger-function --- .../HsOfficeBankAccountEntity.java | 5 + .../office/contact/HsOfficeContactEntity.java | 5 + .../office/debitor/HsOfficeDebitorEntity.java | 11 ++- .../partner/HsOfficePartnerDetailsEntity.java | 5 + .../office/partner/HsOfficePartnerEntity.java | 2 +- .../office/person/HsOfficePersonEntity.java | 6 ++ .../HsOfficeRelationshipEntity.java | 14 ++- .../HsOfficeSepaMandateEntity.java | 2 +- .../hsadminng/rbac/rbacdef/RbacView.java | 55 ++++++++++- .../rbacdef/RbacViewMermaidFlowchart.java | 4 +- .../rbacdef/RbacViewPostgresGenerator.java | 11 ++- .../RolesGrantsAndPermissionsGenerator.java | 97 +++++++++++-------- .../hsadminng/rbac/rbacdef/StringWriter.java | 62 +++++++++--- 13 files changed, 208 insertions(+), 71 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 5655fead..24258251 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 @@ -12,6 +12,7 @@ import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.Table; +import java.io.IOException; import java.util.UUID; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.*; @@ -72,4 +73,8 @@ public class HsOfficeBankAccountEntity implements HasUuid, Stringifyable { with.permission(VIEW); }); } + + public static void main(String[] args) throws IOException { + rbac().generateWithBaseFileName("243-hs-office-bankaccount-rbac"); + } } 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 64baa4bc..7d655fc3 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 @@ -11,6 +11,7 @@ import net.hostsharing.hsadminng.stringify.Stringifyable; import org.hibernate.annotations.GenericGenerator; import jakarta.persistence.*; +import java.io.IOException; import java.util.UUID; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL; @@ -76,4 +77,8 @@ public class HsOfficeContactEntity implements Stringifyable, HasUuid { with.permission(VIEW); }); } + + public static void main(String[] args) throws IOException { + rbac().generateWithBaseFileName("203-hs-office-contact-rbac"); + } } 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 afe905b3..89dcf05d 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 @@ -14,6 +14,7 @@ import net.hostsharing.hsadminng.stringify.Stringifyable; import org.hibernate.annotations.GenericGenerator; import jakarta.persistence.*; +import java.io.IOException; import java.util.Optional; import java.util.UUID; @@ -123,7 +124,7 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable { .withUpdatableColumns( "debitorRel", "billable", - "billingContactUuid", + "debitorUuid", "refundBankAccountUuid", "vatId", "vatCountryCode", @@ -144,7 +145,7 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable { .createPermission(VIEW).grantedTo("debitorRel", TENANT) .importEntityAlias("refundBankAccount", HsOfficeBankAccountEntity.class, - dependsOnColumn("bankAccountUuid"), fetchedBySql(""" + dependsOnColumn("refundBankAccountUuid"), fetchedBySql(""" SELECT * FROM hs_office_relationship AS r WHERE r.relType = 'ACCOUNTING' AND r.relHolderUuid = ${REF}.debitorRelUuid @@ -154,7 +155,7 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable { .toRole("debitorRel", AGENT).grantRole("refundBankAccount", REFERRER) .importEntityAlias("partnerRel", HsOfficeRelationshipEntity.class, - dependsOnColumn("debitorRelUuid"), fetchedBySql(""" + dependsOnColumn("partnerRelUuid"), fetchedBySql(""" SELECT * FROM hs_office_relationship AS partnerRel WHERE ${debitorRel}.relAnchorUuid = partnerRel.relHolderUuid @@ -168,4 +169,8 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable { .forExampleRole("operationalPerson", ADMIN).wouldBeGrantedTo("partnerRel", ADMIN) .forExampleRole("partnerRel", TENANT).wouldBeGrantedTo("partnerPerson", REFERRER); } + + public static void main(String[] args) throws IOException { + rbac().generateWithBaseFileName("273-hs-office-debitor-rbac"); + } } 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 736e95cc..dbc6c17c 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 @@ -10,6 +10,7 @@ import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; import jakarta.persistence.*; +import java.io.IOException; import java.time.LocalDate; import java.util.UUID; @@ -100,4 +101,8 @@ public class HsOfficePartnerDetailsEntity implements HasUuid, Stringifyable { // not when anything in partner details changes. ; } + + public static void main(String[] args) throws IOException { + rbac().generateWithBaseFileName("234-hs-office-partner-details-rbac"); + } } 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 c2a11c0e..cbd184c6 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 @@ -108,6 +108,6 @@ public class HsOfficePartnerEntity implements Stringifyable, HasUuid { } public static void main(String[] args) throws IOException { - HsOfficePartnerEntity.rbac().generateWithBaseFileName("233-hs-office-partner-rbac"); + 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 87232918..104b5d61 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 @@ -10,6 +10,7 @@ import net.hostsharing.hsadminng.stringify.Stringifyable; import org.apache.commons.lang3.StringUtils; import jakarta.persistence.*; +import java.io.IOException; import java.util.UUID; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL; @@ -80,4 +81,9 @@ public class HsOfficePersonEntity implements HasUuid, Stringifyable { with.permission(VIEW); }); } + + + public static void main(String[] args) throws IOException { + rbac().generateWithBaseFileName("213-hs-office-person-rbac"); + } } 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 44f12cf5..82356e28 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 @@ -11,6 +11,7 @@ import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; import jakarta.persistence.*; +import java.io.IOException; import java.util.UUID; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; @@ -86,13 +87,16 @@ public class HsOfficeRelationshipEntity implements HasUuid, Stringifyable { """)) .withUpdatableColumns("contactUuid") .importEntityAlias("anchorPerson", HsOfficePersonEntity.class, - dependsOnColumn("relAnchorUuid"), fetchedBySql("select * from hs_office_person as p where p.uuid = ${REF}.relAnchorUuid") + dependsOnColumn("relAnchorUuid"), + fetchedBySql("select * from hs_office_person as p where p.uuid = ${REF}.relAnchorUuid") ) .importEntityAlias("holderPerson", HsOfficePersonEntity.class, - dependsOnColumn("relHolderUuid"), fetchedBySql("select * from hs_office_person as p where p.uuid = ${REF}.relHolderUuid") + dependsOnColumn("relHolderUuid"), + fetchedBySql("select * from hs_office_person as p where p.uuid = ${REF}.relHolderUuid") ) .importEntityAlias("contact", HsOfficeContactEntity.class, - dependsOnColumn("contactUuid"), fetchedBySql("select * from hs_office_contact as c where c.uuid = ${REF}.contactUuid") + dependsOnColumn("contactUuid"), + fetchedBySql("select * from hs_office_contact as c where c.uuid = ${REF}.contactUuid") ) .createRole(OWNER, (with) -> { with.owningUser(CREATOR); @@ -115,4 +119,8 @@ public class HsOfficeRelationshipEntity implements HasUuid, Stringifyable { with.permission(VIEW); }); } + + public static void main(String[] args) throws IOException { + rbac().generateWithBaseFileName("223-hs-office-relationship-rbac"); + } } 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 fec9c105..ac0d6f99 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 @@ -123,6 +123,6 @@ public class HsOfficeSepaMandateEntity implements Stringifyable, HasUuid { } public static void main(String[] args) throws IOException { - HsOfficeSepaMandateEntity.rbac().generateWithBaseFileName("253-hs-office-sepamandate-rbac"); + 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 dde18a66..00e74c95 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.lang.reflect.Method; import java.nio.file.Path; import java.util.function.Consumer; @@ -12,7 +13,9 @@ import jakarta.persistence.Table; import jakarta.validation.constraints.NotNull; import java.lang.reflect.InvocationTargetException; import java.util.*; +import java.util.stream.Stream; +import static java.lang.reflect.Modifier.isStatic; import static java.util.Optional.ofNullable; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.autoFetched; @@ -57,7 +60,6 @@ public class RbacView { new RbacUserReference(CREATOR); entityAliases.put("global", new EntityAlias("global")); } - public RbacView withUpdatableColumns(final String... columnNames) { Collections.addAll(updatableColumns, columnNames); return this; @@ -493,10 +495,11 @@ public class RbacView { return entityClass == null; } + @NotNull @Override public SQL fetchSql() { if ( fetchSql == null ) { - return null; + return SQL.noop(); } return switch (fetchSql.part) { case SQL_QUERY -> fetchSql; @@ -505,6 +508,10 @@ public class RbacView { }; } + public boolean hasFetchSql() { + return fetchSql != null; + } + private String withoutEntitySuffix(final String simpleEntityName) { return simpleEntityName.substring(0, simpleEntityName.length()-"Entity".length()); } @@ -583,6 +590,15 @@ public class RbacView { return new SQL(null, Part.AUTO_FETCH); } + /** + * DSL method to specify there there is no SQL query specified. + * + * @return a wrapped SQL definition object representing a noop query + */ + public static SQL noop() { + return new SQL(null, Part.NOOP); + } + /** Generic DSL method to specify an SQL SELECT expression. * * @param sql an SQL SELECT expression (not ending with ';) @@ -604,8 +620,10 @@ public class RbacView { } enum Part { + NOOP, SQL_QUERY, - AUTO_FETCH, SQL_PROJECTION + AUTO_FETCH, + SQL_PROJECTION } final String sql; @@ -668,4 +686,35 @@ public class RbacView { return outerAliasName + "." + originalAliasName; } } + + 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 + ).forEach(c -> { + final Method mainMethod = Arrays.stream(c.getMethods()).filter( + m -> isStatic(m.getModifiers()) && m.getName().equals("main") + ) + .findFirst() + .orElse(null); + if (mainMethod != null) { + try { + mainMethod.invoke(null, new Object[]{null}); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } else { + System.err.println("no main method in: " + c.getName()); + } + }); + } } 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 1495fab1..ef7fa3b0 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java @@ -76,7 +76,7 @@ public class RbacViewMermaidFlowchart { private void wrapOutputInSubgraph(final String name, final String color, final String content) { if (!StringUtils.isEmpty(content)) { - flowchart.ensureEmptyLine(); + flowchart.ensureSingleEmptyLine(); flowchart.writeLn("subgraph " + name + "[ ]\n"); flowchart.indented(() -> { flowchart.writeLn("style %{aliasName} fill:%{fillColor},stroke:white" @@ -102,7 +102,7 @@ public class RbacViewMermaidFlowchart { .filter(g -> g.grantType() == f) .toList(); if ( !userGrants.isEmpty()) { - flowchart.ensureEmptyLine(); + flowchart.ensureSingleEmptyLine(); flowchart.writeLn(t); userGrants.forEach(g -> flowchart.writeLn(grantDef(g))); } 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 36e963c0..75333987 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java @@ -8,6 +8,8 @@ import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.time.LocalDateTime; +import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.NEW; +import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with; public class RbacViewPostgresGenerator { @@ -21,10 +23,11 @@ public class RbacViewPostgresGenerator { liqibaseTagPrefix = rbacDef.getRootEntityAlias().entityClass().getSimpleName(); plPgSql.writeLn(""" --liquibase formatted sql - -- This code generated was by ${generator} at %{timestamp}. - """ - .replace("${generator}", getClass().getSimpleName()) - .replace("%{timestamp}", LocalDateTime.now().toString())); + -- This code generated was by ${generator} at ${timestamp}. + """, + with("generator", getClass().getSimpleName()), + with("timestamp", LocalDateTime.now().toString()), + with("ref", NEW.name())); new RolesGrantsAndPermissionsGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql); } 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 4347549f..887328e8 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java @@ -4,7 +4,6 @@ import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacPermissionDefinition; import java.util.HashSet; import java.util.List; -import java.util.Objects; import java.util.Set; import java.util.stream.Stream; @@ -15,6 +14,7 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.OL 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 net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with; import static org.apache.commons.lang3.StringUtils.capitalize; import static org.apache.commons.lang3.StringUtils.uncapitalize; @@ -65,27 +65,32 @@ class RolesGrantsAndPermissionsGenerator { /* A Creates the roles, grants and permission for the AFTER INSERT TRIGGER. */ - + create or replace procedure buildRbacSystemFor${simpleEntityName}( TG_OP text, OLD ${rawTableName}, NEW ${rawTableName} ) language plpgsql as $$ + declare """ .replace("${simpleEntityName}", simpleEntityName) .replace("${rawTableName}", rawTableName)); + plPgSql.chopEmptyLines(); plPgSql.indented(() -> { + + referencedEntityAliases() + .forEach((ea) -> plPgSql.writeLn(entityRefVar(NEW, ea) + " " + getRawTableName(ea.entityClass()) + ";")); + updatableEntityAliases() - .forEach((ea) -> { - plPgSql.writeLn(entityRefVar(NEW, ea) + " " + getRawTableName(ea.entityClass()) + ";"); - }); + .forEach((ea) -> plPgSql.writeLn(entityRefVar(OLD, ea) + " " + getRawTableName(ea.entityClass()) + ";")); }); + plPgSql.writeLn(); + plPgSql.writeLn("begin"); plPgSql.indented(() -> { - plPgSql.writeLn("begin"); generateCreateRolesAndGrantsAfterInsert(plPgSql); if (hasAnyUpdatableEntityAliases()) { @@ -96,28 +101,28 @@ class RolesGrantsAndPermissionsGenerator { raise exception 'invalid usage of TRIGGER'; end if; """); - plPgSql.ensureEmptyLine(); + plPgSql.ensureSingleEmptyLine(); }); plPgSql.writeLn("end; $$;"); plPgSql.writeLn(); } private boolean hasAnyUpdatableEntityAliases() { - return updatableEntityAliases().anyMatch(e -> true); + return updatableEntityAliases().anyMatch(e -> true); } private void generateCreateRolesAndGrantsAfterInsert(final StringWriter plPgSql) { - plPgSql.ensureEmptyLine(); + referencedEntityAliases() + .forEach((ea) -> plPgSql.writeLn( + ea.fetchSql().sql + " into " + entityRefVar(NEW, ea) + ";", + with("ref", NEW.name()))); + + plPgSql.ensureSingleEmptyLine(); plPgSql.writeLn("if TG_OP = 'INSERT' then"); plPgSql.indented(() -> { - updatableEntityAliases() - .forEach((ea) -> { - plPgSql.writeLn( - ea.fetchSql().sql.replace("${ref}", NEW.name()) + " into " + entityRefVar(NEW, ea) + ";"); - }); - + plPgSql.chopEmptyLines(); createRolesWithGrantsSql(plPgSql, OWNER); createRolesWithGrantsSql(plPgSql, ADMIN); createRolesWithGrantsSql(plPgSql, AGENT); @@ -127,38 +132,36 @@ class RolesGrantsAndPermissionsGenerator { generateGrants(plPgSql, ROLE_TO_USER); generateGrants(plPgSql, ROLE_TO_ROLE); generateGrants(plPgSql, PERM_TO_ROLE); + plPgSql.ensureSingleEmptyLine(); }); } private Stream referencedEntityAliases() { return rbacDef.getEntityAliases().values().stream() - .filter((ea) -> !rbacDef.isRootEntityAlias(ea)) - .filter((ea) -> ea.fetchSql() != null); + .filter(ea -> !rbacDef.isRootEntityAlias(ea)) + .filter(ea -> ea.dependsOnColum() != null) + .filter(ea -> ea.entityClass() != null) + .filter(ea -> ea.fetchSql() != null); } private Stream updatableEntityAliases() { return referencedEntityAliases() - .filter(ea -> rbacDef.getUpdatableColumns().contains(ea.dependsOnColum().column) ); + .filter(ea -> rbacDef.getUpdatableColumns().contains(ea.dependsOnColum().column)); } private void generateUpdateRolesAndGrantsAfterUpdate(final StringWriter plPgSql) { - plPgSql.ensureEmptyLine(); + plPgSql.ensureSingleEmptyLine(); plPgSql.writeLn("elsif TG_OP = 'UPDATE' then"); plPgSql.indented(() -> { - rbacDef.getEntityAliases().values().stream() - .filter(ea -> !rbacDef.isRootEntityAlias(ea)) - .filter(ea -> ea.fetchSql() != null) - .forEach(ea -> { - plPgSql.writeLn( - ea.fetchSql().sql.replace("${ref}", OLD.name()) + " into " + entityRefVar(OLD, ea) + ";"); - }); + updatableEntityAliases() + .forEach((ea) -> plPgSql.writeLn( + ea.fetchSql().sql + " into " + entityRefVar(OLD, ea) + ";", + with("ref", OLD.name()))); - rbacDef.getEntityAliases().values().stream() + updatableEntityAliases() .map(RbacView.EntityAlias::dependsOnColum) - .filter(Objects::nonNull) - .filter(this::isUpdatable) .map(c -> c.column) .sorted() .distinct() @@ -182,34 +185,48 @@ class RolesGrantsAndPermissionsGenerator { .filter(RbacView.RbacGrantDefinition::isToCreate) .filter(g -> g.dependsOnColumn(columnName)) .forEach(g -> { - plPgSql.writeLn("-- TODO: revoke " + g); + plPgSql.ensureSingleEmptyLine(); + plPgSql.writeLn(generateRevoke(g)); plPgSql.writeLn(generateGrant(g)); + plPgSql.writeLn(); }); } private void generateGrants(final StringWriter plPgSql, final RbacView.RbacGrantDefinition.GrantType grantType) { - plPgSql.ensureEmptyLine(); + plPgSql.ensureSingleEmptyLine(); rbacGrants.stream() .filter(g -> g.grantType() == grantType) .map(this::generateGrant) .sorted() - .forEach(plPgSql::writeLn); + .forEach(text -> plPgSql.writeLn(text)); + } + + private String generateRevoke(RbacView.RbacGrantDefinition grantDef) { + return switch (grantDef.grantType()) { + case ROLE_TO_USER -> throw new IllegalArgumentException("unexpected grant"); + case ROLE_TO_ROLE -> "call revokeRoleFromRole(${subRoleRef}, ${superRoleRef});" + .replace("${subRoleRef}", roleRef(OLD, grantDef.getSubRoleDef())) + .replace("${superRoleRef}", roleRef(OLD, grantDef.getSuperRoleDef())); + case PERM_TO_ROLE -> "call revokePermissionFromRole(${permRef}, ${superRoleRef});" + .replace("${permRef}", permRef(OLD, grantDef.getPermDef())) + .replace("${superRoleRef}", roleRef(OLD, grantDef.getSuperRoleDef())); + }; } private String generateGrant(RbacView.RbacGrantDefinition grantDef) { return switch (grantDef.grantType()) { case ROLE_TO_USER -> throw new IllegalArgumentException("unexpected grant"); - case ROLE_TO_ROLE -> "call grantRoleToRole(${subRoleRef}, ${superRoleRef}));" + case ROLE_TO_ROLE -> "call grantRoleToRole(${subRoleRef}, ${superRoleRef});" .replace("${subRoleRef}", roleRef(NEW, grantDef.getSubRoleDef())) .replace("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef())); - case PERM_TO_ROLE -> "call grantPermissionsToRole(${permRef}, ${superRoleRef}));" + case PERM_TO_ROLE -> "call grantPermissionsToRole(${permRef}, ${superRoleRef});" .replace("${permRef}", permRef(NEW, grantDef.getPermDef())) .replace("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef())); }; } private String permRef(final PostgresTriggerReference ref, final RbacPermissionDefinition permDef) { - return "createPermissions(${entityRef}.uuid, array ['${perm}']" + return "createPermissions(${entityRef}.uuid, array ['${perm}'])" .replace("${entityRef}", rbacDef.isRootEntityAlias(permDef.entityAlias) ? ref.name() : refVarName(ref, permDef.entityAlias)) @@ -232,10 +249,12 @@ class RolesGrantsAndPermissionsGenerator { + "(" + entityRefVar + ")"; } - private static String entityRefVar( + private String entityRefVar( final PostgresTriggerReference rootRefVar, final RbacView.EntityAlias entityAlias) { - return rootRefVar.name().toLowerCase() + capitalize(entityAlias.aliasName()); + return rbacDef.isRootEntityAlias(entityAlias) + ? rootRefVar.name() + : rootRefVar.name().toLowerCase() + capitalize(entityAlias.aliasName()); } private void createRolesWithGrantsSql(final StringWriter plPgSql, final RbacView.Role role) { @@ -369,7 +388,7 @@ class RolesGrantsAndPermissionsGenerator { private void generateInsertTrigger(final StringWriter plPgSql) { plPgSql.writeLn(""" /* - An AFTER INSERT TRIGGER which creates the role structure for a new ${simpleEntityName} + AFTER INSERT TRIGGER to create the role+grant structure for a new ${rawTableName} row. */ create or replace function insertTriggerFor${simpleEntityName}_tf() @@ -396,7 +415,7 @@ class RolesGrantsAndPermissionsGenerator { private void generateUpdateTrigger(final StringWriter plPgSql) { plPgSql.writeLn(""" /* - An AFTER UPDATE TRIGGER which re-wires the grant structure for an updated ${simpleEntityName} + AFTER INSERT TRIGGER to re-wire the grant structure for a new ${rawTableName} row. */ create or replace function updateTriggerFor${simpleEntityName}_tf() 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 9ee873ec..876527dc 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/StringWriter.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/StringWriter.java @@ -2,6 +2,8 @@ package net.hostsharing.hsadminng.rbac.rbacdef; import org.apache.commons.lang3.StringUtils; +import java.util.regex.Pattern; + import static java.util.Arrays.stream; import static java.util.stream.Collectors.joining; @@ -10,24 +12,22 @@ public class StringWriter { private final StringBuilder string = new StringBuilder(); private int indentLevel = 0; + static VarDef with(final String var, final String name) { + return new VarDef(var, name); + } + void writeLn(final String text) { string.append( indented(text)); writeLn(); } - void writeLn() { - string.append( "\n"); + void writeLn(final String text, final VarDef... varDefs) { + string.append( indented( new VarReplacer(varDefs).apply(text) )); + writeLn(); } - private String indented(final String text) { - if ( indentLevel == 0) { - return text; - } - final var indentation = StringUtils.repeat(" ", indentLevel); - final var indented = stream(text.split("\n")) - .map(line -> line.trim().isBlank() ? "" : indentation + line) - .collect(joining("\n")); - return indented; + void writeLn() { + string.append( "\n"); } void indent() { @@ -58,14 +58,46 @@ public class StringWriter { }; } - void ensureEmptyLine() { - if (!string.toString().endsWith("\n\n")) { - writeLn(); - } + void ensureSingleEmptyLine() { + chopEmptyLines(); + writeLn(); } @Override public String toString() { return string.toString(); } + + private String indented(final String text) { + if ( indentLevel == 0) { + return text; + } + final var indentation = StringUtils.repeat(" ", indentLevel); + final var indented = stream(text.split("\n")) + .map(line -> line.trim().isBlank() ? "" : indentation + line) + .collect(joining("\n")); + return indented; + } + + record VarDef(String name, String value){} + + private static final class VarReplacer { + + private final VarDef[] varDefs; + private String text; + + private VarReplacer(VarDef[] varDefs) { + this.varDefs = varDefs; + } + + String apply(final String text) { + this.text = text; + stream(varDefs).forEach(varDef -> { + final var pattern = Pattern.compile("\\$\\{" + varDef.name() + "}", Pattern.CASE_INSENSITIVE); + final var matcher = pattern.matcher(text); + this.text = matcher.replaceAll(varDef.value()); + }); + return this.text; + } + } }