From 491516e516507d7003bd48c8b052dd315d6ce724 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 21 Feb 2024 13:02:54 +0100 Subject: [PATCH 01/53] experimental RbacView: API for a potential Mermaid + pl/pgSQL generator --- .../HsOfficeBankAccountEntity.java | 88 +++++++++++ .../hsadminng/rbac/rbacdef/RbacView.java | 147 ++++++++++++++++++ 2 files changed, 235 insertions(+) create mode 100644 src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java 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 4d067f68..94cd60e4 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 @@ -3,7 +3,10 @@ package net.hostsharing.hsadminng.hs.office.bankaccount; import lombok.*; import lombok.experimental.FieldNameConstants; import net.hostsharing.hsadminng.errors.DisplayName; +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; @@ -13,6 +16,10 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; import java.util.UUID; +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.Role.*; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.stringify.Stringify.stringify; @Entity @@ -50,4 +57,85 @@ public class HsOfficeBankAccountEntity implements HasUuid, Stringifyable { public String toShortString() { return holder; } + + public static RbacView hsOfficeBankAccount() { + // @formatter:off + return rbacViewFor(HsOfficeBankAccountEntity.class) + .alias("bankAccount") + .withIdentityViewSqlQuery("target.iban || ':' || target.holder") + .withUpdatableColumns("holder", "iban", "bic") + .createRole(OWNER) + .withCurrentUserAsOwner() + .withPermission(ALL) + .withIncomingSuperRole(GLOBAL, ADMIN) + .createSubRole(ADMIN) + .withPermission(UPDATE) + .createSubRole(REFERRER) + .withPermission(READ) + .pop(); + // @formatter:on + } + + public static RbacView hsOfficeDebitor() { + // @formatter:off + return rbacViewFor(HsOfficeDebitorEntity.class) + .alias("debitor") + .withIdentityViewSqlQuery(""" + SELECT debitor.uuid, + 'D-' || (SELECT partner.partnerNumber + FROM hs_office_partner partner + JOIN hs_office_relationship partnerRel + ON partnerRel.uuid = partner.partnerRoleUUid AND partnerRel.relType = 'PARTNER' + JOIN hs_office_relationship debitorRel + ON debitorRel.relAnchorUuid = partnerRel.relHolderUuid AND partnerRel.relType = 'ACCOUNTING' + WHERE debitorRel.uuid = debitor.debitorRelUuid) + || to_char(debitorNumberSuffix, 'fm00') + from hs_office_debitor as debitor; + """) + .withUpdatableColumns( + "debitorRel", + "billable", + "billingContactUuid", + "refundBankAccountUuid", + "vatId", + "vatCountryCode", + "vatBusiness", + "vatreversecharge", + "defaultPrefix" /* TODO: do we want that updatable? */ ) + .createPermission(extraPermission("new-debitor")).grantedTo("global", ADMIN).pop() + + .defineEntityAlias("debitorRel", HsOfficeRelationshipEntity.class, """ + SELECT * + FROM hs_office_relationship AS r + WHERE r.relType = 'ACCOUNTING' AND r.relHolderUuid = ${REF}.debitorRelUuid; + """, "debitorRelUuid") + .createPermission(ALL).grantedTo("hsOfficeRelationship:DEBITOR", OWNER).pop() + .createPermission(UPDATE).grantedTo("hsOfficeRelationship:DEBITOR", ADMIN).pop() + .createPermission(READ).grantedTo("hsOfficeRelationship:DEBITOR", TENANT).pop() + + .defineEntityAlias("bankAccount", HsOfficeBankAccountEntity.class, """ + SELECT * + FROM hs_office_relationship AS r + WHERE r.relType = 'ACCOUNTING' AND r.relHolderUuid = ${REF}.debitorRelUuid; + """, "bankAccountUuid") + .toRole("hsOfficeBankAccount", ADMIN).grantRole("debitorRel", AGENT) + .toRole("debitorRel", AGENT).grantRole("hsOfficeBankAccount", REFERRER) + + .defineEntityAlias("partnerRel", HsOfficeRelationshipEntity.class, """ + SELECT * + FROM hs_office_relationship AS partnerRel + WHERE ${debitorRel}.relAnchorUuid = partnerRel.relHolderUuid; + """, "debitorRelUuid") + .toRole("partnerRel", ADMIN).grantRole("debitorRel", ADMIN) + .toRole("debitorRel", ADMIN).grantRole("partnerRel", AGENT) + .toRole("partnerRel", AGENT).grantRole("debitorRel", AGENT) + .toRole("debitorRel", AGENT).grantRole("partnerRel", TENANT) + .declareEntityAliases("partnerPerson", "operationalPerson") + .forExampleRole("partnerPerson", ADMIN).wouldBeGrantedTo("partnerRel", ADMIN) + .forExampleRole("operationalPerson", ADMIN).wouldBeGrantedTo("partnerRel", ADMIN) + .forExampleRole("partnerRel", TENANT).wouldBeGrantedTo("partnerPerson", REFERRER); + + + // @formatter:on + } } diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java new file mode 100644 index 00000000..1b6af664 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -0,0 +1,147 @@ +package net.hostsharing.hsadminng.rbac.rbacdef; + +import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity; +import net.hostsharing.hsadminng.persistence.HasUuid; + +public class RbacView { + + public static final String GLOBAL = "global"; + + public static RbacView rbacViewFor(final Class entityClass) { + return new RbacView<>(entityClass); + } + + RbacView(final Class entityClass) { + + } + public RbacView alias(final String bankAccount) { + return this; + } + + public RbacView withUpdatableColumns(final String... columnNames) { + return this; + } + + public RbacView withIdentityViewSqlQuery(final String sqlExpression) { + return this; + } + + public RbacRoleDefinition createRole(final Role role) { + return new RbacRoleDefinition<>(role); + } + + public RbacPermissionDefinition createPermission(final Permission permission) { + return new RbacPermissionDefinition<>(permission); + } + + public RbacView declareEntityAliases(final String... aliases) { + return this; + } + + public RbacView defineEntityAlias( + final String alias, final Class entityClass, final String fetchSql, final String dependsOnColum) { + return this; + } + + public RbacRole toRole(final String hsOfficeBankAccount, final Role role) { + return new RbacRole(hsOfficeBankAccount, role); + } + + public RbacExampleRole forExampleRole(final String entityAlias, final Role role) { + return new RbacExampleRole(entityAlias, role); + } + + public class RbacRole { + + public RbacRole(final String entityAlias, final Role role) { + } + + public RbacView grantRole(final String entityAlias, final Role role) { + return RbacView.this; + } + } + + public class RbacExampleRole { + + public RbacExampleRole(final String entityAlias, final Role role) { + } + + public RbacView wouldBeGrantedTo(final String entityAlias, final Role role) { + return RbacView.this; + } + } + + public class RbacPermissionDefinition { + + public RbacPermissionDefinition(final Permission permission) { + } + + 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 owner) { + return this; + } + } + + public class RbacRoleDefinition { + + public RbacRoleDefinition(final Role role) { + } + + public RbacRoleDefinition withCurrentUserAsOwner() { + return this; + } + + public RbacRoleDefinition withPermission(final Permission permission) { + return this; + } + + public RbacRoleDefinition withIncomingSuperRole(final String tableName, final Role role) { + return this; + } + + public RbacRoleDefinition createSubRole(final Role role) { + return this; + } + + public RbacView pop() { + return RbacView.this; + } + } + + public static class Role { + public static final Role OWNER = new Role("owner"); + public static final Role ADMIN = new Role("admin"); + public static final Role AGENT = new Role("agent"); + public static final Role TENANT = new Role("tenant"); + public static final Role REFERRER = new Role("referrer"); + + public Role(final String roleName) { + + } + } + + public static class Permission { + public static final Permission ALL = new Permission("*"); + public static final Permission UPDATE = new Permission("edit"); + public static final Permission READ = new Permission("view"); + + public static Permission extraPermission(final String permission) { + return new Permission(permission); + } + + final String permission; + + private Permission(final String permission) { + this.permission = permission; + } + } +} -- 2.39.5 From a0473976d5fdfa83fb142ed8d38890802175ab30 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 21 Feb 2024 13:22:45 +0100 Subject: [PATCH 02/53] improve readability for defineEntityAlias --- .../HsOfficeBankAccountEntity.java | 15 +++++---- .../hsadminng/rbac/rbacdef/RbacView.java | 31 +++++++++++++++++-- 2 files changed, 36 insertions(+), 10 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 94cd60e4..b424f3d1 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 @@ -16,10 +16,9 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; import java.util.UUID; -import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.*; 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.rbacViewFor; import static net.hostsharing.hsadminng.stringify.Stringify.stringify; @Entity @@ -104,28 +103,28 @@ public class HsOfficeBankAccountEntity implements HasUuid, Stringifyable { "defaultPrefix" /* TODO: do we want that updatable? */ ) .createPermission(extraPermission("new-debitor")).grantedTo("global", ADMIN).pop() - .defineEntityAlias("debitorRel", HsOfficeRelationshipEntity.class, """ + .defineEntityAlias("debitorRel", HsOfficeRelationshipEntity.class, fetchedBySql(""" SELECT * FROM hs_office_relationship AS r WHERE r.relType = 'ACCOUNTING' AND r.relHolderUuid = ${REF}.debitorRelUuid; - """, "debitorRelUuid") + """), dependsOnColumn("debitorRelUuid")) .createPermission(ALL).grantedTo("hsOfficeRelationship:DEBITOR", OWNER).pop() .createPermission(UPDATE).grantedTo("hsOfficeRelationship:DEBITOR", ADMIN).pop() .createPermission(READ).grantedTo("hsOfficeRelationship:DEBITOR", TENANT).pop() - .defineEntityAlias("bankAccount", HsOfficeBankAccountEntity.class, """ + .defineEntityAlias("bankAccount", HsOfficeBankAccountEntity.class, fetchedBySql(""" SELECT * FROM hs_office_relationship AS r WHERE r.relType = 'ACCOUNTING' AND r.relHolderUuid = ${REF}.debitorRelUuid; - """, "bankAccountUuid") + """), dependsOnColumn("bankAccountUuid")) .toRole("hsOfficeBankAccount", ADMIN).grantRole("debitorRel", AGENT) .toRole("debitorRel", AGENT).grantRole("hsOfficeBankAccount", REFERRER) - .defineEntityAlias("partnerRel", HsOfficeRelationshipEntity.class, """ + .defineEntityAlias("partnerRel", HsOfficeRelationshipEntity.class, fetchedBySql(""" SELECT * FROM hs_office_relationship AS partnerRel WHERE ${debitorRel}.relAnchorUuid = partnerRel.relHolderUuid; - """, "debitorRelUuid") + """), dependsOnColumn("debitorRelUuid")) .toRole("partnerRel", ADMIN).grantRole("debitorRel", ADMIN) .toRole("debitorRel", ADMIN).grantRole("partnerRel", AGENT) .toRole("partnerRel", AGENT).grantRole("debitorRel", AGENT) 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 1b6af664..57d7e846 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -7,6 +7,14 @@ public class RbacView { public static final String GLOBAL = "global"; + public static SQL fetchedBySql(final String sql) { + return new SQL(sql); + } + + public static Column dependsOnColumn(final String column) { + return new Column(column); + } + public static RbacView rbacViewFor(final Class entityClass) { return new RbacView<>(entityClass); } @@ -39,7 +47,7 @@ public class RbacView { } public RbacView defineEntityAlias( - final String alias, final Class entityClass, final String fetchSql, final String dependsOnColum) { + final String alias, final Class entityClass, final SQL fetchSql, final Column dependsOnColum) { return this; } @@ -123,9 +131,10 @@ public class RbacView { public static final Role AGENT = new Role("agent"); public static final Role TENANT = new Role("tenant"); public static final Role REFERRER = new Role("referrer"); + private final String roleName; public Role(final String roleName) { - + this.roleName = roleName; } } @@ -144,4 +153,22 @@ public class RbacView { this.permission = permission; } } + + public static class SQL { + + public final String sql; + + public SQL(final String sql) { + this.sql = sql; + } + } + + public static class Column { + + public final String column; + + public Column(final String column) { + this.column = column; + } + } } -- 2.39.5 From f11edc082d81fb6bfd889b3b54db5590aa4cd624 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 22 Feb 2024 18:30:31 +0100 Subject: [PATCH 03/53] generate flowchart for RbacView, with import of simple objects --- .../HsOfficeBankAccountEntity.java | 63 +-- .../hsadminng/rbac/rbacdef/RbacView.java | 401 +++++++++++++++--- .../rbacdef/RbacViewMermaidFlowchart.java | 144 +++++++ .../243-hs-office-bankaccount-rbac.md | 4 +- 4 files changed, 522 insertions(+), 90 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java 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 b424f3d1..351a5f96 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 @@ -17,8 +17,10 @@ import jakarta.persistence.Table; import java.util.UUID; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.*; +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.stringify.Stringify.stringify; @Entity @@ -57,29 +59,27 @@ public class HsOfficeBankAccountEntity implements HasUuid, Stringifyable { return holder; } - public static RbacView hsOfficeBankAccount() { + public static RbacView hsOfficeBankAccount() { // @formatter:off - return rbacViewFor(HsOfficeBankAccountEntity.class) - .alias("bankAccount") - .withIdentityViewSqlQuery("target.iban || ':' || target.holder") + return rbacViewFor("bankAccount", HsOfficeBankAccountEntity.class) + .withIdentityView(SQL.query("target.iban || ':' || target.holder")) .withUpdatableColumns("holder", "iban", "bic") .createRole(OWNER) .withCurrentUserAsOwner() .withPermission(ALL) .withIncomingSuperRole(GLOBAL, ADMIN) .createSubRole(ADMIN) - .withPermission(UPDATE) + .withPermission(EDIT) .createSubRole(REFERRER) - .withPermission(READ) + .withPermission(VIEW) .pop(); // @formatter:on } - public static RbacView hsOfficeDebitor() { + public static RbacView hsOfficeDebitor() { // @formatter:off - return rbacViewFor(HsOfficeDebitorEntity.class) - .alias("debitor") - .withIdentityViewSqlQuery(""" + return rbacViewFor("debitor", HsOfficeDebitorEntity.class) + .withIdentityView(SQL.query(""" SELECT debitor.uuid, 'D-' || (SELECT partner.partnerNumber FROM hs_office_partner partner @@ -90,7 +90,7 @@ public class HsOfficeBankAccountEntity implements HasUuid, Stringifyable { WHERE debitorRel.uuid = debitor.debitorRelUuid) || to_char(debitorNumberSuffix, 'fm00') from hs_office_debitor as debitor; - """) + """)) .withUpdatableColumns( "debitorRel", "billable", @@ -101,32 +101,35 @@ public class HsOfficeBankAccountEntity implements HasUuid, Stringifyable { "vatBusiness", "vatreversecharge", "defaultPrefix" /* TODO: do we want that updatable? */ ) - .createPermission(extraPermission("new-debitor")).grantedTo("global", ADMIN).pop() + .createPermission(custom("new-debitor")).grantedTo("global", ADMIN).pop() - .defineEntityAlias("debitorRel", HsOfficeRelationshipEntity.class, fetchedBySql(""" + .defineProxyEntityAlias("debitorRel", HsOfficeRelationshipEntity.class, fetchedBySql(""" SELECT * FROM hs_office_relationship AS r WHERE r.relType = 'ACCOUNTING' AND r.relHolderUuid = ${REF}.debitorRelUuid; - """), dependsOnColumn("debitorRelUuid")) - .createPermission(ALL).grantedTo("hsOfficeRelationship:DEBITOR", OWNER).pop() - .createPermission(UPDATE).grantedTo("hsOfficeRelationship:DEBITOR", ADMIN).pop() - .createPermission(READ).grantedTo("hsOfficeRelationship:DEBITOR", TENANT).pop() + """), + dependsOnColumn("debitorRelUuid")) + .createPermission(ALL).grantedTo("debitorRel", OWNER).pop() + .createPermission(EDIT).grantedTo("debitorRel", ADMIN).pop() + .createPermission(VIEW).grantedTo("debitorRel", TENANT).pop() - .defineEntityAlias("bankAccount", HsOfficeBankAccountEntity.class, fetchedBySql(""" - SELECT * - FROM hs_office_relationship AS r - WHERE r.relType = 'ACCOUNTING' AND r.relHolderUuid = ${REF}.debitorRelUuid; - """), dependsOnColumn("bankAccountUuid")) - .toRole("hsOfficeBankAccount", ADMIN).grantRole("debitorRel", AGENT) - .toRole("debitorRel", AGENT).grantRole("hsOfficeBankAccount", REFERRER) + .defineEntityAlias("refundBankAccount", HsOfficeBankAccountEntity.class, fetchedBySql(""" + SELECT * + FROM hs_office_relationship AS r + WHERE r.relType = 'ACCOUNTING' AND r.relHolderUuid = ${REF}.debitorRelUuid; + """), + dependsOnColumn("bankAccountUuid")) + .importRbacViewAs("refundBankAccount", HsOfficeBankAccountEntity.hsOfficeBankAccount()) + .toRole("refundBankAccount", ADMIN).grantRole("debitorRel", AGENT) + .toRole("debitorRel", AGENT).grantRole("refundBankAccount", REFERRER) - .defineEntityAlias("partnerRel", HsOfficeRelationshipEntity.class, fetchedBySql(""" - SELECT * - FROM hs_office_relationship AS partnerRel - WHERE ${debitorRel}.relAnchorUuid = partnerRel.relHolderUuid; - """), dependsOnColumn("debitorRelUuid")) + .defineEntityAlias("partnerRel", HsOfficeRelationshipEntity.class, fetchedBySql(""" + SELECT * + FROM hs_office_relationship AS partnerRel + WHERE ${debitorRel}.relAnchorUuid = partnerRel.relHolderUuid; + """), + dependsOnColumn("debitorRelUuid")) .toRole("partnerRel", ADMIN).grantRole("debitorRel", ADMIN) - .toRole("debitorRel", ADMIN).grantRole("partnerRel", AGENT) .toRole("partnerRel", AGENT).grantRole("debitorRel", AGENT) .toRole("debitorRel", AGENT).grantRole("partnerRel", TENANT) .declareEntityAliases("partnerPerson", "operationalPerson") 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 57d7e846..880b1577 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -1,174 +1,459 @@ package net.hostsharing.hsadminng.rbac.rbacdef; +import lombok.EqualsAndHashCode; +import lombok.Getter; import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity; import net.hostsharing.hsadminng.persistence.HasUuid; -public class RbacView { +import jakarta.validation.constraints.NotNull; +import java.util.*; + +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserDefinition.UserRole.CREATOR; + +@Getter +public class RbacView { public static final String GLOBAL = "global"; - public static SQL fetchedBySql(final String sql) { - return new SQL(sql); + private final EntityAlias entityAlias; + + private final Set userDefs = new HashSet<>(); + private final Set roleDefs = new HashSet<>(); + private final Set permDefs = new HashSet<>(); + private final Map entityAliases = new HashMap<>() { + + @Override + public EntityAlias put(final String key, final EntityAlias value) { + if ( containsKey(key) ) { + throw new IllegalArgumentException("duplicate entityAlias: " + key); + } + return super.put(key, value); + } + }; + private final Set updatableColumns = new TreeSet<>(); + private final Set grantDefs = new HashSet<>(); + + private SQL identityViewSqlQuery; + private EntityAlias entityAliasProxy; + + public static RbacView rbacViewFor(final String alias, final Class entityClass) { + return new RbacView(alias, entityClass); } - public static Column dependsOnColumn(final String column) { - return new Column(column); + RbacView(final String alias, final Class entityClass) { + entityAlias = new EntityAlias(alias, entityClass); + entityAliases.put(alias, entityAlias); + new RbacUserDefinition(CREATOR); + entityAliases.put("global", new EntityAlias("global")); } - public static RbacView rbacViewFor(final Class entityClass) { - return new RbacView<>(entityClass); - } - - RbacView(final Class entityClass) { - - } - public RbacView alias(final String bankAccount) { + public RbacView withUpdatableColumns(final String... columnNames) { + Collections.addAll(updatableColumns, columnNames); return this; } - public RbacView withUpdatableColumns(final String... columnNames) { + public RbacView withIdentityView(final SQL sqlExpression) { + this.identityViewSqlQuery = sqlExpression; return this; } - public RbacView withIdentityViewSqlQuery(final String sqlExpression) { + public RbacRoleDefinition createRole(final Role role) { + return findRbacRole(entityAlias, role); + } + + public RbacPermissionDefinition createPermission(final Permission permission) { + return createPermission(entityAlias, permission); + } + + private RbacPermissionDefinition createPermission(final EntityAlias entityAlias, final Permission permission) { + final RbacPermissionDefinition permDef = new RbacPermissionDefinition(entityAlias, permission, true); + permDefs.add(permDef); + return permDef; + } + + public RbacView declareEntityAliases(final String... aliasNames) { + for ( String alias: aliasNames ) { + entityAliases.put(alias, new EntityAlias(alias)); + } return this; } - public RbacRoleDefinition createRole(final Role role) { - return new RbacRoleDefinition<>(role); - } - - public RbacPermissionDefinition createPermission(final Permission permission) { - return new RbacPermissionDefinition<>(permission); - } - - public RbacView declareEntityAliases(final String... aliases) { + public RbacView defineProxyEntityAlias( + final String aliasName, final Class entityClass, final SQL fetchSql, final Column dependsOnColum) { + entityAliasProxy = new EntityAlias(aliasName, entityClass, fetchSql, dependsOnColum); + entityAliases.put(aliasName, entityAliasProxy); return this; } - public RbacView defineEntityAlias( - final String alias, final Class entityClass, final SQL fetchSql, final Column dependsOnColum) { + public RbacView defineEntityAlias( + final String aliasName, final Class entityClass, final SQL fetchSql, final Column dependsOnColum) { + entityAliases.put(aliasName, new EntityAlias(aliasName, entityClass, fetchSql, dependsOnColum)); return this; } - public RbacRole toRole(final String hsOfficeBankAccount, final Role role) { - return new RbacRole(hsOfficeBankAccount, role); + public RbacView importRbacViewAs(final String aliasName, final RbacView importedRbacView) { + final var mapper = new AliasNameMapper(importedRbacView, aliasName); + importedRbacView.getEntityAliases().values().forEach(entityAlias -> { + new EntityAlias( mapper.map(entityAlias.aliasName), entityAlias.entityClass); + }); + importedRbacView.getRoleDefs().forEach(roleDef -> { + new RbacRoleDefinition( findEntityAlias(mapper.map(roleDef.entityAlias.aliasName)), roleDef.role); + }); + importedRbacView.getGrantDefs().forEach(grantDef -> { + if (grantDef.grantType() == RbacGrantDefinition.GrantType.ROLE_TO_ROLE) { + new RbacGrantDefinition( + findRbacRole(mapper.map(grantDef.getSubRoleDef().entityAlias.aliasName), grantDef.getSubRoleDef().getRole()), + findRbacRole(mapper.map(grantDef.getSuperRoleDef().entityAlias.aliasName), grantDef.getSuperRoleDef().getRole()) + ); + } + }); + return this; + } + + public RbacGrantBuilder toRole(final String entityAlias, final Role role) { + return new RbacGrantBuilder(entityAlias, role); } public RbacExampleRole forExampleRole(final String entityAlias, final Role role) { return new RbacExampleRole(entityAlias, role); } - public class RbacRole { - public RbacRole(final String entityAlias, final Role role) { + private RbacGrantDefinition grantRoleToCurrentUser(final RbacRoleDefinition roleDefinition) { + return new RbacGrantDefinition(roleDefinition, currentUser()); + } + + private RbacGrantDefinition grantPermissionToRole(final RbacPermissionDefinition permDef , final RbacRoleDefinition roleDef) { + return new RbacGrantDefinition(permDef, roleDef); + } + + private RbacGrantDefinition grantSubRoleToSuperRole(final RbacRoleDefinition subRoleDefinition, final RbacRoleDefinition superRoleDefinition) { + return new RbacGrantDefinition(subRoleDefinition, superRoleDefinition); + } + + boolean isMainEntityAlias(final EntityAlias entityAlias) { + return entityAlias == this.entityAlias; + } + + public boolean isEntityAliasProxy(final EntityAlias entityAlias) { + return entityAlias == entityAliasProxy; + } + + public class RbacGrantBuilder { + + private final RbacRoleDefinition superRoleDef; + + private RbacGrantBuilder(final String entityAlias, final Role role) { + this.superRoleDef = findRbacRole(entityAlias, role); } - public RbacView grantRole(final String entityAlias, final Role role) { + public RbacView grantRole(final String entityAlias, final Role role) { + new RbacGrantDefinition(findRbacRole(entityAlias, role), superRoleDef); return RbacView.this; } + + } + + @Getter + @EqualsAndHashCode + public class RbacGrantDefinition { + private final RbacUserDefinition userDef; + private final RbacRoleDefinition superRoleDef; + private final RbacRoleDefinition subRoleDef; + private final RbacPermissionDefinition permDef; + + @Override + public String toString() { + return switch (grantType()) { + case USER_TO_ROLE -> userDef.toString() + " --> " + subRoleDef.toString(); + case ROLE_TO_ROLE -> superRoleDef + " --> " + subRoleDef; + case ROLE_TO_PERM -> superRoleDef + " --> " + permDef; + }; + } + + public RbacGrantDefinition(final RbacRoleDefinition subRoleDef, final RbacRoleDefinition superRoleDef) { + this.userDef = null; + this.subRoleDef = subRoleDef; + this.superRoleDef = superRoleDef; + this.permDef = null; + grantDefs.add(this); + } + + public RbacGrantDefinition(final RbacPermissionDefinition permDef, final RbacRoleDefinition roleDef) { + this.userDef = null; + this.subRoleDef = null; + this.superRoleDef = roleDef; + this.permDef = permDef; + grantDefs.add(this); + } + + public RbacGrantDefinition(final RbacRoleDefinition roleDef, final RbacUserDefinition userDef) { + this.userDef = userDef; + this.subRoleDef = roleDef; + this.superRoleDef = null; + this.permDef = null; + } + + @NotNull + GrantType grantType() { + return permDef != null ? GrantType.ROLE_TO_PERM + : userDef != null ? GrantType.USER_TO_ROLE + : GrantType.ROLE_TO_ROLE; + } + + boolean isAssumed() { + // TODO: not implemented yet + return false; + } + + public enum GrantType { + USER_TO_ROLE, + ROLE_TO_ROLE, + ROLE_TO_PERM + } + } + + private void addGrant(final RbacGrantDefinition grant) { + grantDefs.add(grant); } public class RbacExampleRole { + final EntityAlias subRoleEntity; + final Role subRole; + private EntityAlias superRoleEntity; + Role superRole; + public RbacExampleRole(final String entityAlias, final Role role) { + this.subRoleEntity = findEntityAlias(entityAlias); + this.subRole = role; } - public RbacView wouldBeGrantedTo(final String entityAlias, final Role role) { + public RbacView wouldBeGrantedTo(final String entityAlias, final Role role) { + this.superRoleEntity = findEntityAlias(entityAlias); + this.superRole = role; return RbacView.this; } } - public class RbacPermissionDefinition { + @Getter + @EqualsAndHashCode + public class RbacPermissionDefinition { - public RbacPermissionDefinition(final Permission permission) { + final EntityAlias entityAlias; + final Permission permission; + final boolean toCreate; + + public RbacPermissionDefinition(final EntityAlias entityAlias, final Permission permission, final boolean toCreate) { + this.entityAlias = entityAlias; + this.permission = permission; + this.toCreate = toCreate; } - public RbacView pop() { + public RbacView pop() { return RbacView.this; } - public RbacPermissionDefinition withIncomingSuperRole( + public RbacPermissionDefinition withIncomingSuperRole( final Class hsOfficeRelationshipEntityClass, final Role owner) { + return this; } - public RbacPermissionDefinition grantedTo(final String entityAlias, final Role owner) { + public RbacPermissionDefinition grantedTo(final String entityAlias, final Role role) { + new RbacGrantDefinition(this, findRbacRole(entityAlias, role) ); return this; } + + @Override + public String toString() { + return "perm:" + entityAlias.aliasName + permission; + } } - public class RbacRoleDefinition { + @Getter + @EqualsAndHashCode + public class RbacRoleDefinition { - public RbacRoleDefinition(final Role role) { + private final EntityAlias entityAlias; + private final Role role; + private boolean toCreate; + + public RbacRoleDefinition(final EntityAlias entityAlias, final Role role) { + this.entityAlias = entityAlias; + this.role = role; + roleDefs.add(this); } - public RbacRoleDefinition withCurrentUserAsOwner() { + public RbacRoleDefinition toCreate() { + this.toCreate = true; return this; } - public RbacRoleDefinition withPermission(final Permission permission) { + public RbacRoleDefinition withCurrentUserAsOwner() { + addGrant(grantRoleToCurrentUser(this)); return this; } - public RbacRoleDefinition withIncomingSuperRole(final String tableName, final Role role) { + public RbacRoleDefinition withPermission(final Permission permission) { + addGrant(grantPermissionToRole( createPermission(entityAlias, permission) , this)); return this; } - public RbacRoleDefinition createSubRole(final Role role) { + public RbacRoleDefinition withIncomingSuperRole(final String entityAlias, final Role role) { + final var incomingSuperRole = findRbacRole(entityAlias, role); + addGrant(grantSubRoleToSuperRole(this, incomingSuperRole)); return this; } - public RbacView pop() { + public RbacRoleDefinition createSubRole(final Role role) { + final var roleDef = findRbacRole(entityAlias, role).toCreate(); + new RbacGrantDefinition(roleDef, this); + return roleDef; + } + + public RbacView pop() { return RbacView.this; } + + @Override + public String toString() { + return "role:" + entityAlias.aliasName + role; + } } - public static class Role { + public RbacUserDefinition currentUser() { + return userDefs.stream().filter(u -> u.role == CREATOR).findFirst().orElseThrow(); + } + + @EqualsAndHashCode + public class RbacUserDefinition { + + public enum UserRole { + CREATOR + } + + final UserRole role; + public RbacUserDefinition(final UserRole creator) { + this.role = creator; + userDefs.add(this); + } + + @Override + public String toString() { + return "user:" + role; + } + } + + private EntityAlias findEntityAlias(final String aliasName) { + final var found = entityAliases.get(aliasName); + if ( found == null ) { + throw new IllegalArgumentException("entityAlias not found: " + aliasName); + } + return found; + } + + private RbacRoleDefinition findRbacRole(final EntityAlias entityAlias, final Role role) { + return roleDefs.stream() + .filter(r -> r.getEntityAlias() == entityAlias && r.getRole().equals(role)) + .findFirst() + .orElseGet(() -> new RbacRoleDefinition(entityAlias, role)); + } + + private RbacRoleDefinition findRbacRole(final String entityAliasName, final Role role) { + return findRbacRole(findEntityAlias(entityAliasName), role); + } + + record EntityAlias(String aliasName, Class entityClass, SQL fetchSql, Column dependsOnColum) { + + public EntityAlias(final String aliasName) { + this(aliasName, null, null, null); + } + + public EntityAlias(final String aliasName, final Class entityClass) { + this(aliasName, entityClass, null, null); + } + } + + 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"); public static final Role TENANT = new Role("tenant"); public static final Role REFERRER = new Role("referrer"); - private final String roleName; - public Role(final String roleName) { - this.roleName = roleName; + @Override + public String toString() { + return "." + roleName; + } + + @Override + public boolean equals(final Object obj) { + return ((obj instanceof Role) && ((Role)obj).roleName.equals(this.roleName)); } } - public static class Permission { + public record Permission(String permission) { public static final Permission ALL = new Permission("*"); - public static final Permission UPDATE = new Permission("edit"); - public static final Permission READ = new Permission("view"); + public static final Permission EDIT = new Permission("edit"); + public static final Permission VIEW = new Permission("view"); - public static Permission extraPermission(final String permission) { + public static Permission custom(final String permission) { return new Permission(permission); } - final String permission; - - private Permission(final String permission) { - this.permission = permission; + @Override + public String toString() { + return "." + permission; } } public static class SQL { + public static SQL fetchedBySql(final String sql) { + return new SQL(sql); + } + + public static SQL query(final String sql) { + return new SQL(sql); + } + public final String sql; - public SQL(final String sql) { + private SQL(final String sql) { this.sql = sql; } } public static class Column { + public static Column dependsOnColumn(final String column) { + return new Column(column); + } + public final String column; - public Column(final String column) { + private Column(final String column) { this.column = column; } } + + private static class AliasNameMapper { + + private final RbacView importedRbacView; + private final String outerAliasName; + + AliasNameMapper(final RbacView importedRbacView, final String outerAliasName) { + this.importedRbacView = importedRbacView; + this.outerAliasName = outerAliasName; + } + + String map(final String originalAliasName) { + if (originalAliasName.equals(importedRbacView.entityAlias.aliasName) ) { + return outerAliasName; + } + return 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 new file mode 100644 index 00000000..828c45ef --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java @@ -0,0 +1,144 @@ +package net.hostsharing.hsadminng.rbac.rbacdef; + +import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity; +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; +import java.nio.file.*; +import java.time.LocalDateTime; + +import static java.util.stream.Collectors.joining; + +public class RbacViewMermaidFlowchart { + + + private final RbacView rbacDef; + private final StringBuilder flowchart = new StringBuilder(); + + public RbacViewMermaidFlowchart(final RbacView rbacDef) { + this.rbacDef = rbacDef; + flowchart.append(""" + ### rbac %{entityAlias} %{timestamp} + + ```mermaid + flowchart TB + + """ + .replace("%{entityAlias}", rbacDef.getEntityAlias().aliasName()) + .replace("%{timestamp}", LocalDateTime.now().toString())); + renderSubgraphGlobal(); + renderEntitySubgraphs(); + renderGrants(); + flowchart.append("```"); + } + + void renderSubgraphGlobal() { + flowchart.append(""" + subgraph global + style global fill: lightgray + + role:global.admin[global.admin] + end + """); + } + + private void renderEntitySubgraphs() { + rbacDef.getEntityAliases().values().stream() + .filter(entityAlias -> !rbacDef.isEntityAliasProxy(entityAlias)) + .forEach(this::renderEntitySubgraph); + } + + private void renderEntitySubgraph(final RbacView.EntityAlias entity) { + final var color = rbacDef.isMainEntityAlias(entity) ? "lightgreen" : "lightgray"; + flowchart.append(""" + + subgraph %{aliasName} + direction TB + style %{aliasName} fill: %{color} + + """ + .replace("%{aliasName}", entity.aliasName()) + .replace("%{color}", color )); + + wrapOutputInSubgraph(entity.aliasName() + ".roles", color, + rbacDef.getRoleDefs().stream() + .filter(r -> r.getEntityAlias() == entity) + .map(r -> " " + roleDef(r)) + .collect(joining("\n"))); + + wrapOutputInSubgraph(entity.aliasName() + "permissions", color, + rbacDef.getPermDefs().stream() + .filter(p -> p.getEntityAlias() == entity) + .map(p -> " " + permDef(p) ) + .collect(joining("\n"))); + + if (rbacDef.isMainEntityAlias(entity) && rbacDef.getEntityAliasProxy() != null ) { + renderEntitySubgraph(rbacDef.getEntityAliasProxy()); + } + + flowchart.append("end\n\n"); + } + + private void wrapOutputInSubgraph(final String name, final String color, final String content) { + if (!StringUtils.isEmpty(content)) { + flowchart.append("subgraph " + name + "[ ]\n"); + flowchart.append("style %{aliasName} fill: %{color}\n\n" + .replace("%{aliasName}", name) + .replace("%{color}", color)); + flowchart.append(content); + flowchart.append("\nend\n\n"); + } + } + + private void renderGrants() { + rbacDef.getGrantDefs() + .forEach(g -> { + flowchart.append(grantDef(g) + "\n" ); + }); + } + + private String grantDef(final RbacView.RbacGrantDefinition grant) { + return switch (grant.grantType()) { + case USER_TO_ROLE -> + // TODO: other user types not implemented yet + "user:creator" + (grant.isAssumed() ? " -.-> " : " --> ") + roleId(grant.getSubRoleDef()); + case ROLE_TO_ROLE -> + roleId(grant.getSuperRoleDef()) + (grant.isAssumed() ? " -.-> " : " --> ") + roleId(grant.getSubRoleDef()); + case ROLE_TO_PERM -> roleId(grant.getSuperRoleDef()) + " --> " + permId(grant.getPermDef()); + }; + } + + private String permDef(final RbacView.RbacPermissionDefinition perm) { + return permId(perm) + "{{" + perm.getEntityAlias().aliasName() + perm.getPermission() + "}}"; + } + + private static String permId(final RbacView.RbacPermissionDefinition permDef) { + return "perm:" + permDef.getEntityAlias().aliasName() + permDef.getPermission(); + } + + private String roleDef(final RbacView.RbacRoleDefinition roleDef) { + return roleId(roleDef) + "[[" + roleDef.getEntityAlias().aliasName() + roleDef.getRole() + "]]"; + } + + private static String roleId(final RbacView.RbacRoleDefinition r) { + return "role:" + r.getEntityAlias().aliasName() + r.getRole(); + } + + @Override + public String toString() { + return flowchart.toString(); + } + + public static void main(String[] args) throws IOException { + + Files.writeString( + Paths.get("doc", "hsOfficeBankAccount.md"), + new RbacViewMermaidFlowchart(HsOfficeBankAccountEntity.hsOfficeBankAccount()).toString(), + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + + Files.writeString( + Paths.get("doc", "hsOfficeDebitor.md"), + new RbacViewMermaidFlowchart(HsOfficeBankAccountEntity.hsOfficeDebitor()).toString(), + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } +} diff --git a/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.md b/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.md index fc34f147..b2cee782 100644 --- a/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.md +++ b/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.md @@ -4,14 +4,14 @@ flowchart TB subgraph global - style hsOfficeBankAccount fill: #e9f7ef + style global fill: lightgray role:global.admin[global.admin] end subgraph hsOfficeBankAccount direction TB - style hsOfficeBankAccount fill: #e9f7ef + style hsOfficeBankAccount fill: lightgreen user:hsOfficeBankAccount.creator([bankAccount.creator]) -- 2.39.5 From 74071c15db0173ab159a872403233cee3e4068ee Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 23 Feb 2024 12:17:41 +0100 Subject: [PATCH 04/53] generate postgres trigger function + trigger for RbacView for simple objects --- .../rbacdef/PostgresTriggerReference.java | 5 + .../hsadminng/rbac/rbacdef/RbacView.java | 26 +- .../rbacdef/RbacViewPostgresGenerator.java | 47 ++++ .../RolesGrantsAndPermissionsGenerator.java | 238 ++++++++++++++++++ 4 files changed, 305 insertions(+), 11 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/PostgresTriggerReference.java create mode 100644 src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/PostgresTriggerReference.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/PostgresTriggerReference.java new file mode 100644 index 00000000..4fb5cb61 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/PostgresTriggerReference.java @@ -0,0 +1,5 @@ +package net.hostsharing.hsadminng.rbac.rbacdef; + +public enum PostgresTriggerReference { + NEW, OLD +} 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 880b1577..1997696d 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -8,7 +8,7 @@ import net.hostsharing.hsadminng.persistence.HasUuid; import jakarta.validation.constraints.NotNull; import java.util.*; -import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserDefinition.UserRole.CREATOR; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR; @Getter public class RbacView { @@ -17,7 +17,7 @@ public class RbacView { private final EntityAlias entityAlias; - private final Set userDefs = new HashSet<>(); + private final Set userDefs = new HashSet<>(); private final Set roleDefs = new HashSet<>(); private final Set permDefs = new HashSet<>(); private final Map entityAliases = new HashMap<>() { @@ -43,7 +43,7 @@ public class RbacView { RbacView(final String alias, final Class entityClass) { entityAlias = new EntityAlias(alias, entityClass); entityAliases.put(alias, entityAlias); - new RbacUserDefinition(CREATOR); + new RbacUserReference(CREATOR); entityAliases.put("global", new EntityAlias("global")); } @@ -58,7 +58,7 @@ public class RbacView { } public RbacRoleDefinition createRole(final Role role) { - return findRbacRole(entityAlias, role); + return findRbacRole(entityAlias, role).toCreate(); } public RbacPermissionDefinition createPermission(final Permission permission) { @@ -157,7 +157,7 @@ public class RbacView { @Getter @EqualsAndHashCode public class RbacGrantDefinition { - private final RbacUserDefinition userDef; + private final RbacUserReference userDef; private final RbacRoleDefinition superRoleDef; private final RbacRoleDefinition subRoleDef; private final RbacPermissionDefinition permDef; @@ -187,7 +187,7 @@ public class RbacView { grantDefs.add(this); } - public RbacGrantDefinition(final RbacRoleDefinition roleDef, final RbacUserDefinition userDef) { + public RbacGrantDefinition(final RbacRoleDefinition roleDef, final RbacUserReference userDef) { this.userDef = userDef; this.subRoleDef = roleDef; this.superRoleDef = null; @@ -323,19 +323,19 @@ public class RbacView { } } - public RbacUserDefinition currentUser() { + public RbacUserReference currentUser() { return userDefs.stream().filter(u -> u.role == CREATOR).findFirst().orElseThrow(); } @EqualsAndHashCode - public class RbacUserDefinition { + public class RbacUserReference { public enum UserRole { CREATOR } final UserRole role; - public RbacUserDefinition(final UserRole creator) { + public RbacUserReference(final UserRole creator) { this.role = creator; userDefs.add(this); } @@ -354,14 +354,14 @@ public class RbacView { return found; } - private RbacRoleDefinition findRbacRole(final EntityAlias entityAlias, final Role role) { + public RbacRoleDefinition findRbacRole(final EntityAlias entityAlias, final Role role) { return roleDefs.stream() .filter(r -> r.getEntityAlias() == entityAlias && r.getRole().equals(role)) .findFirst() .orElseGet(() -> new RbacRoleDefinition(entityAlias, role)); } - private RbacRoleDefinition findRbacRole(final String entityAliasName, final Role role) { + public RbacRoleDefinition findRbacRole(final String entityAliasName, final Role role) { return findRbacRole(findEntityAlias(entityAliasName), role); } @@ -374,6 +374,10 @@ public class RbacView { public EntityAlias(final String aliasName, final Class entityClass) { this(aliasName, entityClass, null, null); } + + boolean isGlobal() { + return aliasName().equals("global"); + } } public record Role(String roleName) { diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java new file mode 100644 index 00000000..862c3458 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java @@ -0,0 +1,47 @@ +package net.hostsharing.hsadminng.rbac.rbacdef; + +import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.time.LocalDateTime; + + +public class RbacViewPostgresGenerator { + + + private final RbacView rbacDef; + private final String liqibaseTagPrefix; + private final StringBuilder plPgSql = new StringBuilder(); + + public RbacViewPostgresGenerator(final RbacView forRbacDef) { + rbacDef = forRbacDef; + liqibaseTagPrefix = rbacDef.getEntityAlias().entityClass().getSimpleName(); + plPgSql.append(""" + --liquibase formatted sql + -- generated at: %{timestamp} + + """ + .replace("%{timestamp}", LocalDateTime.now().toString())); + + // generateSqlForRelatedRbacObject(); + + new RolesGrantsAndPermissionsGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql); + } + + + @Override +public String toString() { + return plPgSql.toString(); +} + +public static void main(String[] args) throws IOException { + + Files.writeString( + Paths.get("doc", "hsOfficeBankAccount.sql"), + new RbacViewPostgresGenerator(HsOfficeBankAccountEntity.hsOfficeBankAccount()).toString(), + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java new file mode 100644 index 00000000..30953c7b --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java @@ -0,0 +1,238 @@ +package net.hostsharing.hsadminng.rbac.rbacdef; + +import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacPermissionDefinition; +import org.apache.commons.lang3.StringUtils; + +import jakarta.persistence.Table; +import java.util.Set; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.joining; +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 org.apache.commons.lang3.StringUtils.capitalize; +import static org.apache.commons.lang3.StringUtils.uncapitalize; + +class RolesGrantsAndPermissionsGenerator { + + private final RbacView rbacDef; + private final String liquibaseTagPrefix; + private final Class entityClass; + private final String simpleEntityName; + private final String simpleEntityVarName; + private final String rawTableName; + + RolesGrantsAndPermissionsGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) { + this.rbacDef = rbacDef; + this.liquibaseTagPrefix = liquibaseTagPrefix; + + entityClass = rbacDef.getEntityAlias().entityClass(); + simpleEntityName = withoutEntitySuffix(entityClass.getSimpleName()); + simpleEntityVarName = uncapitalize(simpleEntityName); + rawTableName = withoutRvSuffix(entityClass.getAnnotation(Table.class).name()); + } + + void generateTo(final StringBuilder plPgSql) { + generateHeader(plPgSql); + generateTriggerFunction(plPgSql); + generageInsertTrigger(plPgSql); + generateFooter(plPgSql); + } + + private void generateHeader(final StringBuilder plPgSql) { + plPgSql.append(""" + -- ============================================================================ + --changeset %{liquibaseTagPrefix}-rbac-CREATE-ROLES-GRANTS-PERMISSIONS:1 endDelimiter:--// + -- ---------------------------------------------------------------------------- + + """ + .replace("%{liquibaseTagPrefix}", liquibaseTagPrefix)); + } + + private void generateTriggerFunction(final StringBuilder plPgSql) { + plPgSql.append(""" + /* + Creates the roles, grants and permission for the AFTER INSERT TRIGGER. + */ + + create or replace function createRbacRolesFor%{simpleEntityName}() + returns trigger + language plpgsql + strict as $$ + begin + if TG_OP <> 'INSERT' then + raise exception 'invalid usage of TRIGGER AFTER INSERT function'; + end if; + %{createRolesWithGrantsSql} + return NEW; + end; $$; + """ + .replace("%{simpleEntityName}", simpleEntityName) + .replace("%{createRolesWithGrantsSql}", createRolesWithGrantsSql()) + ); + + } + + private String createRolesWithGrantsSql() { + final var plPgSql = new StringBuilder(); + createRolesWithGrantsSql(plPgSql, OWNER); + createRolesWithGrantsSql(plPgSql, ADMIN); + createRolesWithGrantsSql(plPgSql, AGENT); + createRolesWithGrantsSql(plPgSql, TENANT); + createRolesWithGrantsSql(plPgSql, REFERRER); + return plPgSql.toString(); + } + + private void createRolesWithGrantsSql(final StringBuilder plPgSql, final RbacView.Role role) { + + final var isToCreate = rbacDef.getRoleDefs().stream() + .filter(roleDef -> rbacDef.isMainEntityAlias(roleDef.getEntityAlias()) && roleDef.getRole() == role ) + .findFirst().map(RbacView.RbacRoleDefinition::isToCreate).orElse(false); + if (!isToCreate) { + return; + } + + plPgSql.append(""" + + perform createRoleWithGrants( + %{simpleEntityVarName)%{roleSuffix}(NEW), + """ + .replace("%{simpleEntityVarName)", simpleEntityVarName) + .replace("%{roleSuffix}", capitalize(role.roleName()))); + + final var permissionsForRole = findPermissionsGrantsForRole(rbacDef.getEntityAlias(), role); + if (!permissionsForRole.isEmpty()) { + final var permissionsForRoleInPlPgSql = permissionsForRole.stream() + .map(RbacPermissionDefinition::getPermission) + .map(RbacView.Permission::permission) + .map(p -> "'" + p + "'") + .collect(joining(", ")); + plPgSql.append(indent(3) + "permissions => array[" + permissionsForRoleInPlPgSql + "],\n"); + } + + final var grantsToUsers = findGrantsToUserForRole(rbacDef.getEntityAlias(), role); + if (!grantsToUsers.isEmpty()) { + final var grantsToUsersPlPgSql = grantsToUsers.stream() + .map(u -> toPlPgSqlReference(u)) + .collect(joining(", ")); + plPgSql.append(indent(3) + "userUuids => array[" + grantsToUsersPlPgSql + "],\n"); + } + + final var incomingGrants = findIncomingSuperRolesForRole(rbacDef.getEntityAlias(), role); + if (!incomingGrants.isEmpty()) { + final var incomingGrantsInPlPgSql = incomingGrants.stream() + .map(RbacView.RbacGrantDefinition::getSuperRoleDef) + .map(r -> toPlPgSqlReference(NEW, r)) + .collect(joining(", ")); + plPgSql.append(indent(3) + "incomingSuperRoles => array[" + incomingGrantsInPlPgSql + "],\n"); + } + + final var outgoingGrants = findOutgoingSuperRolesForRole(rbacDef.getEntityAlias(), role); + if (!outgoingGrants.isEmpty()) { + final var outgoingGrantsInPlPgSql = outgoingGrants.stream() + .map(RbacView.RbacGrantDefinition::getSuperRoleDef) + .map(r -> toPlPgSqlReference(NEW, r)) + .collect(joining(", ")); + plPgSql.append(indent(3) + "outgoingSubRoles => array[" + outgoingGrantsInPlPgSql + "],\n"); + } + + chopTail(plPgSql, ",\n"); + plPgSql.append("\n" + indent(2) + ");\n"); + } + + private Set findPermissionsGrantsForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { + final var roleDef = rbacDef.findRbacRole(entityAlias, role); + return rbacDef.getGrantDefs().stream() + .filter(g -> g.grantType() == ROLE_TO_PERM && g.getSuperRoleDef()==roleDef ) + .map(RbacView.RbacGrantDefinition::getPermDef) + .collect(Collectors.toSet()); + } + + private Set findGrantsToUserForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { + final var roleDef = rbacDef.findRbacRole(entityAlias, role); + return rbacDef.getGrantDefs().stream() + .filter(g -> g.grantType() == USER_TO_ROLE && g.getSubRoleDef() == roleDef ) + .map(RbacView.RbacGrantDefinition::getUserDef) + .collect(Collectors.toSet()); + } + + private Set findIncomingSuperRolesForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { + final var roleDef = rbacDef.findRbacRole(entityAlias, role); + return rbacDef.getGrantDefs().stream() + .filter(g -> g.grantType() == ROLE_TO_ROLE && g.getSubRoleDef()==roleDef ) + .collect(Collectors.toSet()); + } + + private Set findOutgoingSuperRolesForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { + final var roleDef = rbacDef.findRbacRole(entityAlias, role); + return rbacDef.getGrantDefs().stream() + .filter(g -> g.grantType() == ROLE_TO_ROLE && g.getSuperRoleDef()==roleDef ) + .filter(g -> g.getSubRoleDef().getEntityAlias() != entityAlias) + .collect(Collectors.toSet()); + } + + private void generageInsertTrigger(final StringBuilder plPgSql) { + plPgSql.append(""" + /* + An AFTER INSERT TRIGGER which creates the role structure for a new %{simpleEntityName} + */ + + create trigger createRbacRolesFor%{simpleEntityName}_Trigger + after insert + on %{rawTableName} + for each row + execute procedure createRbacRolesFor%{simpleEntityName}(); + --// + """ + .replace("%{simpleEntityName}", simpleEntityName) + .replace("%{rawTableName}", rawTableName) + ); + } + + private static void generateFooter(final StringBuilder plPgSql) { + plPgSql.append("\n\n"); + } + + private String withoutRvSuffix(final String tableName) { + return tableName.substring(0, tableName.length()-"_rv".length()); + } + + private String withoutEntitySuffix(final String simpleEntityName) { + return simpleEntityName.substring(0, simpleEntityName.length()-"Entity".length()); + } + + private String toPlPgSqlReference(final RbacView.RbacUserReference userRef) { + return switch (userRef.role) { + case CREATOR -> "currentUserUuid()"; + default -> throw new IllegalArgumentException("unknown user role: " + userRef); + }; + } + + private String toPlPgSqlReference(final PostgresTriggerReference triggerRef, final RbacView.RbacRoleDefinition roleDef) { + return toSimpleVarName(roleDef.getEntityAlias()) + StringUtils.capitalize(roleDef.getRole().roleName()) + + ( roleDef.getEntityAlias().isGlobal() ? "()" + : rbacDef.isMainEntityAlias(roleDef.getEntityAlias()) ? ("("+triggerRef.name()+")") + : "(" + toTriggerReference(triggerRef, roleDef.getEntityAlias()) + ")"); + } + + private static String toTriggerReference(final PostgresTriggerReference triggerRef, final RbacView.EntityAlias entityAlias) { + return triggerRef.name().toLowerCase() + StringUtils.capitalize(entityAlias.aliasName()); + } + + private String toSimpleVarName(final RbacView.EntityAlias entityAlias) { + return entityAlias.isGlobal() + ? entityAlias.aliasName() + : uncapitalize(withoutEntitySuffix(entityAlias.entityClass().getSimpleName())); + } + + private String indent(final int tabs) { + return " ".repeat(4*tabs); + } + + private void chopTail(final StringBuilder plPgSql, final String tail) { + if (plPgSql.toString().endsWith(tail)) { + plPgSql.setLength(plPgSql.length() - tail.length()); + } + } +} -- 2.39.5 From 54cff5ece9f70dd6af1411f8d7d9a9870f64e5cf Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 23 Feb 2024 12:31:08 +0100 Subject: [PATCH 05/53] check for unused grants --- .../RolesGrantsAndPermissionsGenerator.java | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) 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 30953c7b..50175146 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java @@ -4,6 +4,7 @@ import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacPermissionDefinition; import org.apache.commons.lang3.StringUtils; import jakarta.persistence.Table; +import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; @@ -17,6 +18,7 @@ import static org.apache.commons.lang3.StringUtils.uncapitalize; class RolesGrantsAndPermissionsGenerator { private final RbacView rbacDef; + private final Set rbacGrants = new HashSet<>(); private final String liquibaseTagPrefix; private final Class entityClass; private final String simpleEntityName; @@ -25,6 +27,7 @@ class RolesGrantsAndPermissionsGenerator { RolesGrantsAndPermissionsGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) { this.rbacDef = rbacDef; + this.rbacGrants.addAll(rbacGrants); this.liquibaseTagPrefix = liquibaseTagPrefix; entityClass = rbacDef.getEntityAlias().entityClass(); @@ -101,22 +104,26 @@ class RolesGrantsAndPermissionsGenerator { .replace("%{simpleEntityVarName)", simpleEntityVarName) .replace("%{roleSuffix}", capitalize(role.roleName()))); - final var permissionsForRole = findPermissionsGrantsForRole(rbacDef.getEntityAlias(), role); - if (!permissionsForRole.isEmpty()) { - final var permissionsForRoleInPlPgSql = permissionsForRole.stream() + final var permissionGrantsForRole = findPermissionsGrantsForRole(rbacDef.getEntityAlias(), 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.append(indent(3) + "permissions => array[" + permissionsForRoleInPlPgSql + "],\n"); + rbacGrants.removeAll(permissionGrantsForRole); } final var grantsToUsers = findGrantsToUserForRole(rbacDef.getEntityAlias(), role); if (!grantsToUsers.isEmpty()) { final var grantsToUsersPlPgSql = grantsToUsers.stream() - .map(u -> toPlPgSqlReference(u)) + .map(RbacView.RbacGrantDefinition::getUserDef) + .map(this::toPlPgSqlReference) .collect(joining(", ")); plPgSql.append(indent(3) + "userUuids => array[" + grantsToUsersPlPgSql + "],\n"); + rbacGrants.removeAll(grantsToUsers); } final var incomingGrants = findIncomingSuperRolesForRole(rbacDef.getEntityAlias(), role); @@ -126,6 +133,7 @@ class RolesGrantsAndPermissionsGenerator { .map(r -> toPlPgSqlReference(NEW, r)) .collect(joining(", ")); plPgSql.append(indent(3) + "incomingSuperRoles => array[" + incomingGrantsInPlPgSql + "],\n"); + rbacGrants.removeAll(incomingGrants); } final var outgoingGrants = findOutgoingSuperRolesForRole(rbacDef.getEntityAlias(), role); @@ -135,38 +143,41 @@ class RolesGrantsAndPermissionsGenerator { .map(r -> toPlPgSqlReference(NEW, r)) .collect(joining(", ")); plPgSql.append(indent(3) + "outgoingSubRoles => array[" + outgoingGrantsInPlPgSql + "],\n"); + rbacGrants.removeAll(outgoingGrants); + } + + if (!rbacGrants.isEmpty()) { + throw new IllegalStateException("unprocessed grants: " + rbacGrants); } chopTail(plPgSql, ",\n"); plPgSql.append("\n" + indent(2) + ");\n"); } - private Set findPermissionsGrantsForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { + private Set findPermissionsGrantsForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { final var roleDef = rbacDef.findRbacRole(entityAlias, role); - return rbacDef.getGrantDefs().stream() + return rbacGrants.stream() .filter(g -> g.grantType() == ROLE_TO_PERM && g.getSuperRoleDef()==roleDef ) - .map(RbacView.RbacGrantDefinition::getPermDef) .collect(Collectors.toSet()); } - private Set findGrantsToUserForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { + private Set findGrantsToUserForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { final var roleDef = rbacDef.findRbacRole(entityAlias, role); - return rbacDef.getGrantDefs().stream() + return rbacGrants.stream() .filter(g -> g.grantType() == USER_TO_ROLE && g.getSubRoleDef() == roleDef ) - .map(RbacView.RbacGrantDefinition::getUserDef) .collect(Collectors.toSet()); } private Set findIncomingSuperRolesForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { final var roleDef = rbacDef.findRbacRole(entityAlias, role); - return rbacDef.getGrantDefs().stream() + return rbacGrants.stream() .filter(g -> g.grantType() == ROLE_TO_ROLE && g.getSubRoleDef()==roleDef ) .collect(Collectors.toSet()); } private Set findOutgoingSuperRolesForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { final var roleDef = rbacDef.findRbacRole(entityAlias, role); - return rbacDef.getGrantDefs().stream() + return rbacGrants.stream() .filter(g -> g.grantType() == ROLE_TO_ROLE && g.getSuperRoleDef()==roleDef ) .filter(g -> g.getSubRoleDef().getEntityAlias() != entityAlias) .collect(Collectors.toSet()); -- 2.39.5 From 3e2fa5a6f6f64f324edf5e4c57eada8a3fcffd42 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 23 Feb 2024 16:09:10 +0100 Subject: [PATCH 06/53] rekursive Entity-Imports and render complex Mermad-Flowcharts (example: Debitor with parterRel+personRel and holderPerspn+anchorPerson each) --- .../HsOfficeBankAccountEntity.java | 71 +------------ .../office/debitor/HsOfficeDebitorEntity.java | 74 +++++++++++++ .../office/person/HsOfficePersonEntity.java | 23 ++++ .../HsOfficeRelationshipEntity.java | 38 +++++++ .../hsadminng/rbac/rbacdef/RbacView.java | 100 ++++++++++++------ .../rbacdef/RbacViewMermaidFlowchart.java | 48 ++++----- .../rbacdef/RbacViewPostgresGenerator.java | 2 +- 7 files changed, 231 insertions(+), 125 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 351a5f96..40b0a3d2 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 @@ -3,8 +3,6 @@ package net.hostsharing.hsadminng.hs.office.bankaccount; import lombok.*; import lombok.experimental.FieldNameConstants; import net.hostsharing.hsadminng.errors.DisplayName; -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; @@ -17,10 +15,8 @@ import jakarta.persistence.Table; import java.util.UUID; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.*; -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.stringify.Stringify.stringify; @Entity @@ -59,7 +55,7 @@ public class HsOfficeBankAccountEntity implements HasUuid, Stringifyable { return holder; } - public static RbacView hsOfficeBankAccount() { + public static RbacView rbac() { // @formatter:off return rbacViewFor("bankAccount", HsOfficeBankAccountEntity.class) .withIdentityView(SQL.query("target.iban || ':' || target.holder")) @@ -75,69 +71,4 @@ public class HsOfficeBankAccountEntity implements HasUuid, Stringifyable { .pop(); // @formatter:on } - - public static RbacView hsOfficeDebitor() { - // @formatter:off - return rbacViewFor("debitor", HsOfficeDebitorEntity.class) - .withIdentityView(SQL.query(""" - SELECT debitor.uuid, - 'D-' || (SELECT partner.partnerNumber - FROM hs_office_partner partner - JOIN hs_office_relationship partnerRel - ON partnerRel.uuid = partner.partnerRoleUUid AND partnerRel.relType = 'PARTNER' - JOIN hs_office_relationship debitorRel - ON debitorRel.relAnchorUuid = partnerRel.relHolderUuid AND partnerRel.relType = 'ACCOUNTING' - WHERE debitorRel.uuid = debitor.debitorRelUuid) - || to_char(debitorNumberSuffix, 'fm00') - from hs_office_debitor as debitor; - """)) - .withUpdatableColumns( - "debitorRel", - "billable", - "billingContactUuid", - "refundBankAccountUuid", - "vatId", - "vatCountryCode", - "vatBusiness", - "vatreversecharge", - "defaultPrefix" /* TODO: do we want that updatable? */ ) - .createPermission(custom("new-debitor")).grantedTo("global", ADMIN).pop() - - .defineProxyEntityAlias("debitorRel", HsOfficeRelationshipEntity.class, fetchedBySql(""" - SELECT * - FROM hs_office_relationship AS r - 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() - - .defineEntityAlias("refundBankAccount", HsOfficeBankAccountEntity.class, fetchedBySql(""" - SELECT * - FROM hs_office_relationship AS r - WHERE r.relType = 'ACCOUNTING' AND r.relHolderUuid = ${REF}.debitorRelUuid; - """), - dependsOnColumn("bankAccountUuid")) - .importRbacViewAs("refundBankAccount", HsOfficeBankAccountEntity.hsOfficeBankAccount()) - .toRole("refundBankAccount", ADMIN).grantRole("debitorRel", AGENT) - .toRole("debitorRel", AGENT).grantRole("refundBankAccount", REFERRER) - - .defineEntityAlias("partnerRel", HsOfficeRelationshipEntity.class, 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) - .declareEntityAliases("partnerPerson", "operationalPerson") - .forExampleRole("partnerPerson", ADMIN).wouldBeGrantedTo("partnerRel", ADMIN) - .forExampleRole("operationalPerson", ADMIN).wouldBeGrantedTo("partnerRel", ADMIN) - .forExampleRole("partnerRel", TENANT).wouldBeGrantedTo("partnerPerson", REFERRER); - - - // @formatter:on - } } 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 76480ac0..d4dbecda 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 @@ -4,8 +4,10 @@ import lombok.*; import net.hostsharing.hsadminng.errors.DisplayName; import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity; +import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity; import net.hostsharing.hsadminng.persistence.HasUuid; import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity; +import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; import org.hibernate.annotations.GenericGenerator; @@ -14,6 +16,13 @@ import jakarta.persistence.*; import java.util.Optional; 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.Permission.VIEW; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.REFERRER; +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 @@ -97,4 +106,69 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable { public String toShortString() { return DEBITOR_NUMBER_TAG + getDebitorNumberString(); } + + public static RbacView rbac() { + // @formatter:off + return rbacViewFor("debitor", HsOfficeDebitorEntity.class) + .withIdentityView(RbacView.SQL.query(""" + SELECT debitor.uuid, + 'D-' || (SELECT partner.partnerNumber + FROM hs_office_partner partner + JOIN hs_office_relationship partnerRel + ON partnerRel.uuid = partner.partnerRoleUUid AND partnerRel.relType = 'PARTNER' + JOIN hs_office_relationship debitorRel + ON debitorRel.relAnchorUuid = partnerRel.relHolderUuid AND partnerRel.relType = 'ACCOUNTING' + WHERE debitorRel.uuid = debitor.debitorRelUuid) + || to_char(debitorNumberSuffix, 'fm00') + from hs_office_debitor as debitor; + """)) + .withUpdatableColumns( + "debitorRel", + "billable", + "billingContactUuid", + "refundBankAccountUuid", + "vatId", + "vatCountryCode", + "vatBusiness", + "vatReverseCharge", + "defaultPrefix" /* TODO: do we want that updatable? */ ) + .createPermission(custom("new-debitor")).grantedTo("global", ADMIN).pop() + + .importProxyEntity("debitorRel", HsOfficeRelationshipEntity.class, + fetchedBySql(""" + SELECT * + FROM hs_office_relationship AS r + 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() + + .importEntityAlias("refundBankAccount", HsOfficeBankAccountEntity.class, + 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(""" + 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) + .declarePlaceholderEntityAliases("partnerPerson", "operationalPerson") + .forExampleRole("partnerPerson", ADMIN).wouldBeGrantedTo("partnerRel", ADMIN) + .forExampleRole("operationalPerson", ADMIN).wouldBeGrantedTo("partnerRel", ADMIN) + .forExampleRole("partnerRel", TENANT).wouldBeGrantedTo("partnerPerson", REFERRER); + // @formatter:on + } } 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 fde3972b..ad3cfc02 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 @@ -4,6 +4,7 @@ import lombok.*; 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.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; import org.apache.commons.lang3.StringUtils; @@ -11,6 +12,11 @@ import org.apache.commons.lang3.StringUtils; import jakarta.persistence.*; import java.util.UUID; +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.Role.*; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.query; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.stringify.Stringify.stringify; @Entity @@ -56,4 +62,21 @@ public class HsOfficePersonEntity implements HasUuid, Stringifyable { return personType + " " + (!StringUtils.isEmpty(tradeName) ? tradeName : (familyName + ", " + givenName)); } + + public static RbacView rbac() { + // @formatter:off + return rbacViewFor("person", HsOfficePersonEntity.class) + .withIdentityView(query("concat(target.tradeName, target.familyName, target.givenName)")) + .withUpdatableColumns("personType", "tradeName", "givenName", "familyName") + .createRole(OWNER) + .withPermission(ALL) + .withCurrentUserAsOwner() + .withIncomingSuperRole(GLOBAL, ADMIN) + .createSubRole(ADMIN) + .withPermission(EDIT) + .createSubRole(REFERRER) + .withPermission(VIEW) + .pop(); + // @formatter:on + } } 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 704f2760..9a94279b 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 @@ -5,12 +5,20 @@ import lombok.experimental.FieldNameConstants; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity; import net.hostsharing.hsadminng.persistence.HasUuid; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; +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 jakarta.persistence.*; import java.util.UUID; +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.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 @@ -67,4 +75,34 @@ public class HsOfficeRelationshipEntity implements HasUuid, Stringifyable { public String toShortString() { return toShortString.apply(this); } + + public static RbacView rbac() { + // @formatter:off + return rbacViewFor("relationship", HsOfficeRelationshipEntity.class) + .withIdentityView(SQL.query(""" + (select idName from hs_office_person_iv p where p.uuid = target.relAnchorUuid) + || '-with-' || target.relType || '-' + || (select idName from hs_office_person_iv p where p.uuid = target.relHolderUuid) + """)) + .withUpdatableColumns("contactUuid") + .importEntityAlias("anchorPerson", HsOfficePersonEntity.class, + fetchedBySql("select * from hs_office_person as p where p.uuid = ${REF}.relAnchorUuid"), + dependsOnColumn("relAnchorUuid")) + .importEntityAlias("holderPerson", HsOfficePersonEntity.class, + fetchedBySql("select * from hs_office_person as p where p.uuid = ${REF}.relHolderUuid"), + dependsOnColumn("relHolderUuid")) + .createRole(OWNER) + .withCurrentUserAsOwner() + .withPermission(ALL) + .withIncomingSuperRole(GLOBAL, ADMIN) + .withIncomingSuperRole("anchorPerson", ADMIN) + .createSubRole(ADMIN) + .withPermission(EDIT) + .createSubRole(AGENT) + .withIncomingSuperRole("holderPerson", ADMIN) + .createSubRole(TENANT) + .withPermission(VIEW) + .pop(); + // @formatter:on + } } 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 1997696d..df377ab6 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -6,6 +6,7 @@ import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEnti import net.hostsharing.hsadminng.persistence.HasUuid; import jakarta.validation.constraints.NotNull; +import java.lang.reflect.InvocationTargetException; import java.util.*; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR; @@ -36,11 +37,11 @@ public class RbacView { private SQL identityViewSqlQuery; private EntityAlias entityAliasProxy; - public static RbacView rbacViewFor(final String alias, final Class entityClass) { + public static RbacView rbacViewFor(final String alias, final Class entityClass) { return new RbacView(alias, entityClass); } - RbacView(final String alias, final Class entityClass) { + RbacView(final String alias, final Class entityClass) { entityAlias = new EntityAlias(alias, entityClass); entityAliases.put(alias, entityAlias); new RbacUserReference(CREATOR); @@ -71,31 +72,53 @@ public class RbacView { return permDef; } - public RbacView declareEntityAliases(final String... aliasNames) { + public RbacView declarePlaceholderEntityAliases(final String... aliasNames) { for ( String alias: aliasNames ) { entityAliases.put(alias, new EntityAlias(alias)); } return this; } - public RbacView defineProxyEntityAlias( + public RbacView importProxyEntity( final String aliasName, final Class entityClass, final SQL fetchSql, final Column dependsOnColum) { - entityAliasProxy = new EntityAlias(aliasName, entityClass, fetchSql, dependsOnColum); - entityAliases.put(aliasName, entityAliasProxy); + if ( entityAliasProxy != null ) { + throw new IllegalStateException("there is already an entityAliasProxy: " + entityAliasProxy); + } + entityAliasProxy = importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum); return this; } - public RbacView defineEntityAlias( + public RbacView importEntityAlias( final String aliasName, final Class entityClass, final SQL fetchSql, final Column dependsOnColum) { - entityAliases.put(aliasName, new EntityAlias(aliasName, entityClass, fetchSql, dependsOnColum)); + importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum); return this; } - public RbacView importRbacViewAs(final String aliasName, final RbacView importedRbacView) { + private EntityAlias importEntityAliasImpl(final String aliasName, final Class entityClass, final SQL fetchSql, final Column dependsOnColum) { + final var entityAlias = new EntityAlias(aliasName, entityClass, fetchSql, dependsOnColum); + entityAliases.put(aliasName, entityAlias); + try { + importAsAlias(aliasName, rbacDefinition(entityClass)); + } catch ( final Exception exc) { + new RuntimeException("cannot import entity: " + entityClass, exc); + } + return entityAlias; + } + + private static RbacView rbacDefinition(final Class entityClass) + throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { + return (RbacView) entityClass.getMethod("rbac").invoke(null); + } + + private RbacView importAsAlias(final String aliasName, final RbacView importedRbacView) { final var mapper = new AliasNameMapper(importedRbacView, aliasName); - importedRbacView.getEntityAliases().values().forEach(entityAlias -> { - new EntityAlias( mapper.map(entityAlias.aliasName), entityAlias.entityClass); - }); + importedRbacView.getEntityAliases().values().stream() + .filter(entityAlias -> !importedRbacView.isMainEntityAlias(entityAlias)) + .filter(entityAlias -> !entityAlias.isGlobal()) + .forEach(entityAlias -> { + final String mappedAliasName = mapper.map(entityAlias.aliasName); + entityAliases.put(mappedAliasName, new EntityAlias(mappedAliasName, entityAlias.entityClass)); + }); importedRbacView.getRoleDefs().forEach(roleDef -> { new RbacRoleDefinition( findEntityAlias(mapper.map(roleDef.entityAlias.aliasName)), roleDef.role); }); @@ -120,15 +143,15 @@ public class RbacView { private RbacGrantDefinition grantRoleToCurrentUser(final RbacRoleDefinition roleDefinition) { - return new RbacGrantDefinition(roleDefinition, currentUser()); + return new RbacGrantDefinition(roleDefinition, currentUser()).toCreate(); } private RbacGrantDefinition grantPermissionToRole(final RbacPermissionDefinition permDef , final RbacRoleDefinition roleDef) { - return new RbacGrantDefinition(permDef, roleDef); + return new RbacGrantDefinition(permDef, roleDef).toCreate(); } private RbacGrantDefinition grantSubRoleToSuperRole(final RbacRoleDefinition subRoleDefinition, final RbacRoleDefinition superRoleDefinition) { - return new RbacGrantDefinition(subRoleDefinition, superRoleDefinition); + return new RbacGrantDefinition(subRoleDefinition, superRoleDefinition).toCreate(); } boolean isMainEntityAlias(final EntityAlias entityAlias) { @@ -148,7 +171,7 @@ public class RbacView { } public RbacView grantRole(final String entityAlias, final Role role) { - new RbacGrantDefinition(findRbacRole(entityAlias, role), superRoleDef); + new RbacGrantDefinition(findRbacRole(entityAlias, role), superRoleDef).toCreate(); return RbacView.this; } @@ -161,6 +184,7 @@ public class RbacView { private final RbacRoleDefinition superRoleDef; private final RbacRoleDefinition subRoleDef; private final RbacPermissionDefinition permDef; + private boolean toCreate; @Override public String toString() { @@ -203,7 +227,16 @@ public class RbacView { boolean isAssumed() { // TODO: not implemented yet - return false; + return true; + } + + boolean isToCreate() { + return toCreate; + } + + RbacGrantDefinition toCreate() { + toCreate = true; + return this; } public enum GrantType { @@ -262,7 +295,7 @@ public class RbacView { } public RbacPermissionDefinition grantedTo(final String entityAlias, final Role role) { - new RbacGrantDefinition(this, findRbacRole(entityAlias, role) ); + new RbacGrantDefinition(this, findRbacRole(entityAlias, role) ).toCreate(); return this; } @@ -309,7 +342,7 @@ public class RbacView { public RbacRoleDefinition createSubRole(final Role role) { final var roleDef = findRbacRole(entityAlias, role).toCreate(); - new RbacGrantDefinition(roleDef, this); + new RbacGrantDefinition(roleDef, this).toCreate(); return roleDef; } @@ -346,7 +379,7 @@ public class RbacView { } } - private EntityAlias findEntityAlias(final String aliasName) { + EntityAlias findEntityAlias(final String aliasName) { final var found = entityAliases.get(aliasName); if ( found == null ) { throw new IllegalArgumentException("entityAlias not found: " + aliasName); @@ -354,7 +387,7 @@ public class RbacView { return found; } - public RbacRoleDefinition findRbacRole(final EntityAlias entityAlias, final Role role) { + RbacRoleDefinition findRbacRole(final EntityAlias entityAlias, final Role role) { return roleDefs.stream() .filter(r -> r.getEntityAlias() == entityAlias && r.getRole().equals(role)) .findFirst() @@ -367,17 +400,21 @@ public class RbacView { record EntityAlias(String aliasName, Class entityClass, SQL fetchSql, Column dependsOnColum) { - public EntityAlias(final String aliasName) { - this(aliasName, null, null, null); - } + public EntityAlias(final String aliasName) { + this(aliasName, null, null, null); + } - public EntityAlias(final String aliasName, final Class entityClass) { - this(aliasName, entityClass, null, null); - } + public EntityAlias(final String aliasName, final Class entityClass) { + this(aliasName, entityClass, null, null); + } boolean isGlobal() { return aliasName().equals("global"); } + + boolean isPlaceholder() { + return entityClass == null; + } } public record Role(String roleName) { @@ -389,7 +426,7 @@ public class RbacView { @Override public String toString() { - return "." + roleName; + return ":" + roleName; } @Override @@ -409,7 +446,7 @@ public class RbacView { @Override public String toString() { - return "." + permission; + return ":" + permission; } } @@ -457,7 +494,10 @@ public class RbacView { if (originalAliasName.equals(importedRbacView.entityAlias.aliasName) ) { return outerAliasName; } - return originalAliasName; + 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 828c45ef..8a6e6ff7 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java @@ -1,6 +1,6 @@ package net.hostsharing.hsadminng.rbac.rbacdef; -import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity; +import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; import org.apache.commons.lang3.StringUtils; import java.io.IOException; @@ -11,7 +11,8 @@ import static java.util.stream.Collectors.joining; public class RbacViewMermaidFlowchart { - + public static final String HOSTSHARING_ORANGE = "#dd4901"; + public static final String HOSTSHARING_LIGHTBLUE = "#99bcdb"; private final RbacView rbacDef; private final StringBuilder flowchart = new StringBuilder(); @@ -21,38 +22,28 @@ public class RbacViewMermaidFlowchart { ### rbac %{entityAlias} %{timestamp} ```mermaid + %%{init:{'flowchart':{'htmlLabels':false}}}%% flowchart TB """ .replace("%{entityAlias}", rbacDef.getEntityAlias().aliasName()) .replace("%{timestamp}", LocalDateTime.now().toString())); - renderSubgraphGlobal(); renderEntitySubgraphs(); renderGrants(); flowchart.append("```"); } - - void renderSubgraphGlobal() { - flowchart.append(""" - subgraph global - style global fill: lightgray - - role:global.admin[global.admin] - end - """); - } - private void renderEntitySubgraphs() { rbacDef.getEntityAliases().values().stream() .filter(entityAlias -> !rbacDef.isEntityAliasProxy(entityAlias)) + .filter(entityAlias -> !entityAlias.isPlaceholder()) .forEach(this::renderEntitySubgraph); } private void renderEntitySubgraph(final RbacView.EntityAlias entity) { - final var color = rbacDef.isMainEntityAlias(entity) ? "lightgreen" : "lightgray"; + final var color = rbacDef.isMainEntityAlias(entity) ? HOSTSHARING_ORANGE : HOSTSHARING_LIGHTBLUE; flowchart.append(""" - subgraph %{aliasName} + subgraph %{aliasName}["`**%{aliasName}**`"] direction TB style %{aliasName} fill: %{color} @@ -98,13 +89,16 @@ public class RbacViewMermaidFlowchart { } private String grantDef(final RbacView.RbacGrantDefinition grant) { + final var arrow = grant.isToCreate() + ? grant.isAssumed() ? " ==> " : " == // ==> " + : grant.isAssumed() ? " -.-> " : " -.- // -.-> "; return switch (grant.grantType()) { case USER_TO_ROLE -> // TODO: other user types not implemented yet - "user:creator" + (grant.isAssumed() ? " -.-> " : " --> ") + roleId(grant.getSubRoleDef()); + "user:creator" + arrow + roleId(grant.getSubRoleDef()); case ROLE_TO_ROLE -> - roleId(grant.getSuperRoleDef()) + (grant.isAssumed() ? " -.-> " : " --> ") + roleId(grant.getSubRoleDef()); - case ROLE_TO_PERM -> roleId(grant.getSuperRoleDef()) + " --> " + permId(grant.getPermDef()); + roleId(grant.getSuperRoleDef()) + arrow + roleId(grant.getSubRoleDef()); + case ROLE_TO_PERM -> roleId(grant.getSuperRoleDef()) + arrow + permId(grant.getPermDef()); }; } @@ -131,14 +125,20 @@ public class RbacViewMermaidFlowchart { public static void main(String[] args) throws IOException { - Files.writeString( - Paths.get("doc", "hsOfficeBankAccount.md"), - new RbacViewMermaidFlowchart(HsOfficeBankAccountEntity.hsOfficeBankAccount()).toString(), - StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); +// Files.writeString( +// Paths.get("doc", "hsOfficeRelationship.md"), +// new RbacViewMermaidFlowchart(HsOfficeRelationshipEntity.rbac()).toString(), +// StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); +// +// +// Files.writeString( +// Paths.get("doc", "hsOfficeBankAccount.md"), +// new RbacViewMermaidFlowchart(HsOfficeBankAccountEntity.rbac()).toString(), +// StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); Files.writeString( Paths.get("doc", "hsOfficeDebitor.md"), - new RbacViewMermaidFlowchart(HsOfficeBankAccountEntity.hsOfficeDebitor()).toString(), + new RbacViewMermaidFlowchart(HsOfficeDebitorEntity.rbac()).toString(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); } } 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 862c3458..6079ba3b 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java @@ -41,7 +41,7 @@ public static void main(String[] args) throws IOException { Files.writeString( Paths.get("doc", "hsOfficeBankAccount.sql"), - new RbacViewPostgresGenerator(HsOfficeBankAccountEntity.hsOfficeBankAccount()).toString(), + new RbacViewPostgresGenerator(HsOfficeBankAccountEntity.rbac()).toString(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); } } -- 2.39.5 From fc1cc5815fa61cec743923a299510b269177a07c Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sun, 25 Feb 2024 07:15:15 +0100 Subject: [PATCH 07/53] introduce RbacObject and initial test for RbacViewMermaidFlowchart --- .../errors/ReferenceNotFoundException.java | 4 +- .../hsadminng/persistence/HasUuid.java | 6 +-- .../hsadminng/rbac/rbacdef/RbacView.java | 16 +++--- .../rbacdef/RbacViewMermaidFlowchart.java | 25 +++++---- .../hsadminng/rbac/rbacobject/RbacObject.java | 8 +++ .../test/cust/TestCustomerEntity.java | 28 +++++++++- .../hs/office/migration/ImportOfficeData.java | 6 +-- .../test/ContextBasedTestWithCleanup.java | 5 +- .../test/cust/TestCustomerEntityTest.java | 51 +++++++++++++++++++ 9 files changed, 120 insertions(+), 29 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/rbac/rbacobject/RbacObject.java create mode 100644 src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityTest.java diff --git a/src/main/java/net/hostsharing/hsadminng/errors/ReferenceNotFoundException.java b/src/main/java/net/hostsharing/hsadminng/errors/ReferenceNotFoundException.java index e20d1357..deeae9f8 100644 --- a/src/main/java/net/hostsharing/hsadminng/errors/ReferenceNotFoundException.java +++ b/src/main/java/net/hostsharing/hsadminng/errors/ReferenceNotFoundException.java @@ -1,6 +1,6 @@ package net.hostsharing.hsadminng.errors; -import net.hostsharing.hsadminng.persistence.HasUuid; +import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import java.util.UUID; @@ -8,7 +8,7 @@ public class ReferenceNotFoundException extends RuntimeException { private final Class entityClass; private final UUID uuid; - public ReferenceNotFoundException(final Class entityClass, final UUID uuid, final Throwable exc) { + public ReferenceNotFoundException(final Class entityClass, final UUID uuid, final Throwable exc) { super(exc); this.entityClass = entityClass; this.uuid = uuid; diff --git a/src/main/java/net/hostsharing/hsadminng/persistence/HasUuid.java b/src/main/java/net/hostsharing/hsadminng/persistence/HasUuid.java index 1f3ead14..03e6abf3 100644 --- a/src/main/java/net/hostsharing/hsadminng/persistence/HasUuid.java +++ b/src/main/java/net/hostsharing/hsadminng/persistence/HasUuid.java @@ -1,7 +1,7 @@ package net.hostsharing.hsadminng.persistence; -import java.util.UUID; +import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; -public interface HasUuid { - UUID getUuid(); +// TODO: remove this interface, I just wanted to avoid to many changes in that PR +public interface HasUuid extends RbacObject { } 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 df377ab6..b89ac18b 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -3,7 +3,7 @@ package net.hostsharing.hsadminng.rbac.rbacdef; 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; import jakarta.validation.constraints.NotNull; import java.lang.reflect.InvocationTargetException; @@ -37,11 +37,11 @@ public class RbacView { private SQL identityViewSqlQuery; private EntityAlias entityAliasProxy; - public static RbacView rbacViewFor(final String alias, final Class entityClass) { + public static RbacView rbacViewFor(final String alias, final Class entityClass) { return new RbacView(alias, entityClass); } - RbacView(final String alias, final Class entityClass) { + RbacView(final String alias, final Class entityClass) { entityAlias = new EntityAlias(alias, entityClass); entityAliases.put(alias, entityAlias); new RbacUserReference(CREATOR); @@ -72,14 +72,14 @@ public class RbacView { return permDef; } - public RbacView declarePlaceholderEntityAliases(final String... aliasNames) { + public RbacView declarePlaceholderEntityAliases(final String... aliasNames) { for ( String alias: aliasNames ) { entityAliases.put(alias, new EntityAlias(alias)); } return this; } - public RbacView importProxyEntity( + public RbacView importProxyEntity( final String aliasName, final Class entityClass, final SQL fetchSql, final Column dependsOnColum) { if ( entityAliasProxy != null ) { throw new IllegalStateException("there is already an entityAliasProxy: " + entityAliasProxy); @@ -105,7 +105,7 @@ public class RbacView { return entityAlias; } - private static RbacView rbacDefinition(final Class entityClass) + private static RbacView rbacDefinition(final Class entityClass) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { return (RbacView) entityClass.getMethod("rbac").invoke(null); } @@ -398,13 +398,13 @@ public class RbacView { return findRbacRole(findEntityAlias(entityAliasName), role); } - record EntityAlias(String aliasName, Class entityClass, SQL fetchSql, Column dependsOnColum) { + record EntityAlias(String aliasName, Class entityClass, SQL fetchSql, Column dependsOnColum) { public EntityAlias(final String aliasName) { this(aliasName, null, null, null); } - public EntityAlias(final String aliasName, final Class entityClass) { + public EntityAlias(final String aliasName, final Class entityClass) { this(aliasName, entityClass, null, null); } 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 8a6e6ff7..5c63334f 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java @@ -19,15 +19,10 @@ public class RbacViewMermaidFlowchart { public RbacViewMermaidFlowchart(final RbacView rbacDef) { this.rbacDef = rbacDef; flowchart.append(""" - ### rbac %{entityAlias} %{timestamp} - - ```mermaid %%{init:{'flowchart':{'htmlLabels':false}}}%% flowchart TB - """ - .replace("%{entityAlias}", rbacDef.getEntityAlias().aliasName()) - .replace("%{timestamp}", LocalDateTime.now().toString())); + """); renderEntitySubgraphs(); renderGrants(); flowchart.append("```"); @@ -51,13 +46,13 @@ public class RbacViewMermaidFlowchart { .replace("%{aliasName}", entity.aliasName()) .replace("%{color}", color )); - wrapOutputInSubgraph(entity.aliasName() + ".roles", color, + wrapOutputInSubgraph(entity.aliasName() + ":roles", color, rbacDef.getRoleDefs().stream() .filter(r -> r.getEntityAlias() == entity) .map(r -> " " + roleDef(r)) .collect(joining("\n"))); - wrapOutputInSubgraph(entity.aliasName() + "permissions", color, + wrapOutputInSubgraph(entity.aliasName() + ":permissions", color, rbacDef.getPermDefs().stream() .filter(p -> p.getEntityAlias() == entity) .map(p -> " " + permDef(p) ) @@ -138,7 +133,17 @@ public class RbacViewMermaidFlowchart { Files.writeString( Paths.get("doc", "hsOfficeDebitor.md"), - new RbacViewMermaidFlowchart(HsOfficeDebitorEntity.rbac()).toString(), - StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + """ + ### rbac %{entityAlias} %{timestamp} + + ```mermaid + %{flowchart} + ``` + """ + .replace("%{entityAlias}", "contact") + .replace("%{timestamp}", LocalDateTime.now().toString()) + .replace("%{flowchart}", new RbacViewMermaidFlowchart(HsOfficeDebitorEntity.rbac()).toString()), + + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); } } diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacobject/RbacObject.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacobject/RbacObject.java new file mode 100644 index 00000000..4d7646d1 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacobject/RbacObject.java @@ -0,0 +1,8 @@ +package net.hostsharing.hsadminng.rbac.rbacobject; + + +import java.util.UUID; + +public interface RbacObject { + UUID getUuid(); +} 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 1f2bb0e1..3d5e6f19 100644 --- a/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java @@ -4,17 +4,24 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; +import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import jakarta.persistence.*; import java.util.UUID; +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.Role.*; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; + @Entity @Table(name = "test_customer_rv") @Getter @Setter @NoArgsConstructor @AllArgsConstructor -public class TestCustomerEntity { +public class TestCustomerEntity implements RbacObject { @Id @GeneratedValue @@ -25,4 +32,23 @@ public class TestCustomerEntity { @Column(name = "adminusername") private String adminUserName; + + + + public static RbacView rbac() { + // @formatter:off + return rbacViewFor("contact", TestCustomerEntity.class) + .withIdentityView(RbacView.SQL.query("target.prefix")) + .withUpdatableColumns("reference", "prefix", "adminUserName") + .createRole(OWNER) + .withPermission(ALL) + .withCurrentUserAsOwner() + .withIncomingSuperRole(GLOBAL, ADMIN) + .createSubRole(ADMIN) + .withPermission(RbacView.Permission.custom("add-package")) + .createSubRole(TENANT) + .withPermission(VIEW) + .pop(); + // @formatter:on + } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java b/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java index 325317b2..929aa919 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java @@ -21,7 +21,7 @@ import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType; import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity; import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipType; import net.hostsharing.hsadminng.hs.office.sepamandate.HsOfficeSepaMandateEntity; -import net.hostsharing.hsadminng.persistence.HasUuid; +import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.test.JpaAttempt; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; @@ -520,7 +520,7 @@ public class ImportOfficeData extends ContextBasedTest { } - private void persist(final Integer id, final HasUuid entity) { + private void persist(final Integer id, final RbacObject entity) { try { //System.out.println("persisting #" + entity.hashCode() + ": " + entity); em.persist(entity); @@ -591,7 +591,7 @@ public class ImportOfficeData extends ContextBasedTest { }).assertSuccessful(); } - private void updateLegacyIds( + private void updateLegacyIds( Map entities, final String legacyIdTable, final String legacyIdColumn) { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/test/ContextBasedTestWithCleanup.java b/src/test/java/net/hostsharing/hsadminng/hs/office/test/ContextBasedTestWithCleanup.java index 9b6c14ed..968e5416 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/test/ContextBasedTestWithCleanup.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/test/ContextBasedTestWithCleanup.java @@ -4,6 +4,7 @@ import net.hostsharing.hsadminng.context.ContextBasedTest; import net.hostsharing.hsadminng.persistence.HasUuid; import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantEntity; import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantRepository; +import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.rbac.rbacrole.RbacRoleEntity; import net.hostsharing.hsadminng.rbac.rbacrole.RbacRoleRepository; import net.hostsharing.test.JpaAttempt; @@ -43,7 +44,7 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest { @Autowired JpaAttempt jpaAttempt; - private TreeMap> entitiesToCleanup = new TreeMap<>(); + private TreeMap> entitiesToCleanup = new TreeMap<>(); private static Long latestIntialTestDataSerialId; private static boolean countersInitialized = false; @@ -61,7 +62,7 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest { return uuidToCleanup; } - public E toCleanup(final E entity) { + public E toCleanup(final E entity) { out.println("toCleanup(" + entity.getClass() + ", " + entity.getUuid()); if ( entity.getUuid() == null ) { throw new IllegalArgumentException("only persisted entities with valid uuid allowed"); diff --git a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityTest.java b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityTest.java new file mode 100644 index 00000000..c7f9800a --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityTest.java @@ -0,0 +1,51 @@ +package net.hostsharing.hsadminng.test.cust; + +import net.hostsharing.hsadminng.rbac.rbacdef.RbacViewMermaidFlowchart; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class TestCustomerEntityTest { + + @Test + void definesRbac() { + final var rbacFlowchart = new RbacViewMermaidFlowchart(TestCustomerEntity.rbac()).toString(); + assertThat(rbacFlowchart).isEqualToIgnoringWhitespace(""" + %%{init:{'flowchart':{'htmlLabels':false}}}%% + flowchart TB + + subgraph contact["`**contact**`"] + direction TB + style contact fill:#dd4901,stroke:darkblue,stroke-width:8px + + subgraph contact:roles[ ] + style contact:roles fill: #dd4901 + + role:contact:owner[[contact:owner]] + role:contact:admin[[contact:admin]] + role:contact:tenant[[contact:tenant]] + end + + subgraph contact:permissions[ ] + style contact:permissions fill: #dd4901 + + perm:contact:*{{contact:*}} + perm:contact:add-package{{contact:add-package}} + perm:contact:view{{contact:view}} + end + end + + role:contact:owner ==> perm:contact:* + role:contact:owner ==> perm:contact:* + user:creator ==> role:contact:owner + role:global:admin ==> role:contact:owner + role:global:admin ==> role:contact:owner + role:contact:owner ==> role:contact:admin + role:contact:admin ==> perm:contact:add-package + role:contact:admin ==> perm:contact:add-package + role:contact:admin ==> role:contact:tenant + role:contact:tenant ==> perm:contact:view + role:contact:tenant ==> perm:contact:view + """); + } +} -- 2.39.5 From f45f88ba7756a9c3405527d5fa51ec086d4a6068 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sun, 25 Feb 2024 09:28:15 +0100 Subject: [PATCH 08/53] add customer and outgoing grants to RelationshipEntity --- .../office/contact/HsOfficeContactEntity.java | 22 ++++++++ .../partner/HsOfficePartnerController.java | 4 +- .../HsOfficeRelationshipEntity.java | 10 +++- .../hsadminng/rbac/rbacdef/RbacView.java | 29 ++++++++-- .../rbacdef/RbacViewMermaidFlowchart.java | 53 +++++++++---------- .../RolesGrantsAndPermissionsGenerator.java | 23 +++----- 6 files changed, 91 insertions(+), 50 deletions(-) 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 69555dc4..922d3065 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 @@ -4,6 +4,7 @@ import lombok.*; 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.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; import org.hibernate.annotations.GenericGenerator; @@ -11,6 +12,10 @@ import org.hibernate.annotations.GenericGenerator; import jakarta.persistence.*; import java.util.UUID; +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.Role.*; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.stringify.Stringify.stringify; @Entity @@ -53,4 +58,21 @@ public class HsOfficeContactEntity implements Stringifyable, HasUuid { public String toShortString() { return label; } + + public static RbacView rbac() { + // @formatter:off + return rbacViewFor("contact", HsOfficeContactEntity.class) + .withIdentityView(RbacView.SQL.query("target.label")) + .withUpdatableColumns("label", "postalAddress", "emailAddresses", "phoneNumbers") + .createRole(OWNER) + .withPermission(ALL) + .withCurrentUserAsOwner() + .withIncomingSuperRole(GLOBAL, ADMIN) + .createSubRole(ADMIN) + .withPermission(EDIT) + .createSubRole(REFERRER) + .withPermission(VIEW) + .pop(); + // @formatter:on + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java index 04dcbb6a..6fdd0732 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java @@ -8,12 +8,12 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartne import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerPatchResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerRoleInsertResource; -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.hs.office.relationship.HsOfficeRelationshipRepository; import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipType; import net.hostsharing.hsadminng.mapper.Mapper; +import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -158,7 +158,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { return entity; } - private E ref(final Class entityClass, final UUID uuid) { + private E ref(final Class entityClass, final UUID uuid) { try { return em.getReference(entityClass, uuid); } catch (final Throwable exc) { 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 9a94279b..ebb17d58 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 @@ -91,6 +91,9 @@ public class HsOfficeRelationshipEntity implements HasUuid, Stringifyable { .importEntityAlias("holderPerson", HsOfficePersonEntity.class, fetchedBySql("select * from hs_office_person as p where p.uuid = ${REF}.relHolderUuid"), dependsOnColumn("relHolderUuid")) + .importEntityAlias("contact", HsOfficeContactEntity.class, + fetchedBySql("select * from hs_office_contact as c where c.uuid = ${REF}.contactUuid"), + dependsOnColumn("contactUuid")) .createRole(OWNER) .withCurrentUserAsOwner() .withPermission(ALL) @@ -99,9 +102,14 @@ public class HsOfficeRelationshipEntity implements HasUuid, Stringifyable { .createSubRole(ADMIN) .withPermission(EDIT) .createSubRole(AGENT) - .withIncomingSuperRole("holderPerson", ADMIN) .createSubRole(TENANT) .withPermission(VIEW) + .withIncomingSuperRole("anchorPerson", ADMIN) + .withIncomingSuperRole("holderPerson", ADMIN) + .withIncomingSuperRole("contact", ADMIN) + .withOutgoingSubRole("anchorPerson", REFERRER) + .withOutgoingSubRole("holderPerson", REFERRER) + .withOutgoingSubRole("contact", REFERRER) .pop(); // @formatter:on } 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 b89ac18b..628e3c03 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -10,6 +10,7 @@ import java.lang.reflect.InvocationTargetException; import java.util.*; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR; +import static org.apache.commons.lang3.StringUtils.uncapitalize; @Getter public class RbacView { @@ -18,9 +19,9 @@ public class RbacView { private final EntityAlias entityAlias; - private final Set userDefs = new HashSet<>(); - private final Set roleDefs = new HashSet<>(); - private final Set permDefs = new HashSet<>(); + private final Set userDefs = new LinkedHashSet<>(); + private final Set roleDefs = new LinkedHashSet<>(); + private final Set permDefs = new LinkedHashSet<>(); private final Map entityAliases = new HashMap<>() { @Override @@ -31,8 +32,8 @@ public class RbacView { return super.put(key, value); } }; - private final Set updatableColumns = new TreeSet<>(); - private final Set grantDefs = new HashSet<>(); + private final Set updatableColumns = new LinkedHashSet<>(); + private final Set grantDefs = new LinkedHashSet<>(); private SQL identityViewSqlQuery; private EntityAlias entityAliasProxy; @@ -340,6 +341,12 @@ public class RbacView { return this; } + public RbacRoleDefinition withOutgoingSubRole(final String entityAlias, final Role role) { + final var outgoingSubRole = findRbacRole(entityAlias, role); + addGrant(grantSubRoleToSuperRole(outgoingSubRole, this)); + return this; + } + public RbacRoleDefinition createSubRole(final Role role) { final var roleDef = findRbacRole(entityAlias, role).toCreate(); new RbacGrantDefinition(roleDef, this).toCreate(); @@ -415,6 +422,18 @@ public class RbacView { boolean isPlaceholder() { return entityClass == null; } + + + + private String withoutEntitySuffix(final String simpleEntityName) { + return simpleEntityName.substring(0, simpleEntityName.length()-"Entity".length()); + } + + String simpleName() { + return isGlobal() + ? aliasName + : uncapitalize(withoutEntitySuffix(entityClass.getSimpleName())); + } } public record Role(String roleName) { 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 5c63334f..9f34673f 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java @@ -1,6 +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.relationship.HsOfficeRelationshipEntity; import org.apache.commons.lang3.StringUtils; import java.io.IOException; @@ -21,11 +23,9 @@ public class RbacViewMermaidFlowchart { flowchart.append(""" %%{init:{'flowchart':{'htmlLabels':false}}}%% flowchart TB - """); renderEntitySubgraphs(); renderGrants(); - flowchart.append("```"); } private void renderEntitySubgraphs() { rbacDef.getEntityAliases().values().stream() @@ -40,12 +40,16 @@ public class RbacViewMermaidFlowchart { subgraph %{aliasName}["`**%{aliasName}**`"] direction TB - style %{aliasName} fill: %{color} + style %{aliasName} fill:%{color},stroke:darkblue,stroke-width:8px """ .replace("%{aliasName}", entity.aliasName()) .replace("%{color}", color )); + rbacDef.getEntityAliases().values().stream() + .filter(e -> e.aliasName().startsWith(entity.aliasName() + ".")) + .forEach(this::renderEntitySubgraph); + wrapOutputInSubgraph(entity.aliasName() + ":roles", color, rbacDef.getRoleDefs().stream() .filter(r -> r.getEntityAlias() == entity) @@ -118,32 +122,27 @@ public class RbacViewMermaidFlowchart { return flowchart.toString(); } - public static void main(String[] args) throws IOException { - -// Files.writeString( -// Paths.get("doc", "hsOfficeRelationship.md"), -// new RbacViewMermaidFlowchart(HsOfficeRelationshipEntity.rbac()).toString(), -// StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); -// -// -// Files.writeString( -// Paths.get("doc", "hsOfficeBankAccount.md"), -// new RbacViewMermaidFlowchart(HsOfficeBankAccountEntity.rbac()).toString(), -// StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - + void generateToMarkdownFile() throws IOException { + final Path path = Paths.get("doc", rbacDef.getEntityAlias().simpleName() + ".md"); Files.writeString( - Paths.get("doc", "hsOfficeDebitor.md"), + path, """ - ### rbac %{entityAlias} %{timestamp} - - ```mermaid - %{flowchart} - ``` - """ - .replace("%{entityAlias}", "contact") - .replace("%{timestamp}", LocalDateTime.now().toString()) - .replace("%{flowchart}", new RbacViewMermaidFlowchart(HsOfficeDebitorEntity.rbac()).toString()), + ### rbac %{entityAlias} %{timestamp} + + ```mermaid + %{flowchart} + ``` + """ + .replace("%{entityAlias}", rbacDef.getEntityAlias().aliasName()) + .replace("%{timestamp}", LocalDateTime.now().toString()) + .replace("%{flowchart}", flowchart.toString()), + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + System.out.println("Markdown-File: " + path.toAbsolutePath()); + } - StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + public static void main(String[] args) throws IOException { + new RbacViewMermaidFlowchart(HsOfficeBankAccountEntity.rbac()).generateToMarkdownFile(); + new RbacViewMermaidFlowchart(HsOfficeRelationshipEntity.rbac()).generateToMarkdownFile(); + new RbacViewMermaidFlowchart(HsOfficeDebitorEntity.rbac()).generateToMarkdownFile(); } } 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 50175146..75501578 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java @@ -1,7 +1,6 @@ package net.hostsharing.hsadminng.rbac.rbacdef; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacPermissionDefinition; -import org.apache.commons.lang3.StringUtils; import jakarta.persistence.Table; import java.util.HashSet; @@ -31,7 +30,7 @@ class RolesGrantsAndPermissionsGenerator { this.liquibaseTagPrefix = liquibaseTagPrefix; entityClass = rbacDef.getEntityAlias().entityClass(); - simpleEntityName = withoutEntitySuffix(entityClass.getSimpleName()); + simpleEntityName = entityClass.getSimpleName(); simpleEntityVarName = uncapitalize(simpleEntityName); rawTableName = withoutRvSuffix(entityClass.getAnnotation(Table.class).name()); } @@ -209,10 +208,6 @@ class RolesGrantsAndPermissionsGenerator { return tableName.substring(0, tableName.length()-"_rv".length()); } - private String withoutEntitySuffix(final String simpleEntityName) { - return simpleEntityName.substring(0, simpleEntityName.length()-"Entity".length()); - } - private String toPlPgSqlReference(final RbacView.RbacUserReference userRef) { return switch (userRef.role) { case CREATOR -> "currentUserUuid()"; @@ -221,20 +216,18 @@ class RolesGrantsAndPermissionsGenerator { } private String toPlPgSqlReference(final PostgresTriggerReference triggerRef, final RbacView.RbacRoleDefinition roleDef) { - return toSimpleVarName(roleDef.getEntityAlias()) + StringUtils.capitalize(roleDef.getRole().roleName()) + - ( roleDef.getEntityAlias().isGlobal() ? "()" - : rbacDef.isMainEntityAlias(roleDef.getEntityAlias()) ? ("("+triggerRef.name()+")") + return toVar(roleDef) + + (roleDef.getEntityAlias().isGlobal() ? "()" + : rbacDef.isMainEntityAlias(roleDef.getEntityAlias()) ? ("(" + triggerRef.name() + ")") : "(" + toTriggerReference(triggerRef, roleDef.getEntityAlias()) + ")"); } - private static String toTriggerReference(final PostgresTriggerReference triggerRef, final RbacView.EntityAlias entityAlias) { - return triggerRef.name().toLowerCase() + StringUtils.capitalize(entityAlias.aliasName()); + private static String toVar(final RbacView.RbacRoleDefinition roleDef) { + return uncapitalize(roleDef.getEntityAlias().simpleName()) + capitalize(roleDef.getRole().roleName()); } - private String toSimpleVarName(final RbacView.EntityAlias entityAlias) { - return entityAlias.isGlobal() - ? entityAlias.aliasName() - : uncapitalize(withoutEntitySuffix(entityAlias.entityClass().getSimpleName())); + private static String toTriggerReference(final PostgresTriggerReference triggerRef, final RbacView.EntityAlias entityAlias) { + return triggerRef.name().toLowerCase() + capitalize(entityAlias.aliasName()); } private String indent(final int tabs) { -- 2.39.5 From b4d6930fbeb9ac669f70426929fed233048961e3 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sun, 25 Feb 2024 11:58:45 +0100 Subject: [PATCH 09/53] introduce StringWriter and generate properly indented Flowchart --- .../rbacdef/RbacViewMermaidFlowchart.java | 71 ++++++++++--------- .../hsadminng/rbac/rbacdef/StringWriter.java | 71 +++++++++++++++++++ .../test/cust/TestCustomerEntityTest.java | 2 +- 3 files changed, 111 insertions(+), 33 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/StringWriter.java 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 9f34673f..f7286316 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java @@ -1,7 +1,5 @@ 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.relationship.HsOfficeRelationshipEntity; import org.apache.commons.lang3.StringUtils; @@ -16,11 +14,11 @@ public class RbacViewMermaidFlowchart { public static final String HOSTSHARING_ORANGE = "#dd4901"; public static final String HOSTSHARING_LIGHTBLUE = "#99bcdb"; private final RbacView rbacDef; - private final StringBuilder flowchart = new StringBuilder(); + private final StringWriter flowchart = new StringWriter(); public RbacViewMermaidFlowchart(final RbacView rbacDef) { this.rbacDef = rbacDef; - flowchart.append(""" + flowchart.writeLn(""" %%{init:{'flowchart':{'htmlLabels':false}}}%% flowchart TB """); @@ -36,54 +34,63 @@ public class RbacViewMermaidFlowchart { private void renderEntitySubgraph(final RbacView.EntityAlias entity) { final var color = rbacDef.isMainEntityAlias(entity) ? HOSTSHARING_ORANGE : HOSTSHARING_LIGHTBLUE; - flowchart.append(""" + flowchart.writeLn(""" subgraph %{aliasName}["`**%{aliasName}**`"] direction TB style %{aliasName} fill:%{color},stroke:darkblue,stroke-width:8px - """ .replace("%{aliasName}", entity.aliasName()) .replace("%{color}", color )); - rbacDef.getEntityAliases().values().stream() - .filter(e -> e.aliasName().startsWith(entity.aliasName() + ".")) - .forEach(this::renderEntitySubgraph); + flowchart.indented( () -> { + rbacDef.getEntityAliases().values().stream() + .filter(e -> e.aliasName().startsWith(entity.aliasName() + ".")) + .forEach(this::renderEntitySubgraph); - wrapOutputInSubgraph(entity.aliasName() + ":roles", color, - rbacDef.getRoleDefs().stream() - .filter(r -> r.getEntityAlias() == entity) - .map(r -> " " + roleDef(r)) - .collect(joining("\n"))); - - wrapOutputInSubgraph(entity.aliasName() + ":permissions", color, - rbacDef.getPermDefs().stream() - .filter(p -> p.getEntityAlias() == entity) - .map(p -> " " + permDef(p) ) + wrapOutputInSubgraph(entity.aliasName() + ":roles", color, + rbacDef.getRoleDefs().stream() + .filter(r -> r.getEntityAlias() == entity) + .map(r -> " " + roleDef(r)) .collect(joining("\n"))); - if (rbacDef.isMainEntityAlias(entity) && rbacDef.getEntityAliasProxy() != null ) { - renderEntitySubgraph(rbacDef.getEntityAliasProxy()); - } + wrapOutputInSubgraph(entity.aliasName() + ":permissions", color, + rbacDef.getPermDefs().stream() + .filter(p -> p.getEntityAlias() == entity) + .map(p -> " " + permDef(p) ) + .collect(joining("\n"))); - flowchart.append("end\n\n"); + if (rbacDef.isMainEntityAlias(entity) && rbacDef.getEntityAliasProxy() != null ) { + renderEntitySubgraph(rbacDef.getEntityAliasProxy()); + } + + }); + flowchart.chopEmptyLines(); + flowchart.writeLn("end"); + flowchart.writeLn(); } private void wrapOutputInSubgraph(final String name, final String color, final String content) { if (!StringUtils.isEmpty(content)) { - flowchart.append("subgraph " + name + "[ ]\n"); - flowchart.append("style %{aliasName} fill: %{color}\n\n" - .replace("%{aliasName}", name) - .replace("%{color}", color)); - flowchart.append(content); - flowchart.append("\nend\n\n"); + flowchart.emptyLine(); + flowchart.writeLn("subgraph " + name + "[ ]\n"); + flowchart.indented(() -> { + flowchart.writeLn("style %{aliasName} fill: %{color}" + .replace("%{aliasName}", name) + .replace("%{color}", color)); + flowchart.writeLn(); + flowchart.writeLn(content); + }); + flowchart.chopEmptyLines(); + flowchart.writeLn("end"); + flowchart.writeLn(); } } private void renderGrants() { rbacDef.getGrantDefs() .forEach(g -> { - flowchart.append(grantDef(g) + "\n" ); + flowchart.writeLn(grantDef(g) + "\n"); }); } @@ -141,8 +148,8 @@ public class RbacViewMermaidFlowchart { } public static void main(String[] args) throws IOException { - new RbacViewMermaidFlowchart(HsOfficeBankAccountEntity.rbac()).generateToMarkdownFile(); +// new RbacViewMermaidFlowchart(HsOfficeBankAccountEntity.rbac()).generateToMarkdownFile(); new RbacViewMermaidFlowchart(HsOfficeRelationshipEntity.rbac()).generateToMarkdownFile(); - new RbacViewMermaidFlowchart(HsOfficeDebitorEntity.rbac()).generateToMarkdownFile(); +// new RbacViewMermaidFlowchart(HsOfficeDebitorEntity.rbac()).generateToMarkdownFile(); } } diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/StringWriter.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/StringWriter.java new file mode 100644 index 00000000..9b0bfc7a --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/StringWriter.java @@ -0,0 +1,71 @@ +package net.hostsharing.hsadminng.rbac.rbacdef; + +import org.apache.commons.lang3.StringUtils; + +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.joining; + +public class StringWriter { + + private final StringBuilder string = new StringBuilder(); + private int indentLevel = 0; + + void writeLn(final String text) { + string.append( indented(text)); + writeLn(); + } + + void writeLn() { + string.append( "\n"); + } + + private String indented(final String text) { + if ( indentLevel == 0) { + return text.trim(); + } + final var indentation = StringUtils.repeat(" ", indentLevel); + final var indented = stream(text.split("\n")) + .map(line -> line.trim().isBlank() ? "" : indentation + line.trim()) + .collect(joining("\n")); + return indented; + } + + void indent() { + ++indentLevel; + } + + void unindent() { + --indentLevel; + } + + void indented(final Runnable indented) { + indent(); + indented.run(); + unindent(); + } + + boolean chopTail(final String tail) { + if (string.toString().endsWith(tail)) { + string.setLength(string.length() - tail.length()); + return true; + } + return false; + } + + void chopEmptyLines() { + while (string.toString().endsWith("\n\n")) { + string.setLength(string.length() - 1); + }; + } + + void emptyLine() { + if (!string.toString().endsWith("\n\n")) { + writeLn(); + } + } + + @Override + public String toString() { + return string.toString(); + } +} 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 c7f9800a..9bdbafc6 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityTest.java @@ -27,7 +27,7 @@ class TestCustomerEntityTest { end subgraph contact:permissions[ ] - style contact:permissions fill: #dd4901 + style contact:permissions fill: #dd4901 perm:contact:*{{contact:*}} perm:contact:add-package{{contact:add-package}} -- 2.39.5 From 5ac616e4252b39b43284058f83f2a63de59193a9 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sun, 25 Feb 2024 13:19:27 +0100 Subject: [PATCH 10/53] improve RBAC definition DSL --- .../HsOfficeBankAccountEntity.java | 23 ++++---- .../office/contact/HsOfficeContactEntity.java | 24 ++++---- .../office/debitor/HsOfficeDebitorEntity.java | 56 +++++++++---------- .../office/person/HsOfficePersonEntity.java | 23 ++++---- .../HsOfficeRelationshipEntity.java | 47 ++++++++-------- .../hsadminng/rbac/rbacdef/RbacView.java | 53 +++++++++++------- .../rbacdef/RbacViewMermaidFlowchart.java | 1 + .../test/cust/TestCustomerEntity.java | 26 +++++---- .../test/cust/TestCustomerEntityTest.java | 4 +- 9 files changed, 135 insertions(+), 122 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 40b0a3d2..7f5a0185 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 @@ -16,6 +16,7 @@ import java.util.UUID; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.*; 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.stringify.Stringify.stringify; @@ -56,19 +57,19 @@ public class HsOfficeBankAccountEntity implements HasUuid, Stringifyable { } public static RbacView rbac() { - // @formatter:off return rbacViewFor("bankAccount", HsOfficeBankAccountEntity.class) .withIdentityView(SQL.query("target.iban || ':' || target.holder")) .withUpdatableColumns("holder", "iban", "bic") - .createRole(OWNER) - .withCurrentUserAsOwner() - .withPermission(ALL) - .withIncomingSuperRole(GLOBAL, ADMIN) - .createSubRole(ADMIN) - .withPermission(EDIT) - .createSubRole(REFERRER) - .withPermission(VIEW) - .pop(); - // @formatter:on + .createRole(OWNER, (with) -> { + with.owningUser(CREATOR); + with.incomingSuperRole(GLOBAL, ADMIN); + with.permission(ALL); + }) + .createSubRole(ADMIN, (with) -> { + with.permission(EDIT); + }) + .createSubRole(REFERRER, (with) -> { + with.permission(VIEW); + }); } } 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 922d3065..b9522d0d 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 @@ -14,6 +14,7 @@ import java.util.UUID; 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.rbacViewFor; import static net.hostsharing.hsadminng.stringify.Stringify.stringify; @@ -33,7 +34,6 @@ public class HsOfficeContactEntity implements Stringifyable, HasUuid { .withProp(Fields.label, HsOfficeContactEntity::getLabel) .withProp(Fields.emailAddresses, HsOfficeContactEntity::getEmailAddresses); - @Id @GeneratedValue(generator = "UUID") @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator") @@ -60,19 +60,19 @@ public class HsOfficeContactEntity implements Stringifyable, HasUuid { } public static RbacView rbac() { - // @formatter:off return rbacViewFor("contact", HsOfficeContactEntity.class) .withIdentityView(RbacView.SQL.query("target.label")) .withUpdatableColumns("label", "postalAddress", "emailAddresses", "phoneNumbers") - .createRole(OWNER) - .withPermission(ALL) - .withCurrentUserAsOwner() - .withIncomingSuperRole(GLOBAL, ADMIN) - .createSubRole(ADMIN) - .withPermission(EDIT) - .createSubRole(REFERRER) - .withPermission(VIEW) - .pop(); - // @formatter:on + .createRole(OWNER, (with) -> { + with.owningUser(CREATOR); + with.incomingSuperRole(GLOBAL, ADMIN); + with.permission(ALL); + }) + .createSubRole(ADMIN, (with) -> { + with.permission(EDIT); + }) + .createSubRole(REFERRER, (with) -> { + with.permission(VIEW); + }); } } 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 d4dbecda..8905a34f 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 @@ -4,9 +4,9 @@ import lombok.*; import net.hostsharing.hsadminng.errors.DisplayName; import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity; +import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity; import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity; import net.hostsharing.hsadminng.persistence.HasUuid; -import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; @@ -18,9 +18,7 @@ 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.Permission.VIEW; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*; -import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.REFERRER; 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; @@ -87,7 +85,7 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable { private String defaultPrefix; private String getDebitorNumberString() { - if (partner == null || partner.getPartnerNumber() == null || debitorNumberSuffix == null ) { + if (partner == null || partner.getPartnerNumber() == null || debitorNumberSuffix == null) { return null; } return partner.getPartnerNumber() + String.format("%02d", debitorNumberSuffix); @@ -108,20 +106,19 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable { } public static RbacView rbac() { - // @formatter:off return rbacViewFor("debitor", HsOfficeDebitorEntity.class) .withIdentityView(RbacView.SQL.query(""" - SELECT debitor.uuid, - 'D-' || (SELECT partner.partnerNumber - FROM hs_office_partner partner - JOIN hs_office_relationship partnerRel - ON partnerRel.uuid = partner.partnerRoleUUid AND partnerRel.relType = 'PARTNER' - JOIN hs_office_relationship debitorRel - ON debitorRel.relAnchorUuid = partnerRel.relHolderUuid AND partnerRel.relType = 'ACCOUNTING' - WHERE debitorRel.uuid = debitor.debitorRelUuid) - || to_char(debitorNumberSuffix, 'fm00') - from hs_office_debitor as debitor; - """)) + SELECT debitor.uuid, + 'D-' || (SELECT partner.partnerNumber + FROM hs_office_partner partner + JOIN hs_office_relationship partnerRel + ON partnerRel.uuid = partner.partnerRoleUUid AND partnerRel.relType = 'PARTNER' + JOIN hs_office_relationship debitorRel + ON debitorRel.relAnchorUuid = partnerRel.relHolderUuid AND partnerRel.relType = 'ACCOUNTING' + WHERE debitorRel.uuid = debitor.debitorRelUuid) + || to_char(debitorNumberSuffix, 'fm00') + from hs_office_debitor as debitor; + """)) .withUpdatableColumns( "debitorRel", "billable", @@ -131,15 +128,15 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable { "vatCountryCode", "vatBusiness", "vatReverseCharge", - "defaultPrefix" /* TODO: do we want that updatable? */ ) + "defaultPrefix" /* TODO: do we want that updatable? */) .createPermission(custom("new-debitor")).grantedTo("global", ADMIN).pop() .importProxyEntity("debitorRel", HsOfficeRelationshipEntity.class, fetchedBySql(""" - SELECT * - FROM hs_office_relationship AS r - WHERE r.relType = 'ACCOUNTING' AND r.relHolderUuid = ${REF}.debitorRelUuid; - """), + SELECT * + FROM hs_office_relationship AS r + WHERE r.relType = 'ACCOUNTING' AND r.relHolderUuid = ${REF}.debitorRelUuid; + """), dependsOnColumn("debitorRelUuid")) .createPermission(ALL).grantedTo("debitorRel", OWNER).pop() .createPermission(EDIT).grantedTo("debitorRel", ADMIN).pop() @@ -147,20 +144,20 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable { .importEntityAlias("refundBankAccount", HsOfficeBankAccountEntity.class, fetchedBySql(""" - SELECT * - FROM hs_office_relationship AS r - WHERE r.relType = 'ACCOUNTING' AND r.relHolderUuid = ${REF}.debitorRelUuid; - """), + 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(""" - SELECT * - FROM hs_office_relationship AS partnerRel - WHERE ${debitorRel}.relAnchorUuid = partnerRel.relHolderUuid; - """), + 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) @@ -169,6 +166,5 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable { .forExampleRole("partnerPerson", ADMIN).wouldBeGrantedTo("partnerRel", ADMIN) .forExampleRole("operationalPerson", ADMIN).wouldBeGrantedTo("partnerRel", ADMIN) .forExampleRole("partnerRel", TENANT).wouldBeGrantedTo("partnerPerson", REFERRER); - // @formatter:on } } 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 ad3cfc02..83c9c94c 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 @@ -14,6 +14,7 @@ import java.util.UUID; 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.rbacViewFor; @@ -64,19 +65,19 @@ public class HsOfficePersonEntity implements HasUuid, Stringifyable { } public static RbacView rbac() { - // @formatter:off return rbacViewFor("person", HsOfficePersonEntity.class) .withIdentityView(query("concat(target.tradeName, target.familyName, target.givenName)")) .withUpdatableColumns("personType", "tradeName", "givenName", "familyName") - .createRole(OWNER) - .withPermission(ALL) - .withCurrentUserAsOwner() - .withIncomingSuperRole(GLOBAL, ADMIN) - .createSubRole(ADMIN) - .withPermission(EDIT) - .createSubRole(REFERRER) - .withPermission(VIEW) - .pop(); - // @formatter:on + .createRole(OWNER, (with) -> { + with.permission(ALL); + with.owningUser(CREATOR); + with.incomingSuperRole(GLOBAL, ADMIN); + }) + .createSubRole(ADMIN, (with) -> { + with.permission(EDIT); + }) + .createSubRole(REFERRER, (with) -> { + with.permission(VIEW); + }); } } 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 ebb17d58..3637a0ac 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 @@ -3,8 +3,8 @@ package net.hostsharing.hsadminng.hs.office.relationship; import lombok.*; import lombok.experimental.FieldNameConstants; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity; -import net.hostsharing.hsadminng.persistence.HasUuid; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; +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; @@ -16,6 +16,7 @@ import java.util.UUID; 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.fetchedBySql; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; @@ -77,13 +78,12 @@ public class HsOfficeRelationshipEntity implements HasUuid, Stringifyable { } public static RbacView rbac() { - // @formatter:off return rbacViewFor("relationship", HsOfficeRelationshipEntity.class) .withIdentityView(SQL.query(""" - (select idName from hs_office_person_iv p where p.uuid = target.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 = target.relAnchorUuid) + || '-with-' || target.relType || '-' + || (select idName from hs_office_person_iv p where p.uuid = target.relHolderUuid) + """)) .withUpdatableColumns("contactUuid") .importEntityAlias("anchorPerson", HsOfficePersonEntity.class, fetchedBySql("select * from hs_office_person as p where p.uuid = ${REF}.relAnchorUuid"), @@ -94,23 +94,24 @@ public class HsOfficeRelationshipEntity implements HasUuid, Stringifyable { .importEntityAlias("contact", HsOfficeContactEntity.class, fetchedBySql("select * from hs_office_contact as c where c.uuid = ${REF}.contactUuid"), dependsOnColumn("contactUuid")) - .createRole(OWNER) - .withCurrentUserAsOwner() - .withPermission(ALL) - .withIncomingSuperRole(GLOBAL, ADMIN) - .withIncomingSuperRole("anchorPerson", ADMIN) - .createSubRole(ADMIN) - .withPermission(EDIT) + .createRole(OWNER, (with) -> { + with.owningUser(CREATOR); + with.incomingSuperRole(GLOBAL, ADMIN); + with.incomingSuperRole("anchorPerson", ADMIN); + with.permission(ALL); + }) + .createSubRole(ADMIN, (with) -> { + with.permission(EDIT); + }) .createSubRole(AGENT) - .createSubRole(TENANT) - .withPermission(VIEW) - .withIncomingSuperRole("anchorPerson", ADMIN) - .withIncomingSuperRole("holderPerson", ADMIN) - .withIncomingSuperRole("contact", ADMIN) - .withOutgoingSubRole("anchorPerson", REFERRER) - .withOutgoingSubRole("holderPerson", REFERRER) - .withOutgoingSubRole("contact", REFERRER) - .pop(); - // @formatter:on + .createSubRole(TENANT, (with) -> { + with.incomingSuperRole("anchorPerson", ADMIN); + with.incomingSuperRole("holderPerson", ADMIN); + with.incomingSuperRole("contact", ADMIN); + with.outgoingSubRole("anchorPerson", REFERRER); + with.outgoingSubRole("holderPerson", REFERRER); + with.outgoingSubRole("contact", REFERRER); + with.permission(VIEW); + }); } } 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 628e3c03..33fb29fa 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,7 @@ package net.hostsharing.hsadminng.rbac.rbacdef; +import java.util.function.Consumer; + import lombok.EqualsAndHashCode; import lombok.Getter; import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity; @@ -37,6 +39,7 @@ public class RbacView { private SQL identityViewSqlQuery; private EntityAlias entityAliasProxy; + private RbacRoleDefinition previousRoleDef; public static RbacView rbacViewFor(final String alias, final Class entityClass) { return new RbacView(alias, entityClass); @@ -59,8 +62,26 @@ public class RbacView { return this; } - public RbacRoleDefinition createRole(final Role role) { - return findRbacRole(entityAlias, role).toCreate(); + public RbacView createRole(final Role role, final Consumer with) { + final RbacRoleDefinition newRoleDef = findRbacRole(entityAlias, role).toCreate(); + with.accept(newRoleDef); + previousRoleDef = newRoleDef; + return this; + } + + public RbacView createSubRole(final Role role) { + final RbacRoleDefinition newRoleDef = findRbacRole(entityAlias, role).toCreate(); + new RbacGrantDefinition(newRoleDef, previousRoleDef).toCreate(); + previousRoleDef = newRoleDef; + return this; + } + + public RbacView createSubRole(final Role role, final Consumer with) { + final RbacRoleDefinition newRoleDef = findRbacRole(entityAlias, role).toCreate(); + new RbacGrantDefinition(newRoleDef, previousRoleDef).toCreate(); + with.accept(newRoleDef); + previousRoleDef = newRoleDef; + return this; } public RbacPermissionDefinition createPermission(final Permission permission) { @@ -143,8 +164,8 @@ public class RbacView { } - private RbacGrantDefinition grantRoleToCurrentUser(final RbacRoleDefinition roleDefinition) { - return new RbacGrantDefinition(roleDefinition, currentUser()).toCreate(); + private RbacGrantDefinition grantRoleToUser(final RbacRoleDefinition roleDefinition, final RbacUserReference user) { + return new RbacGrantDefinition(roleDefinition, user).toCreate(); } private RbacGrantDefinition grantPermissionToRole(final RbacPermissionDefinition permDef , final RbacRoleDefinition roleDef) { @@ -325,46 +346,36 @@ public class RbacView { return this; } - public RbacRoleDefinition withCurrentUserAsOwner() { - addGrant(grantRoleToCurrentUser(this)); + public RbacRoleDefinition owningUser(final RbacUserReference.UserRole userRole) { + addGrant(grantRoleToUser(this, findUserRef(userRole))); return this; } - public RbacRoleDefinition withPermission(final Permission permission) { + public RbacRoleDefinition permission(final Permission permission) { addGrant(grantPermissionToRole( createPermission(entityAlias, permission) , this)); return this; } - public RbacRoleDefinition withIncomingSuperRole(final String entityAlias, final Role role) { + public RbacRoleDefinition incomingSuperRole(final String entityAlias, final Role role) { final var incomingSuperRole = findRbacRole(entityAlias, role); addGrant(grantSubRoleToSuperRole(this, incomingSuperRole)); return this; } - public RbacRoleDefinition withOutgoingSubRole(final String entityAlias, final Role role) { + public RbacRoleDefinition outgoingSubRole(final String entityAlias, final Role role) { final var outgoingSubRole = findRbacRole(entityAlias, role); addGrant(grantSubRoleToSuperRole(outgoingSubRole, this)); return this; } - public RbacRoleDefinition createSubRole(final Role role) { - final var roleDef = findRbacRole(entityAlias, role).toCreate(); - new RbacGrantDefinition(roleDef, this).toCreate(); - return roleDef; - } - - public RbacView pop() { - return RbacView.this; - } - @Override public String toString() { return "role:" + entityAlias.aliasName + role; } } - public RbacUserReference currentUser() { - return userDefs.stream().filter(u -> u.role == CREATOR).findFirst().orElseThrow(); + public RbacUserReference findUserRef(final RbacUserReference.UserRole userRole) { + return userDefs.stream().filter(u -> u.role == userRole).findFirst().orElseThrow(); } @EqualsAndHashCode 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 f7286316..b6c71024 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java @@ -22,6 +22,7 @@ public class RbacViewMermaidFlowchart { %%{init:{'flowchart':{'htmlLabels':false}}}%% flowchart TB """); + flowchart.writeLn(); renderEntitySubgraphs(); renderGrants(); } 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 3d5e6f19..fecee7b7 100644 --- a/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java @@ -11,7 +11,9 @@ import jakarta.persistence.*; import java.util.UUID; 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.Permission.ALL; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.VIEW; +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; @@ -36,19 +38,19 @@ public class TestCustomerEntity implements RbacObject { public static RbacView rbac() { - // @formatter:off return rbacViewFor("contact", TestCustomerEntity.class) .withIdentityView(RbacView.SQL.query("target.prefix")) .withUpdatableColumns("reference", "prefix", "adminUserName") - .createRole(OWNER) - .withPermission(ALL) - .withCurrentUserAsOwner() - .withIncomingSuperRole(GLOBAL, ADMIN) - .createSubRole(ADMIN) - .withPermission(RbacView.Permission.custom("add-package")) - .createSubRole(TENANT) - .withPermission(VIEW) - .pop(); - // @formatter:on + .createRole(OWNER, (with) -> { + with.owningUser(CREATOR); + with.incomingSuperRole(GLOBAL, ADMIN); + with.permission(ALL); + }) + .createSubRole(ADMIN, (with) -> { + with.permission(RbacView.Permission.custom("add-package")); + }) + .createSubRole(TENANT, (with) -> { + with.permission(VIEW); + }); } } 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 9bdbafc6..faf126a3 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityTest.java @@ -35,11 +35,11 @@ class TestCustomerEntityTest { end end - role:contact:owner ==> perm:contact:* - role:contact:owner ==> perm:contact:* user:creator ==> role:contact:owner role:global:admin ==> role:contact:owner role:global:admin ==> role:contact:owner + role:contact:owner ==> perm:contact:* + role:contact:owner ==> perm:contact:* role:contact:owner ==> role:contact:admin role:contact:admin ==> perm:contact:add-package role:contact:admin ==> perm:contact:add-package -- 2.39.5 From 217142411899a5637a7d09e849cd4b8f47f231f4 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sun, 25 Feb 2024 14:14:02 +0100 Subject: [PATCH 11/53] fix duplicate grangs error --- .../hsadminng/rbac/rbacdef/RbacView.java | 68 +++++++++++++------ .../rbacdef/RbacViewMermaidFlowchart.java | 6 +- .../test/cust/TestCustomerEntityTest.java | 8 +-- 3 files changed, 52 insertions(+), 30 deletions(-) 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 33fb29fa..c6a73ade 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -71,14 +71,14 @@ public class RbacView { public RbacView createSubRole(final Role role) { final RbacRoleDefinition newRoleDef = findRbacRole(entityAlias, role).toCreate(); - new RbacGrantDefinition(newRoleDef, previousRoleDef).toCreate(); + findOrCreateGrantDef(newRoleDef, previousRoleDef).toCreate(); previousRoleDef = newRoleDef; return this; } public RbacView createSubRole(final Role role, final Consumer with) { final RbacRoleDefinition newRoleDef = findRbacRole(entityAlias, role).toCreate(); - new RbacGrantDefinition(newRoleDef, previousRoleDef).toCreate(); + findOrCreateGrantDef(newRoleDef, previousRoleDef).toCreate(); with.accept(newRoleDef); previousRoleDef = newRoleDef; return this; @@ -146,7 +146,7 @@ public class RbacView { }); importedRbacView.getGrantDefs().forEach(grantDef -> { if (grantDef.grantType() == RbacGrantDefinition.GrantType.ROLE_TO_ROLE) { - new RbacGrantDefinition( + findOrCreateGrantDef( findRbacRole(mapper.map(grantDef.getSubRoleDef().entityAlias.aliasName), grantDef.getSubRoleDef().getRole()), findRbacRole(mapper.map(grantDef.getSuperRoleDef().entityAlias.aliasName), grantDef.getSuperRoleDef().getRole()) ); @@ -165,15 +165,15 @@ public class RbacView { private RbacGrantDefinition grantRoleToUser(final RbacRoleDefinition roleDefinition, final RbacUserReference user) { - return new RbacGrantDefinition(roleDefinition, user).toCreate(); + return findOrCreateGrantDef(roleDefinition, user).toCreate(); } private RbacGrantDefinition grantPermissionToRole(final RbacPermissionDefinition permDef , final RbacRoleDefinition roleDef) { - return new RbacGrantDefinition(permDef, roleDef).toCreate(); + return findOrCreateGrantDef(permDef, roleDef).toCreate(); } private RbacGrantDefinition grantSubRoleToSuperRole(final RbacRoleDefinition subRoleDefinition, final RbacRoleDefinition superRoleDefinition) { - return new RbacGrantDefinition(subRoleDefinition, superRoleDefinition).toCreate(); + return findOrCreateGrantDef(subRoleDefinition, superRoleDefinition).toCreate(); } boolean isMainEntityAlias(final EntityAlias entityAlias) { @@ -193,7 +193,7 @@ public class RbacView { } public RbacView grantRole(final String entityAlias, final Role role) { - new RbacGrantDefinition(findRbacRole(entityAlias, role), superRoleDef).toCreate(); + findOrCreateGrantDef(findRbacRole(entityAlias, role), superRoleDef).toCreate(); return RbacView.this; } @@ -210,19 +210,20 @@ public class RbacView { @Override public String toString() { + final var arrow = isAssumed() ? " --> " : " -- // --> "; return switch (grantType()) { - case USER_TO_ROLE -> userDef.toString() + " --> " + subRoleDef.toString(); - case ROLE_TO_ROLE -> superRoleDef + " --> " + subRoleDef; - case ROLE_TO_PERM -> superRoleDef + " --> " + permDef; + case USER_TO_ROLE -> userDef.toString() + arrow + subRoleDef.toString(); + case ROLE_TO_ROLE -> superRoleDef + arrow + subRoleDef; + case ROLE_TO_PERM -> superRoleDef + arrow + permDef; }; } - public RbacGrantDefinition(final RbacRoleDefinition subRoleDef, final RbacRoleDefinition superRoleDef) { + RbacGrantDefinition(final RbacRoleDefinition subRoleDef, final RbacRoleDefinition superRoleDef) { this.userDef = null; this.subRoleDef = subRoleDef; this.superRoleDef = superRoleDef; this.permDef = null; - grantDefs.add(this); + register(this); } public RbacGrantDefinition(final RbacPermissionDefinition permDef, final RbacRoleDefinition roleDef) { @@ -230,7 +231,7 @@ public class RbacView { this.subRoleDef = null; this.superRoleDef = roleDef; this.permDef = permDef; - grantDefs.add(this); + register(this); } public RbacGrantDefinition(final RbacRoleDefinition roleDef, final RbacUserReference userDef) { @@ -238,6 +239,11 @@ public class RbacView { this.subRoleDef = roleDef; this.superRoleDef = null; this.permDef = null; + register(this); + } + + private void register(final RbacGrantDefinition rbacGrantDefinition) { + grantDefs.add(rbacGrantDefinition); } @NotNull @@ -268,10 +274,6 @@ public class RbacView { } } - private void addGrant(final RbacGrantDefinition grant) { - grantDefs.add(grant); - } - public class RbacExampleRole { final EntityAlias subRoleEntity; @@ -317,7 +319,7 @@ public class RbacView { } public RbacPermissionDefinition grantedTo(final String entityAlias, final Role role) { - new RbacGrantDefinition(this, findRbacRole(entityAlias, role) ).toCreate(); + findOrCreateGrantDef(this, findRbacRole(entityAlias, role) ).toCreate(); return this; } @@ -347,24 +349,24 @@ public class RbacView { } public RbacRoleDefinition owningUser(final RbacUserReference.UserRole userRole) { - addGrant(grantRoleToUser(this, findUserRef(userRole))); + grantRoleToUser(this, findUserRef(userRole)); return this; } public RbacRoleDefinition permission(final Permission permission) { - addGrant(grantPermissionToRole( createPermission(entityAlias, permission) , this)); + grantPermissionToRole( createPermission(entityAlias, permission) , this); return this; } public RbacRoleDefinition incomingSuperRole(final String entityAlias, final Role role) { final var incomingSuperRole = findRbacRole(entityAlias, role); - addGrant(grantSubRoleToSuperRole(this, incomingSuperRole)); + grantSubRoleToSuperRole(this, incomingSuperRole); return this; } public RbacRoleDefinition outgoingSubRole(final String entityAlias, final Role role) { final var outgoingSubRole = findRbacRole(entityAlias, role); - addGrant(grantSubRoleToSuperRole(outgoingSubRole, this)); + grantSubRoleToSuperRole(outgoingSubRole, this); return this; } @@ -414,6 +416,28 @@ public class RbacView { public RbacRoleDefinition findRbacRole(final String entityAliasName, final Role role) { return findRbacRole(findEntityAlias(entityAliasName), role); + + } + + private RbacGrantDefinition findOrCreateGrantDef(final RbacRoleDefinition roleDefinition, final RbacUserReference user) { + return grantDefs.stream() + .filter(g -> g.subRoleDef == roleDefinition && g.userDef == user) + .findFirst() + .orElseGet(() -> new RbacGrantDefinition(roleDefinition, user)); + } + + private RbacGrantDefinition findOrCreateGrantDef(final RbacPermissionDefinition permDef, final RbacRoleDefinition roleDef) { + return grantDefs.stream() + .filter(g -> g.permDef == permDef && g.subRoleDef == roleDef) + .findFirst() + .orElseGet(() -> new RbacGrantDefinition(permDef, roleDef)); + } + + private RbacGrantDefinition findOrCreateGrantDef(final RbacRoleDefinition subRoleDefinition, final RbacRoleDefinition superRoleDefinition) { + return grantDefs.stream() + .filter(g -> g.subRoleDef == subRoleDefinition && g.superRoleDef == superRoleDefinition) + .findFirst() + .orElseGet(() -> new RbacGrantDefinition(subRoleDefinition, superRoleDefinition)); } record EntityAlias(String aliasName, Class entityClass, SQL fetchSql, Column dependsOnColum) { 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 b6c71024..8e2e6e57 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java @@ -1,5 +1,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.relationship.HsOfficeRelationshipEntity; import org.apache.commons.lang3.StringUtils; @@ -149,8 +151,8 @@ public class RbacViewMermaidFlowchart { } public static void main(String[] args) throws IOException { -// new RbacViewMermaidFlowchart(HsOfficeBankAccountEntity.rbac()).generateToMarkdownFile(); + new RbacViewMermaidFlowchart(HsOfficeBankAccountEntity.rbac()).generateToMarkdownFile(); new RbacViewMermaidFlowchart(HsOfficeRelationshipEntity.rbac()).generateToMarkdownFile(); -// new RbacViewMermaidFlowchart(HsOfficeDebitorEntity.rbac()).generateToMarkdownFile(); + new RbacViewMermaidFlowchart(HsOfficeDebitorEntity.rbac()).generateToMarkdownFile(); } } 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 faf126a3..10f1c4ce 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityTest.java @@ -15,8 +15,8 @@ class TestCustomerEntityTest { flowchart TB subgraph contact["`**contact**`"] - direction TB - style contact fill:#dd4901,stroke:darkblue,stroke-width:8px + direction TB + style contact fill:#dd4901,stroke:darkblue,stroke-width:8px subgraph contact:roles[ ] style contact:roles fill: #dd4901 @@ -37,15 +37,11 @@ class TestCustomerEntityTest { user:creator ==> role:contact:owner role:global:admin ==> role:contact:owner - role:global:admin ==> role:contact:owner - role:contact:owner ==> perm:contact:* role:contact:owner ==> perm:contact:* role:contact:owner ==> role:contact:admin role:contact:admin ==> perm:contact:add-package - role:contact:admin ==> perm:contact:add-package role:contact:admin ==> role:contact:tenant role:contact:tenant ==> perm:contact:view - role:contact:tenant ==> perm:contact:view """); } } -- 2.39.5 From c1c67b3c7b7bf3589970634683e98319fa2d81f0 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 26 Feb 2024 09:07:09 +0100 Subject: [PATCH 12/53] align naming to rootEntityAlias --- .../hsadminng/rbac/rbacdef/RbacView.java | 32 +++++++++---------- .../rbacdef/RbacViewMermaidFlowchart.java | 10 +++--- .../rbacdef/RbacViewPostgresGenerator.java | 2 +- .../RolesGrantsAndPermissionsGenerator.java | 14 ++++---- 4 files changed, 29 insertions(+), 29 deletions(-) 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 c6a73ade..88cfa329 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -19,7 +19,7 @@ public class RbacView { public static final String GLOBAL = "global"; - private final EntityAlias entityAlias; + private final EntityAlias rootEntityAlias; private final Set userDefs = new LinkedHashSet<>(); private final Set roleDefs = new LinkedHashSet<>(); @@ -38,7 +38,7 @@ public class RbacView { private final Set grantDefs = new LinkedHashSet<>(); private SQL identityViewSqlQuery; - private EntityAlias entityAliasProxy; + private EntityAlias rootEntityAliasProxy; private RbacRoleDefinition previousRoleDef; public static RbacView rbacViewFor(final String alias, final Class entityClass) { @@ -46,8 +46,8 @@ public class RbacView { } RbacView(final String alias, final Class entityClass) { - entityAlias = new EntityAlias(alias, entityClass); - entityAliases.put(alias, entityAlias); + rootEntityAlias = new EntityAlias(alias, entityClass); + entityAliases.put(alias, rootEntityAlias); new RbacUserReference(CREATOR); entityAliases.put("global", new EntityAlias("global")); } @@ -63,21 +63,21 @@ public class RbacView { } public RbacView createRole(final Role role, final Consumer with) { - final RbacRoleDefinition newRoleDef = findRbacRole(entityAlias, role).toCreate(); + final RbacRoleDefinition newRoleDef = findRbacRole(rootEntityAlias, role).toCreate(); with.accept(newRoleDef); previousRoleDef = newRoleDef; return this; } public RbacView createSubRole(final Role role) { - final RbacRoleDefinition newRoleDef = findRbacRole(entityAlias, role).toCreate(); + final RbacRoleDefinition newRoleDef = findRbacRole(rootEntityAlias, role).toCreate(); findOrCreateGrantDef(newRoleDef, previousRoleDef).toCreate(); previousRoleDef = newRoleDef; return this; } public RbacView createSubRole(final Role role, final Consumer with) { - final RbacRoleDefinition newRoleDef = findRbacRole(entityAlias, role).toCreate(); + final RbacRoleDefinition newRoleDef = findRbacRole(rootEntityAlias, role).toCreate(); findOrCreateGrantDef(newRoleDef, previousRoleDef).toCreate(); with.accept(newRoleDef); previousRoleDef = newRoleDef; @@ -85,7 +85,7 @@ public class RbacView { } public RbacPermissionDefinition createPermission(final Permission permission) { - return createPermission(entityAlias, permission); + return createPermission(rootEntityAlias, permission); } private RbacPermissionDefinition createPermission(final EntityAlias entityAlias, final Permission permission) { @@ -103,10 +103,10 @@ public class RbacView { public RbacView importProxyEntity( final String aliasName, final Class entityClass, final SQL fetchSql, final Column dependsOnColum) { - if ( entityAliasProxy != null ) { - throw new IllegalStateException("there is already an entityAliasProxy: " + entityAliasProxy); + if ( rootEntityAliasProxy != null ) { + throw new IllegalStateException("there is already an entityAliasProxy: " + rootEntityAliasProxy); } - entityAliasProxy = importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum); + rootEntityAliasProxy = importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum); return this; } @@ -135,7 +135,7 @@ public class RbacView { private RbacView importAsAlias(final String aliasName, final RbacView importedRbacView) { final var mapper = new AliasNameMapper(importedRbacView, aliasName); importedRbacView.getEntityAliases().values().stream() - .filter(entityAlias -> !importedRbacView.isMainEntityAlias(entityAlias)) + .filter(entityAlias -> !importedRbacView.isRootEntityAlias(entityAlias)) .filter(entityAlias -> !entityAlias.isGlobal()) .forEach(entityAlias -> { final String mappedAliasName = mapper.map(entityAlias.aliasName); @@ -176,12 +176,12 @@ public class RbacView { return findOrCreateGrantDef(subRoleDefinition, superRoleDefinition).toCreate(); } - boolean isMainEntityAlias(final EntityAlias entityAlias) { - return entityAlias == this.entityAlias; + boolean isRootEntityAlias(final EntityAlias entityAlias) { + return entityAlias == this.rootEntityAlias; } public boolean isEntityAliasProxy(final EntityAlias entityAlias) { - return entityAlias == entityAliasProxy; + return entityAlias == rootEntityAliasProxy; } public class RbacGrantBuilder { @@ -545,7 +545,7 @@ public class RbacView { } String map(final String originalAliasName) { - if (originalAliasName.equals(importedRbacView.entityAlias.aliasName) ) { + if (originalAliasName.equals(importedRbacView.rootEntityAlias.aliasName) ) { return outerAliasName; } if (originalAliasName.equals("global") ) { 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 8e2e6e57..04cfbbe6 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java @@ -36,7 +36,7 @@ public class RbacViewMermaidFlowchart { } private void renderEntitySubgraph(final RbacView.EntityAlias entity) { - final var color = rbacDef.isMainEntityAlias(entity) ? HOSTSHARING_ORANGE : HOSTSHARING_LIGHTBLUE; + final var color = rbacDef.isRootEntityAlias(entity) ? HOSTSHARING_ORANGE : HOSTSHARING_LIGHTBLUE; flowchart.writeLn(""" subgraph %{aliasName}["`**%{aliasName}**`"] @@ -63,8 +63,8 @@ public class RbacViewMermaidFlowchart { .map(p -> " " + permDef(p) ) .collect(joining("\n"))); - if (rbacDef.isMainEntityAlias(entity) && rbacDef.getEntityAliasProxy() != null ) { - renderEntitySubgraph(rbacDef.getEntityAliasProxy()); + if (rbacDef.isRootEntityAlias(entity) && rbacDef.getRootEntityAliasProxy() != null ) { + renderEntitySubgraph(rbacDef.getRootEntityAliasProxy()); } }); @@ -133,7 +133,7 @@ public class RbacViewMermaidFlowchart { } void generateToMarkdownFile() throws IOException { - final Path path = Paths.get("doc", rbacDef.getEntityAlias().simpleName() + ".md"); + final Path path = Paths.get("doc", rbacDef.getRootEntityAlias().simpleName() + ".md"); Files.writeString( path, """ @@ -143,7 +143,7 @@ public class RbacViewMermaidFlowchart { %{flowchart} ``` """ - .replace("%{entityAlias}", rbacDef.getEntityAlias().aliasName()) + .replace("%{entityAlias}", rbacDef.getRootEntityAlias().aliasName()) .replace("%{timestamp}", LocalDateTime.now().toString()) .replace("%{flowchart}", flowchart.toString()), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); 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 6079ba3b..a12e06d5 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java @@ -18,7 +18,7 @@ public class RbacViewPostgresGenerator { public RbacViewPostgresGenerator(final RbacView forRbacDef) { rbacDef = forRbacDef; - liqibaseTagPrefix = rbacDef.getEntityAlias().entityClass().getSimpleName(); + liqibaseTagPrefix = rbacDef.getRootEntityAlias().entityClass().getSimpleName(); plPgSql.append(""" --liquibase formatted sql -- generated at: %{timestamp} 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 75501578..d12ca747 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java @@ -29,7 +29,7 @@ class RolesGrantsAndPermissionsGenerator { this.rbacGrants.addAll(rbacGrants); this.liquibaseTagPrefix = liquibaseTagPrefix; - entityClass = rbacDef.getEntityAlias().entityClass(); + entityClass = rbacDef.getRootEntityAlias().entityClass(); simpleEntityName = entityClass.getSimpleName(); simpleEntityVarName = uncapitalize(simpleEntityName); rawTableName = withoutRvSuffix(entityClass.getAnnotation(Table.class).name()); @@ -89,7 +89,7 @@ class RolesGrantsAndPermissionsGenerator { private void createRolesWithGrantsSql(final StringBuilder plPgSql, final RbacView.Role role) { final var isToCreate = rbacDef.getRoleDefs().stream() - .filter(roleDef -> rbacDef.isMainEntityAlias(roleDef.getEntityAlias()) && roleDef.getRole() == role ) + .filter(roleDef -> rbacDef.isRootEntityAlias(roleDef.getEntityAlias()) && roleDef.getRole() == role ) .findFirst().map(RbacView.RbacRoleDefinition::isToCreate).orElse(false); if (!isToCreate) { return; @@ -103,7 +103,7 @@ class RolesGrantsAndPermissionsGenerator { .replace("%{simpleEntityVarName)", simpleEntityVarName) .replace("%{roleSuffix}", capitalize(role.roleName()))); - final var permissionGrantsForRole = findPermissionsGrantsForRole(rbacDef.getEntityAlias(), role); + final var permissionGrantsForRole = findPermissionsGrantsForRole(rbacDef.getRootEntityAlias(), role); if (!permissionGrantsForRole.isEmpty()) { final var permissionsForRoleInPlPgSql = permissionGrantsForRole.stream() .map(RbacView.RbacGrantDefinition::getPermDef) @@ -115,7 +115,7 @@ class RolesGrantsAndPermissionsGenerator { rbacGrants.removeAll(permissionGrantsForRole); } - final var grantsToUsers = findGrantsToUserForRole(rbacDef.getEntityAlias(), role); + final var grantsToUsers = findGrantsToUserForRole(rbacDef.getRootEntityAlias(), role); if (!grantsToUsers.isEmpty()) { final var grantsToUsersPlPgSql = grantsToUsers.stream() .map(RbacView.RbacGrantDefinition::getUserDef) @@ -125,7 +125,7 @@ class RolesGrantsAndPermissionsGenerator { rbacGrants.removeAll(grantsToUsers); } - final var incomingGrants = findIncomingSuperRolesForRole(rbacDef.getEntityAlias(), role); + final var incomingGrants = findIncomingSuperRolesForRole(rbacDef.getRootEntityAlias(), role); if (!incomingGrants.isEmpty()) { final var incomingGrantsInPlPgSql = incomingGrants.stream() .map(RbacView.RbacGrantDefinition::getSuperRoleDef) @@ -135,7 +135,7 @@ class RolesGrantsAndPermissionsGenerator { rbacGrants.removeAll(incomingGrants); } - final var outgoingGrants = findOutgoingSuperRolesForRole(rbacDef.getEntityAlias(), role); + final var outgoingGrants = findOutgoingSuperRolesForRole(rbacDef.getRootEntityAlias(), role); if (!outgoingGrants.isEmpty()) { final var outgoingGrantsInPlPgSql = outgoingGrants.stream() .map(RbacView.RbacGrantDefinition::getSuperRoleDef) @@ -218,7 +218,7 @@ class RolesGrantsAndPermissionsGenerator { private String toPlPgSqlReference(final PostgresTriggerReference triggerRef, final RbacView.RbacRoleDefinition roleDef) { return toVar(roleDef) + (roleDef.getEntityAlias().isGlobal() ? "()" - : rbacDef.isMainEntityAlias(roleDef.getEntityAlias()) ? ("(" + triggerRef.name() + ")") + : rbacDef.isRootEntityAlias(roleDef.getEntityAlias()) ? ("(" + triggerRef.name() + ")") : "(" + toTriggerReference(triggerRef, roleDef.getEntityAlias()) + ")"); } -- 2.39.5 From d7f0727efe149c668c5d3d247145d3018c4604d1 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 26 Feb 2024 11:19:38 +0100 Subject: [PATCH 13/53] fix relationship holderPerson-role --- .../hs/office/relationship/HsOfficeRelationshipEntity.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 3637a0ac..80cd5607 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 @@ -103,9 +103,10 @@ public class HsOfficeRelationshipEntity implements HasUuid, Stringifyable { .createSubRole(ADMIN, (with) -> { with.permission(EDIT); }) - .createSubRole(AGENT) + .createSubRole(AGENT, (with) -> { + with.incomingSuperRole("holderPerson", ADMIN); + }) .createSubRole(TENANT, (with) -> { - with.incomingSuperRole("anchorPerson", ADMIN); with.incomingSuperRole("holderPerson", ADMIN); with.incomingSuperRole("contact", ADMIN); with.outgoingSubRole("anchorPerson", REFERRER); -- 2.39.5 From 86cf4f6c977f9bc45f7085d849f748c113fe089f Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 26 Feb 2024 13:35:39 +0100 Subject: [PATCH 14/53] use StringWriter in Postgres generator --- .../rbacdef/RbacViewPostgresGenerator.java | 5 +- .../RolesGrantsAndPermissionsGenerator.java | 173 +++++++++--------- 2 files changed, 86 insertions(+), 92 deletions(-) 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 a12e06d5..21257a24 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java @@ -14,15 +14,14 @@ public class RbacViewPostgresGenerator { private final RbacView rbacDef; private final String liqibaseTagPrefix; - private final StringBuilder plPgSql = new StringBuilder(); + private final StringWriter plPgSql = new StringWriter(); public RbacViewPostgresGenerator(final RbacView forRbacDef) { rbacDef = forRbacDef; liqibaseTagPrefix = rbacDef.getRootEntityAlias().entityClass().getSimpleName(); - plPgSql.append(""" + plPgSql.writeLn(""" --liquibase formatted sql -- generated at: %{timestamp} - """ .replace("%{timestamp}", LocalDateTime.now().toString())); 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 d12ca747..27e84a3b 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java @@ -26,34 +26,33 @@ class RolesGrantsAndPermissionsGenerator { RolesGrantsAndPermissionsGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) { this.rbacDef = rbacDef; - this.rbacGrants.addAll(rbacGrants); + this.rbacGrants.addAll(rbacDef.getGrantDefs()); this.liquibaseTagPrefix = liquibaseTagPrefix; entityClass = rbacDef.getRootEntityAlias().entityClass(); - simpleEntityName = entityClass.getSimpleName(); - simpleEntityVarName = uncapitalize(simpleEntityName); + simpleEntityVarName = rbacDef.getRootEntityAlias().simpleName(); + simpleEntityName = capitalize(simpleEntityVarName); rawTableName = withoutRvSuffix(entityClass.getAnnotation(Table.class).name()); } - void generateTo(final StringBuilder plPgSql) { + void generateTo(final StringWriter plPgSql) { generateHeader(plPgSql); generateTriggerFunction(plPgSql); generageInsertTrigger(plPgSql); generateFooter(plPgSql); } - private void generateHeader(final StringBuilder plPgSql) { - plPgSql.append(""" + private void generateHeader(final StringWriter plPgSql) { + plPgSql.writeLn(""" -- ============================================================================ --changeset %{liquibaseTagPrefix}-rbac-CREATE-ROLES-GRANTS-PERMISSIONS:1 endDelimiter:--// -- ---------------------------------------------------------------------------- - """ .replace("%{liquibaseTagPrefix}", liquibaseTagPrefix)); } - private void generateTriggerFunction(final StringBuilder plPgSql) { - plPgSql.append(""" + private void generateTriggerFunction(final StringWriter plPgSql) { + plPgSql.writeLn(""" /* Creates the roles, grants and permission for the AFTER INSERT TRIGGER. */ @@ -66,27 +65,30 @@ class RolesGrantsAndPermissionsGenerator { if TG_OP <> 'INSERT' then raise exception 'invalid usage of TRIGGER AFTER INSERT function'; end if; - %{createRolesWithGrantsSql} - return NEW; - end; $$; """ - .replace("%{simpleEntityName}", simpleEntityName) - .replace("%{createRolesWithGrantsSql}", createRolesWithGrantsSql()) - ); + .replace("%{simpleEntityName}", simpleEntityName)); + plPgSql.indented(() -> { + createRolesWithGrantsSql(plPgSql, OWNER); + createRolesWithGrantsSql(plPgSql, ADMIN); + createRolesWithGrantsSql(plPgSql, AGENT); + createRolesWithGrantsSql(plPgSql, TENANT); + createRolesWithGrantsSql(plPgSql, REFERRER); + + if (!rbacGrants.isEmpty()) { + throw new IllegalStateException("unprocessed grants: " + rbacGrants); + // rbacGrants.forEach(g -> plPgSql.writeLn("-- unprocessed grant: " + g)); + } + + plPgSql.writeLn("return NEW;"); + }); + + plPgSql.writeLn("end; $$;"); + plPgSql.writeLn(); } - private String createRolesWithGrantsSql() { - final var plPgSql = new StringBuilder(); - createRolesWithGrantsSql(plPgSql, OWNER); - createRolesWithGrantsSql(plPgSql, ADMIN); - createRolesWithGrantsSql(plPgSql, AGENT); - createRolesWithGrantsSql(plPgSql, TENANT); - createRolesWithGrantsSql(plPgSql, REFERRER); - return plPgSql.toString(); - } - private void createRolesWithGrantsSql(final StringBuilder plPgSql, final RbacView.Role role) { + private void createRolesWithGrantsSql(final StringWriter plPgSql, final RbacView.Role role) { final var isToCreate = rbacDef.getRoleDefs().stream() .filter(roleDef -> rbacDef.isRootEntityAlias(roleDef.getEntityAlias()) && roleDef.getRole() == role ) @@ -95,62 +97,64 @@ class RolesGrantsAndPermissionsGenerator { return; } - plPgSql.append(""" - - perform createRoleWithGrants( - %{simpleEntityVarName)%{roleSuffix}(NEW), - """ - .replace("%{simpleEntityVarName)", simpleEntityVarName) - .replace("%{roleSuffix}", capitalize(role.roleName()))); + plPgSql.writeLn(); + plPgSql.writeLn("perform createRoleWithGrants("); + plPgSql.indented( () -> { + plPgSql.writeLn("%{simpleVarName)%{roleSuffix}(NEW)," + .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.append(indent(3) + "permissions => array[" + permissionsForRoleInPlPgSql + "],\n"); - rbacGrants.removeAll(permissionGrantsForRole); - } + 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); + } - 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.append(indent(3) + "userUuids => array[" + grantsToUsersPlPgSql + "],\n"); - rbacGrants.removeAll(grantsToUsers); - } + 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); + } - 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.append(indent(3) + "incomingSuperRoles => array[" + incomingGrantsInPlPgSql + "],\n"); - rbacGrants.removeAll(incomingGrants); - } + 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); + } - 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.append(indent(3) + "outgoingSubRoles => array[" + outgoingGrantsInPlPgSql + "],\n"); - rbacGrants.removeAll(outgoingGrants); - } + 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); + } - if (!rbacGrants.isEmpty()) { - throw new IllegalStateException("unprocessed grants: " + rbacGrants); - } + plPgSql.chopTail(",\n"); + plPgSql.writeLn(); + }); - chopTail(plPgSql, ",\n"); - plPgSql.append("\n" + indent(2) + ");\n"); + plPgSql.writeLn(");"); } private Set findPermissionsGrantsForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { @@ -182,8 +186,8 @@ class RolesGrantsAndPermissionsGenerator { .collect(Collectors.toSet()); } - private void generageInsertTrigger(final StringBuilder plPgSql) { - plPgSql.append(""" + private void generageInsertTrigger(final StringWriter plPgSql) { + plPgSql.writeLn(""" /* An AFTER INSERT TRIGGER which creates the role structure for a new %{simpleEntityName} */ @@ -200,8 +204,9 @@ class RolesGrantsAndPermissionsGenerator { ); } - private static void generateFooter(final StringBuilder plPgSql) { - plPgSql.append("\n\n"); + private static void generateFooter(final StringWriter plPgSql) { + plPgSql.writeLn(); + plPgSql.writeLn(); } private String withoutRvSuffix(final String tableName) { @@ -229,14 +234,4 @@ class RolesGrantsAndPermissionsGenerator { private static String toTriggerReference(final PostgresTriggerReference triggerRef, final RbacView.EntityAlias entityAlias) { return triggerRef.name().toLowerCase() + capitalize(entityAlias.aliasName()); } - - private String indent(final int tabs) { - return " ".repeat(4*tabs); - } - - private void chopTail(final StringBuilder plPgSql, final String tail) { - if (plPgSql.toString().endsWith(tail)) { - plPgSql.setLength(plPgSql.length() - tail.length()); - } - } } -- 2.39.5 From faf6710ef159a9eeedfc1ce49b62ec870565c91a Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 26 Feb 2024 15:48:03 +0100 Subject: [PATCH 15/53] generates Postgres for Relationship --- .../rbacdef/RbacViewPostgresGenerator.java | 18 ++-- .../RolesGrantsAndPermissionsGenerator.java | 96 ++++++++++++++----- 2 files changed, 81 insertions(+), 33 deletions(-) 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 21257a24..bae8d498 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java @@ -1,9 +1,10 @@ package net.hostsharing.hsadminng.rbac.rbacdef; -import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity; +import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.time.LocalDateTime; @@ -32,15 +33,18 @@ public class RbacViewPostgresGenerator { @Override -public String toString() { + public String toString() { return plPgSql.toString(); } -public static void main(String[] args) throws IOException { + public static void main(String[] args) throws IOException { + final var rbac = HsOfficeRelationshipEntity.rbac(); + final Path outputPath = Paths.get("doc", rbac.getRootEntityAlias().simpleName() + ".sql"); + Files.writeString( + outputPath, + new RbacViewPostgresGenerator(rbac).toString(), + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - Files.writeString( - Paths.get("doc", "hsOfficeBankAccount.sql"), - new RbacViewPostgresGenerator(HsOfficeBankAccountEntity.rbac()).toString(), - StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + System.out.println(outputPath.toAbsolutePath()); } } 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 27e84a3b..ccb2988f 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java @@ -5,9 +5,8 @@ import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacPermissionDefinition; import jakarta.persistence.Table; import java.util.HashSet; import java.util.Set; -import java.util.stream.Collectors; -import static java.util.stream.Collectors.joining; +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.*; @@ -26,13 +25,15 @@ class RolesGrantsAndPermissionsGenerator { RolesGrantsAndPermissionsGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) { this.rbacDef = rbacDef; - this.rbacGrants.addAll(rbacDef.getGrantDefs()); + this.rbacGrants.addAll(rbacDef.getGrantDefs().stream() + .filter(RbacView.RbacGrantDefinition::isToCreate) + .collect(toSet())); this.liquibaseTagPrefix = liquibaseTagPrefix; entityClass = rbacDef.getRootEntityAlias().entityClass(); simpleEntityVarName = rbacDef.getRootEntityAlias().simpleName(); simpleEntityName = capitalize(simpleEntityVarName); - rawTableName = withoutRvSuffix(entityClass.getAnnotation(Table.class).name()); + rawTableName = getRawTableName(entityClass); } void generateTo(final StringWriter plPgSql) { @@ -45,10 +46,10 @@ class RolesGrantsAndPermissionsGenerator { private void generateHeader(final StringWriter plPgSql) { plPgSql.writeLn(""" -- ============================================================================ - --changeset %{liquibaseTagPrefix}-rbac-CREATE-ROLES-GRANTS-PERMISSIONS:1 endDelimiter:--// + --changeset ${liquibaseTagPrefix}-rbac-CREATE-ROLES-GRANTS-PERMISSIONS:1 endDelimiter:--// -- ---------------------------------------------------------------------------- """ - .replace("%{liquibaseTagPrefix}", liquibaseTagPrefix)); + .replace("${liquibaseTagPrefix}", liquibaseTagPrefix)); } private void generateTriggerFunction(final StringWriter plPgSql) { @@ -57,28 +58,53 @@ class RolesGrantsAndPermissionsGenerator { Creates the roles, grants and permission for the AFTER INSERT TRIGGER. */ - create or replace function createRbacRolesFor%{simpleEntityName}() + create or replace function createRbacRolesFor${simpleEntityName}() returns trigger language plpgsql strict as $$ + declare + """ + .replace("${simpleEntityName}", simpleEntityName)); + + plPgSql.indented(() -> { + rbacDef.getEntityAliases().values().stream() + .filter((ea) -> !rbacDef.isRootEntityAlias(ea)) + .filter((ea) -> ea.fetchSql() != null) + .forEach((ea) -> { + plPgSql.writeLn( entityRefVar(NEW, ea) + " " + getRawTableName(ea.entityClass()) + ";"); + }); + }); + + plPgSql.writeLn(""" begin if TG_OP <> 'INSERT' then raise exception 'invalid usage of TRIGGER AFTER INSERT function'; end if; - """ - .replace("%{simpleEntityName}", simpleEntityName)); + """); plPgSql.indented(() -> { + + plPgSql.writeLn(); + rbacDef.getEntityAliases().values().stream() + .filter((ea) -> !rbacDef.isRootEntityAlias(ea)) + .filter((ea) -> ea.fetchSql() != null) + .forEach((ea) -> { + plPgSql.writeLn( ea.fetchSql().sql + " into " + entityRefVar(NEW, ea) + ";"); + }); + createRolesWithGrantsSql(plPgSql, OWNER); createRolesWithGrantsSql(plPgSql, ADMIN); createRolesWithGrantsSql(plPgSql, AGENT); createRolesWithGrantsSql(plPgSql, TENANT); createRolesWithGrantsSql(plPgSql, REFERRER); - if (!rbacGrants.isEmpty()) { - throw new IllegalStateException("unprocessed grants: " + rbacGrants); - // rbacGrants.forEach(g -> plPgSql.writeLn("-- unprocessed grant: " + g)); - } + plPgSql.writeLn(); + rbacGrants + .forEach(g -> plPgSql.writeLn( + "call grantRoleToRole(${subRoleRef}, ${superRoleRef});" + .replace("${subRoleRef}", roleRef(NEW, g.getSubRoleDef()) ) + .replace("${superRoleRef}", roleRef(NEW, g.getSuperRoleDef()) )) + ); plPgSql.writeLn("return NEW;"); }); @@ -87,6 +113,24 @@ class RolesGrantsAndPermissionsGenerator { plPgSql.writeLn(); } + 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.getEntityAlias().isGlobal()) { + return "globalAdmin()"; + } + final String entityRefVar = entityRefVar(rootRefVar, roleDef.getEntityAlias()); + return roleDef.getEntityAlias().simpleName() + capitalize(roleDef.getRole().roleName()) + + "(" + entityRefVar + ")"; + } + + private static String entityRefVar( + final PostgresTriggerReference rootRefVar, + final RbacView.EntityAlias entityAlias) { + return rootRefVar.name().toLowerCase() + capitalize(entityAlias.aliasName()); + } private void createRolesWithGrantsSql(final StringWriter plPgSql, final RbacView.Role role) { @@ -100,9 +144,9 @@ class RolesGrantsAndPermissionsGenerator { plPgSql.writeLn(); plPgSql.writeLn("perform createRoleWithGrants("); plPgSql.indented( () -> { - plPgSql.writeLn("%{simpleVarName)%{roleSuffix}(NEW)," - .replace("%{simpleVarName)", simpleEntityVarName) - .replace("%{roleSuffix}", capitalize(role.roleName()))); + plPgSql.writeLn("${simpleVarName)${roleSuffix}(NEW)," + .replace("${simpleVarName)", simpleEntityVarName) + .replace("${roleSuffix}", capitalize(role.roleName()))); final var permissionGrantsForRole = findPermissionsGrantsForRole(rbacDef.getRootEntityAlias(), role); if (!permissionGrantsForRole.isEmpty()) { @@ -161,21 +205,21 @@ class RolesGrantsAndPermissionsGenerator { final var roleDef = rbacDef.findRbacRole(entityAlias, role); return rbacGrants.stream() .filter(g -> g.grantType() == ROLE_TO_PERM && g.getSuperRoleDef()==roleDef ) - .collect(Collectors.toSet()); + .collect(toSet()); } private Set findGrantsToUserForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { final var roleDef = rbacDef.findRbacRole(entityAlias, role); return rbacGrants.stream() .filter(g -> g.grantType() == USER_TO_ROLE && g.getSubRoleDef() == roleDef ) - .collect(Collectors.toSet()); + .collect(toSet()); } private Set findIncomingSuperRolesForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { final var roleDef = rbacDef.findRbacRole(entityAlias, role); return rbacGrants.stream() .filter(g -> g.grantType() == ROLE_TO_ROLE && g.getSubRoleDef()==roleDef ) - .collect(Collectors.toSet()); + .collect(toSet()); } private Set findOutgoingSuperRolesForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { @@ -183,24 +227,24 @@ class RolesGrantsAndPermissionsGenerator { return rbacGrants.stream() .filter(g -> g.grantType() == ROLE_TO_ROLE && g.getSuperRoleDef()==roleDef ) .filter(g -> g.getSubRoleDef().getEntityAlias() != entityAlias) - .collect(Collectors.toSet()); + .collect(toSet()); } private void generageInsertTrigger(final StringWriter plPgSql) { plPgSql.writeLn(""" /* - An AFTER INSERT TRIGGER which creates the role structure for a new %{simpleEntityName} + An AFTER INSERT TRIGGER which creates the role structure for a new ${simpleEntityName} */ - create trigger createRbacRolesFor%{simpleEntityName}_Trigger + create trigger createRbacRolesFor${simpleEntityName}_Trigger after insert - on %{rawTableName} + on ${rawTableName} for each row - execute procedure createRbacRolesFor%{simpleEntityName}(); + execute procedure createRbacRolesFor${simpleEntityName}(); --// """ - .replace("%{simpleEntityName}", simpleEntityName) - .replace("%{rawTableName}", rawTableName) + .replace("${simpleEntityName}", simpleEntityName) + .replace("${rawTableName}", rawTableName) ); } -- 2.39.5 From 4ba78a70c2de670f75d5cfb34af9e82268130aff Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 27 Feb 2024 10:11:22 +0100 Subject: [PATCH 16/53] fix TestCustomerEntity + Flowchart generator --- .../hsadminng/rbac/rbacdef/RbacView.java | 15 ++++--- .../rbacdef/RbacViewMermaidFlowchart.java | 31 ++++++++----- .../RolesGrantsAndPermissionsGenerator.java | 4 +- .../hsadminng/rbac/rbacdef/StringWriter.java | 4 +- .../test/cust/TestCustomerEntity.java | 2 +- .../test/cust/TestCustomerEntityTest.java | 45 ++++++++++--------- 6 files changed, 59 insertions(+), 42 deletions(-) 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 88cfa329..3f31ff08 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -212,9 +212,9 @@ public class RbacView { public String toString() { final var arrow = isAssumed() ? " --> " : " -- // --> "; return switch (grantType()) { - case USER_TO_ROLE -> userDef.toString() + arrow + subRoleDef.toString(); + case ROLE_TO_USER -> userDef.toString() + arrow + subRoleDef.toString(); case ROLE_TO_ROLE -> superRoleDef + arrow + subRoleDef; - case ROLE_TO_PERM -> superRoleDef + arrow + permDef; + case PERM_TO_ROLE -> superRoleDef + arrow + permDef; }; } @@ -248,8 +248,8 @@ public class RbacView { @NotNull GrantType grantType() { - return permDef != null ? GrantType.ROLE_TO_PERM - : userDef != null ? GrantType.USER_TO_ROLE + return permDef != null ? GrantType.PERM_TO_ROLE + : userDef != null ? GrantType.ROLE_TO_USER : GrantType.ROLE_TO_ROLE; } @@ -268,9 +268,9 @@ public class RbacView { } public enum GrantType { - USER_TO_ROLE, + ROLE_TO_USER, ROLE_TO_ROLE, - ROLE_TO_PERM + PERM_TO_ROLE } } @@ -511,6 +511,9 @@ public class RbacView { } public static SQL query(final String sql) { + if (sql.matches(";[ \t]*$")) { + throw new IllegalArgumentException("SQL expression must not end with ';': " + sql); + } return new SQL(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 04cfbbe6..634c4bf6 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java @@ -10,6 +10,7 @@ import java.nio.file.*; import java.time.LocalDateTime; import static java.util.stream.Collectors.joining; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.*; public class RbacViewMermaidFlowchart { @@ -24,7 +25,6 @@ public class RbacViewMermaidFlowchart { %%{init:{'flowchart':{'htmlLabels':false}}}%% flowchart TB """); - flowchart.writeLn(); renderEntitySubgraphs(); renderGrants(); } @@ -37,8 +37,7 @@ public class RbacViewMermaidFlowchart { private void renderEntitySubgraph(final RbacView.EntityAlias entity) { final var color = rbacDef.isRootEntityAlias(entity) ? HOSTSHARING_ORANGE : HOSTSHARING_LIGHTBLUE; - flowchart.writeLn(""" - + flowchart.writeLn(""" subgraph %{aliasName}["`**%{aliasName}**`"] direction TB style %{aliasName} fill:%{color},stroke:darkblue,stroke-width:8px @@ -54,13 +53,13 @@ public class RbacViewMermaidFlowchart { wrapOutputInSubgraph(entity.aliasName() + ":roles", color, rbacDef.getRoleDefs().stream() .filter(r -> r.getEntityAlias() == entity) - .map(r -> " " + roleDef(r)) + .map(this::roleDef) .collect(joining("\n"))); wrapOutputInSubgraph(entity.aliasName() + ":permissions", color, rbacDef.getPermDefs().stream() .filter(p -> p.getEntityAlias() == entity) - .map(p -> " " + permDef(p) ) + .map(this::permDef) .collect(joining("\n"))); if (rbacDef.isRootEntityAlias(entity) && rbacDef.getRootEntityAliasProxy() != null ) { @@ -91,10 +90,20 @@ public class RbacViewMermaidFlowchart { } private void renderGrants() { - rbacDef.getGrantDefs() - .forEach(g -> { - flowchart.writeLn(grantDef(g) + "\n"); - }); + renderGrants(ROLE_TO_USER, "%% granting roles to users"); + renderGrants(ROLE_TO_ROLE, "%% granting roles to roles"); + 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) + .toList(); + if ( !userGrants.isEmpty()) { + flowchart.emptyLine(); + flowchart.writeLn(t); + userGrants.forEach(g -> flowchart.writeLn(grantDef(g))); + } } private String grantDef(final RbacView.RbacGrantDefinition grant) { @@ -102,12 +111,12 @@ public class RbacViewMermaidFlowchart { ? grant.isAssumed() ? " ==> " : " == // ==> " : grant.isAssumed() ? " -.-> " : " -.- // -.-> "; return switch (grant.grantType()) { - case USER_TO_ROLE -> + case ROLE_TO_USER -> // TODO: other user types not implemented yet "user:creator" + arrow + roleId(grant.getSubRoleDef()); case ROLE_TO_ROLE -> roleId(grant.getSuperRoleDef()) + arrow + roleId(grant.getSubRoleDef()); - case ROLE_TO_PERM -> roleId(grant.getSuperRoleDef()) + arrow + permId(grant.getPermDef()); + case PERM_TO_ROLE -> roleId(grant.getSuperRoleDef()) + arrow + permId(grant.getPermDef()); }; } 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 ccb2988f..7921cae6 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java @@ -204,14 +204,14 @@ class RolesGrantsAndPermissionsGenerator { private Set findPermissionsGrantsForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { final var roleDef = rbacDef.findRbacRole(entityAlias, role); return rbacGrants.stream() - .filter(g -> g.grantType() == ROLE_TO_PERM && g.getSuperRoleDef()==roleDef ) + .filter(g -> g.grantType() == PERM_TO_ROLE && g.getSuperRoleDef()==roleDef ) .collect(toSet()); } private Set findGrantsToUserForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { final var roleDef = rbacDef.findRbacRole(entityAlias, role); return rbacGrants.stream() - .filter(g -> g.grantType() == USER_TO_ROLE && g.getSubRoleDef() == roleDef ) + .filter(g -> g.grantType() == ROLE_TO_USER && g.getSubRoleDef() == roleDef ) .collect(toSet()); } 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 9b0bfc7a..6e615dba 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/StringWriter.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/StringWriter.java @@ -21,11 +21,11 @@ public class StringWriter { private String indented(final String text) { if ( indentLevel == 0) { - return text.trim(); + return text; } final var indentation = StringUtils.repeat(" ", indentLevel); final var indented = stream(text.split("\n")) - .map(line -> line.trim().isBlank() ? "" : indentation + line.trim()) + .map(line -> line.trim().isBlank() ? "" : indentation + line) .collect(joining("\n")); return indented; } 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 fecee7b7..80bdf940 100644 --- a/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java @@ -38,7 +38,7 @@ public class TestCustomerEntity implements RbacObject { public static RbacView rbac() { - return rbacViewFor("contact", TestCustomerEntity.class) + return rbacViewFor("customer", TestCustomerEntity.class) .withIdentityView(RbacView.SQL.query("target.prefix")) .withUpdatableColumns("reference", "prefix", "adminUserName") .createRole(OWNER, (with) -> { 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 10f1c4ce..09aeb1f8 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityTest.java @@ -10,38 +10,43 @@ class TestCustomerEntityTest { @Test void definesRbac() { final var rbacFlowchart = new RbacViewMermaidFlowchart(TestCustomerEntity.rbac()).toString(); - assertThat(rbacFlowchart).isEqualToIgnoringWhitespace(""" + assertThat(rbacFlowchart).isEqualTo(""" %%{init:{'flowchart':{'htmlLabels':false}}}%% flowchart TB - subgraph contact["`**contact**`"] + subgraph customer["`**customer**`"] direction TB - style contact fill:#dd4901,stroke:darkblue,stroke-width:8px + style customer fill:#dd4901,stroke:darkblue,stroke-width:8px - subgraph contact:roles[ ] - style contact:roles fill: #dd4901 + subgraph customer:roles[ ] + style customer:roles fill: #dd4901 - role:contact:owner[[contact:owner]] - role:contact:admin[[contact:admin]] - role:contact:tenant[[contact:tenant]] + role:customer:owner[[customer:owner]] + role:customer:admin[[customer:admin]] + role:customer:tenant[[customer:tenant]] end - subgraph contact:permissions[ ] - style contact:permissions fill: #dd4901 + subgraph customer:permissions[ ] + style customer:permissions fill: #dd4901 - perm:contact:*{{contact:*}} - perm:contact:add-package{{contact:add-package}} - perm:contact:view{{contact:view}} + perm:customer:*{{customer:*}} + perm:customer:add-package{{customer:add-package}} + perm:customer:view{{customer:view}} end end - user:creator ==> role:contact:owner - role:global:admin ==> role:contact:owner - role:contact:owner ==> perm:contact:* - role:contact:owner ==> role:contact:admin - role:contact:admin ==> perm:contact:add-package - role:contact:admin ==> role:contact:tenant - role:contact:tenant ==> perm:contact:view + %% granting roles to users + user:creator ==> role:customer:owner + + %% granting roles to roles + role:global:admin ==> role:customer:owner + role:customer:owner ==> role:customer:admin + role:customer:admin ==> role:customer:tenant + + %% granting permissions to roles + role:customer:owner ==> perm:customer:* + role:customer:admin ==> perm:customer:add-package + role:customer:tenant ==> perm:customer:view """); } } -- 2.39.5 From 12010b4dae68f1fe96fbc0ddb72459943b9edf50 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 27 Feb 2024 12:34:52 +0100 Subject: [PATCH 17/53] rbacDef for HsOfficePartnerEntity and related amendments of generators --- .../office/debitor/HsOfficeDebitorEntity.java | 13 ++++---- .../office/partner/HsOfficePartnerEntity.java | 30 +++++++++++++++++ .../HsOfficeRelationshipEntity.java | 2 +- .../hsadminng/rbac/rbacdef/RbacView.java | 2 +- .../rbacdef/RbacViewMermaidFlowchart.java | 2 ++ .../rbacdef/RbacViewPostgresGenerator.java | 14 +++++--- .../RolesGrantsAndPermissionsGenerator.java | 33 ++++++++++++++----- 7 files changed, 75 insertions(+), 21 deletions(-) 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 8905a34f..5c9c2fdb 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 @@ -8,6 +8,7 @@ import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity; 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; import org.hibernate.annotations.GenericGenerator; @@ -107,7 +108,7 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable { public static RbacView rbac() { return rbacViewFor("debitor", HsOfficeDebitorEntity.class) - .withIdentityView(RbacView.SQL.query(""" + .withIdentityView(SQL.query(""" SELECT debitor.uuid, 'D-' || (SELECT partner.partnerNumber FROM hs_office_partner partner @@ -117,7 +118,7 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable { ON debitorRel.relAnchorUuid = partnerRel.relHolderUuid AND partnerRel.relType = 'ACCOUNTING' WHERE debitorRel.uuid = debitor.debitorRelUuid) || to_char(debitorNumberSuffix, 'fm00') - from hs_office_debitor as debitor; + from hs_office_debitor as debitor """)) .withUpdatableColumns( "debitorRel", @@ -131,11 +132,11 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable { "defaultPrefix" /* TODO: do we want that updatable? */) .createPermission(custom("new-debitor")).grantedTo("global", ADMIN).pop() - .importProxyEntity("debitorRel", HsOfficeRelationshipEntity.class, + .importRootEntityAliasProxy("debitorRel", HsOfficeRelationshipEntity.class, fetchedBySql(""" SELECT * FROM hs_office_relationship AS r - WHERE r.relType = 'ACCOUNTING' AND r.relHolderUuid = ${REF}.debitorRelUuid; + WHERE r.relType = 'ACCOUNTING' AND r.relHolderUuid = ${REF}.debitorRelUuid """), dependsOnColumn("debitorRelUuid")) .createPermission(ALL).grantedTo("debitorRel", OWNER).pop() @@ -146,7 +147,7 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable { fetchedBySql(""" SELECT * FROM hs_office_relationship AS r - WHERE r.relType = 'ACCOUNTING' AND r.relHolderUuid = ${REF}.debitorRelUuid; + WHERE r.relType = 'ACCOUNTING' AND r.relHolderUuid = ${REF}.debitorRelUuid """), dependsOnColumn("bankAccountUuid")) .toRole("refundBankAccount", ADMIN).grantRole("debitorRel", AGENT) @@ -156,7 +157,7 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable { fetchedBySql(""" SELECT * FROM hs_office_relationship AS partnerRel - WHERE ${debitorRel}.relAnchorUuid = partnerRel.relHolderUuid; + WHERE ${debitorRel}.relAnchorUuid = partnerRel.relHolderUuid """), dependsOnColumn("debitorRelUuid")) .toRole("partnerRel", ADMIN).grantRole("debitorRel", ADMIN) 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 342b601c..b4afb080 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 @@ -6,6 +6,7 @@ import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity; 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.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; import org.hibernate.annotations.NotFound; @@ -15,6 +16,12 @@ import jakarta.persistence.*; import java.util.Optional; 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.Permission.VIEW; +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 @@ -68,4 +75,27 @@ public class HsOfficePartnerEntity implements Stringifyable, HasUuid { public String toShortString() { return Optional.ofNullable(person).map(HsOfficePersonEntity::toShortString).orElse(""); } + + 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$) + """)) + .withUpdatableColumns( + "partnerRoleUuid", + "personUuid", + "contactUuid") + .createPermission(custom("new-partner")).grantedTo("global", ADMIN).pop() + + .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(); + } } 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 80cd5607..eb433725 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 @@ -97,10 +97,10 @@ public class HsOfficeRelationshipEntity implements HasUuid, Stringifyable { .createRole(OWNER, (with) -> { with.owningUser(CREATOR); with.incomingSuperRole(GLOBAL, ADMIN); - with.incomingSuperRole("anchorPerson", ADMIN); with.permission(ALL); }) .createSubRole(ADMIN, (with) -> { + with.incomingSuperRole("anchorPerson", ADMIN); with.permission(EDIT); }) .createSubRole(AGENT, (with) -> { 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 3f31ff08..b8ae11e4 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -101,7 +101,7 @@ public class RbacView { return this; } - public RbacView importProxyEntity( + public RbacView importRootEntityAliasProxy( final String aliasName, final Class entityClass, final SQL fetchSql, final Column dependsOnColum) { if ( rootEntityAliasProxy != null ) { throw new IllegalStateException("there is already an entityAliasProxy: " + rootEntityAliasProxy); 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 634c4bf6..113bea26 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.HsOfficePartnerEntity; import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity; import org.apache.commons.lang3.StringUtils; @@ -162,6 +163,7 @@ public class RbacViewMermaidFlowchart { 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(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 bae8d498..f27e238f 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java @@ -1,5 +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 java.io.IOException; @@ -26,19 +28,15 @@ public class RbacViewPostgresGenerator { """ .replace("%{timestamp}", LocalDateTime.now().toString())); - // generateSqlForRelatedRbacObject(); - new RolesGrantsAndPermissionsGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql); } - @Override public String toString() { return plPgSql.toString(); } - public static void main(String[] args) throws IOException { - final var rbac = HsOfficeRelationshipEntity.rbac(); + private static void generatePostgres(final RbacView rbac) throws IOException { final Path outputPath = Paths.get("doc", rbac.getRootEntityAlias().simpleName() + ".sql"); Files.writeString( outputPath, @@ -47,4 +45,10 @@ public class RbacViewPostgresGenerator { 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 7921cae6..cfe64710 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java @@ -84,12 +84,11 @@ class RolesGrantsAndPermissionsGenerator { plPgSql.indented(() -> { - plPgSql.writeLn(); rbacDef.getEntityAliases().values().stream() .filter((ea) -> !rbacDef.isRootEntityAlias(ea)) .filter((ea) -> ea.fetchSql() != null) .forEach((ea) -> { - plPgSql.writeLn( ea.fetchSql().sql + " into " + entityRefVar(NEW, ea) + ";"); + plPgSql.writeLn( ea.fetchSql().sql.replace("${ref}", NEW.name()) + " into " + entityRefVar(NEW, ea) + ";"); }); createRolesWithGrantsSql(plPgSql, OWNER); @@ -99,12 +98,7 @@ class RolesGrantsAndPermissionsGenerator { createRolesWithGrantsSql(plPgSql, REFERRER); plPgSql.writeLn(); - rbacGrants - .forEach(g -> plPgSql.writeLn( - "call grantRoleToRole(${subRoleRef}, ${superRoleRef});" - .replace("${subRoleRef}", roleRef(NEW, g.getSubRoleDef()) ) - .replace("${superRoleRef}", roleRef(NEW, g.getSuperRoleDef()) )) - ); + rbacGrants.forEach(g -> plPgSql.writeLn(generateGrant(g))); plPgSql.writeLn("return NEW;"); }); @@ -113,11 +107,34 @@ class RolesGrantsAndPermissionsGenerator { plPgSql.writeLn(); } + 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}));" + .replace("${subRoleRef}", roleRef(NEW, grantDef.getSubRoleDef()) ) + .replace("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef()) ); + 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}']" + .replace("${entityRef}", rbacDef.isRootEntityAlias(permDef.entityAlias) + ? ref.name() + : "not implemented yet" ) // TODO + .replace("${perm}", permDef.permission.permission()); + } + 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"); + } if ( roleDef.getEntityAlias().isGlobal()) { return "globalAdmin()"; } -- 2.39.5 From 4bef9391e1fc0dcaf1fadc21b1ced213111b1f98 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 27 Feb 2024 12:41:29 +0100 Subject: [PATCH 18/53] fix some warnings --- .../hostsharing/hsadminng/rbac/rbacdef/RbacView.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) 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 b8ae11e4..5317b882 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -5,6 +5,7 @@ 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; import jakarta.validation.constraints.NotNull; @@ -102,7 +103,7 @@ public class RbacView { } public RbacView importRootEntityAliasProxy( - final String aliasName, final Class entityClass, final SQL fetchSql, final Column dependsOnColum) { + final String aliasName, final Class entityClass, final SQL fetchSql, final Column dependsOnColum) { if ( rootEntityAliasProxy != null ) { throw new IllegalStateException("there is already an entityAliasProxy: " + rootEntityAliasProxy); } @@ -111,18 +112,18 @@ public class RbacView { } public RbacView importEntityAlias( - final String aliasName, final Class entityClass, final SQL fetchSql, final Column dependsOnColum) { + final String aliasName, final Class entityClass, final SQL fetchSql, final Column dependsOnColum) { importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum); return this; } - private EntityAlias importEntityAliasImpl(final String aliasName, final Class entityClass, final SQL fetchSql, final Column dependsOnColum) { + private EntityAlias importEntityAliasImpl(final String aliasName, final Class entityClass, final SQL fetchSql, final Column dependsOnColum) { final var entityAlias = new EntityAlias(aliasName, entityClass, fetchSql, dependsOnColum); entityAliases.put(aliasName, entityAlias); try { importAsAlias(aliasName, rbacDefinition(entityClass)); - } catch ( final Exception exc) { - new RuntimeException("cannot import entity: " + entityClass, exc); + } catch ( final ReflectiveOperationException exc) { + throw new RuntimeException("cannot import entity: " + entityClass, exc); } return entityAlias; } -- 2.39.5 From e521c3c9c307a0f73a93c19995c530322bf4d3da Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 27 Feb 2024 16:59:47 +0100 Subject: [PATCH 19/53] added PartnerDetails as SubEntity and amend genertors accordingly --- .../office/debitor/HsOfficeDebitorEntity.java | 8 +- .../partner/HsOfficePartnerDetailsEntity.java | 44 ++++++++- .../office/partner/HsOfficePartnerEntity.java | 34 +++++-- .../hsadminng/rbac/rbacdef/RbacView.java | 94 ++++++++++++------- .../rbacdef/RbacViewMermaidFlowchart.java | 9 +- .../rbacdef/RbacViewPostgresGenerator.java | 13 ++- .../RolesGrantsAndPermissionsGenerator.java | 7 +- 7 files changed, 156 insertions(+), 53 deletions(-) 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()); } -- 2.39.5 From 59ea077a4efb218bff49eec46519a2ad1035e2ff Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 28 Feb 2024 10:04:20 +0100 Subject: [PATCH 20/53] stable and better readable order of generated grants --- .../rbac/rbacdef/RbacViewMermaidFlowchart.java | 17 +++++++++-------- .../RolesGrantsAndPermissionsGenerator.java | 15 ++++++++++++--- .../hsadminng/rbac/rbacdef/StringWriter.java | 2 +- 3 files changed, 22 insertions(+), 12 deletions(-) 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 fcc8a83f..9acf5df4 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java @@ -16,9 +16,10 @@ 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"; + public static final String HOSTSHARING_DARK_ORANGE = "#dd4901"; + public static final String HOSTSHARING_LIGHT_ORANGE = "#feb28c"; + public static final String HOSTSHARING_DARK_BLUE = "#274d6e"; + public static final String HOSTSHARING_LIGHT_BLUE = "#99bcdb"; private final RbacView rbacDef; private final StringWriter flowchart = new StringWriter(); @@ -39,9 +40,9 @@ public class RbacViewMermaidFlowchart { } private void renderEntitySubgraph(final RbacView.EntityAlias entity) { - final var color = rbacDef.isRootEntityAlias(entity) ? HOSTSHARING_ORANGE - : entity.isSubEntity() ? HOSTSHARING_ORANGE_LIGHT - : HOSTSHARING_LIGHTBLUE; + final var color = rbacDef.isRootEntityAlias(entity) ? HOSTSHARING_DARK_ORANGE + : entity.isSubEntity() ? HOSTSHARING_LIGHT_ORANGE + : HOSTSHARING_LIGHT_BLUE; flowchart.writeLn(""" subgraph %{aliasName}["`**%{aliasName}**`"] direction TB @@ -79,7 +80,7 @@ public class RbacViewMermaidFlowchart { private void wrapOutputInSubgraph(final String name, final String color, final String content) { if (!StringUtils.isEmpty(content)) { - flowchart.emptyLine(); + flowchart.ensureEmptyLine(); flowchart.writeLn("subgraph " + name + "[ ]\n"); flowchart.indented(() -> { flowchart.writeLn("style %{aliasName} fill: %{color}" @@ -105,7 +106,7 @@ public class RbacViewMermaidFlowchart { .filter(g -> g.grantType() == f) .toList(); if ( !userGrants.isEmpty()) { - flowchart.emptyLine(); + flowchart.ensureEmptyLine(); flowchart.writeLn(t); userGrants.forEach(g -> flowchart.writeLn(grantDef(g))); } 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 0ae74bd7..6801d5d4 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java @@ -97,9 +97,9 @@ class RolesGrantsAndPermissionsGenerator { createRolesWithGrantsSql(plPgSql, TENANT); 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))); + generateGrants(plPgSql, ROLE_TO_USER); + generateGrants(plPgSql, ROLE_TO_ROLE); + generateGrants(plPgSql, PERM_TO_ROLE); plPgSql.writeLn("return NEW;"); }); @@ -108,6 +108,15 @@ class RolesGrantsAndPermissionsGenerator { plPgSql.writeLn(); } + private void generateGrants(final StringWriter plPgSql, final RbacView.RbacGrantDefinition.GrantType grantType) { + plPgSql.ensureEmptyLine(); + rbacGrants.stream() + .filter(g -> g.grantType() == grantType) + .map(this::generateGrant) + .sorted() + .forEach(plPgSql::writeLn); + } + private String generateGrant(RbacView.RbacGrantDefinition grantDef) { return switch (grantDef.grantType()) { case ROLE_TO_USER -> throw new IllegalArgumentException("unexpected grant"); 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 6e615dba..9ee873ec 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/StringWriter.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/StringWriter.java @@ -58,7 +58,7 @@ public class StringWriter { }; } - void emptyLine() { + void ensureEmptyLine() { if (!string.toString().endsWith("\n\n")) { writeLn(); } -- 2.39.5 From dff9803dc34635fa89ebb00982394faa955de21c Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 28 Feb 2024 13:58:55 +0100 Subject: [PATCH 21/53] 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); -- 2.39.5 From fef6e1c01cd2698faf4837a2d06313d987ac185a Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 28 Feb 2024 14:54:58 +0100 Subject: [PATCH 22/53] split trigger function from the procedure which actually generates the groles and grants --- .../RolesGrantsAndPermissionsGenerator.java | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) 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 7b8aae8d..64d14ed6 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java @@ -40,7 +40,7 @@ class RolesGrantsAndPermissionsGenerator { void generateTo(final StringWriter plPgSql) { generateHeader(plPgSql); generateTriggerFunction(plPgSql); - generageInsertTrigger(plPgSql); + generateInsertTrigger(plPgSql); generateFooter(plPgSql); } @@ -56,16 +56,19 @@ class RolesGrantsAndPermissionsGenerator { private void generateTriggerFunction(final StringWriter plPgSql) { plPgSql.writeLn(""" /* - Creates the roles, grants and permission for the AFTER INSERT TRIGGER. + A Creates the roles, grants and permission for the AFTER INSERT TRIGGER. */ - create or replace function createRbacRolesFor${simpleEntityName}() - returns trigger - language plpgsql - strict as $$ + create or replace procedure createRbacRolesFor${simpleEntityName}( + TG_OP text, + OLD ${rawTableName}, + NEW ${rawTableName} + ) + language plpgsql as $$ declare """ - .replace("${simpleEntityName}", simpleEntityName)); + .replace("${simpleEntityName}", simpleEntityName) + .replace("${rawTableName}", rawTableName)); plPgSql.indented(() -> { rbacDef.getEntityAliases().values().stream() @@ -101,8 +104,6 @@ class RolesGrantsAndPermissionsGenerator { generateGrants(plPgSql, ROLE_TO_USER); generateGrants(plPgSql, ROLE_TO_ROLE); generateGrants(plPgSql, PERM_TO_ROLE); - - plPgSql.writeLn("return NEW;"); }); plPgSql.writeLn("end; $$;"); @@ -280,17 +281,26 @@ class RolesGrantsAndPermissionsGenerator { .collect(toSet()); } - private void generageInsertTrigger(final StringWriter plPgSql) { + private void generateInsertTrigger(final StringWriter plPgSql) { plPgSql.writeLn(""" /* An AFTER INSERT TRIGGER which creates the role structure for a new ${simpleEntityName} */ - create trigger createRbacRolesFor${simpleEntityName}_Trigger + create or replace function createRbacRolesFor${simpleEntityName}_tf() + returns trigger + language plpgsql + strict as $$ + begin + call createRbacRolesFor${simpleEntityName}(TG_OP, OLD, NEW); + return NEW; + end; $$; + + create trigger createRbacRolesFor${simpleEntityName}_tg after insert on ${rawTableName} for each row - execute procedure createRbacRolesFor${simpleEntityName}(); + execute procedure createRbacRolesFor${simpleEntityName}_tf(); --// """ .replace("${simpleEntityName}", simpleEntityName) -- 2.39.5 From 5276471adb3c6d3455a0affcc9980a5be6e046bc Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 28 Feb 2024 16:53:11 +0100 Subject: [PATCH 23/53] frame for update trigger --- .../HsOfficeSepaMandateEntity.java | 4 +- .../hsadminng/rbac/rbacdef/RbacView.java | 13 ++++ .../RolesGrantsAndPermissionsGenerator.java | 70 ++++++++++++++++--- 3 files changed, 78 insertions(+), 9 deletions(-) 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 aa0f925e..fec9c105 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 @@ -105,17 +105,19 @@ public class HsOfficeSepaMandateEntity implements Stringifyable, HasUuid { .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("bankAccount", REFERRER); with.outgoingSubRole("debitorRel", AGENT); }) .createSubRole(REFERRER, (with) -> { + with.incomingSuperRole("bankAccount", ADMIN); with.incomingSuperRole("debitorRel", AGENT); + with.outgoingSubRole("debitorRel", TENANT); with.permission(VIEW); }); } 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 42eb80da..dde18a66 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -13,6 +13,7 @@ import jakarta.validation.constraints.NotNull; import java.lang.reflect.InvocationTargetException; import java.util.*; +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; import static org.apache.commons.lang3.StringUtils.uncapitalize; @@ -300,6 +301,18 @@ public class RbacView { return this; } + boolean dependsOnColumn(final String columnName) { + return dependsRoleDefOnColumnName(this.superRoleDef, columnName) + || dependsRoleDefOnColumnName(this.subRoleDef, columnName); + } + + private Boolean dependsRoleDefOnColumnName(final RbacRoleDefinition superRoleDef, final String columnName) { + return ofNullable(superRoleDef) + .map(r -> r.getEntityAlias().dependsOnColum()) + .map(d -> columnName.equals(d.column)) + .orElse(false); + } + public enum GrantType { ROLE_TO_USER, ROLE_TO_ROLE, 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 64d14ed6..653702f7 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java @@ -4,10 +4,12 @@ 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 static java.util.stream.Collectors.*; import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.NEW; +import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.OLD; 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; @@ -79,12 +81,19 @@ class RolesGrantsAndPermissionsGenerator { }); }); - plPgSql.writeLn(""" - begin - if TG_OP <> 'INSERT' then - raise exception 'invalid usage of TRIGGER AFTER INSERT function'; - end if; - """); + plPgSql.indented(() -> { + plPgSql.writeLn("begin"); + generateCreateRolesAndGrantsAfterInsert(plPgSql); + generateUpdateRolesAndGrantsAfterUpdate(plPgSql); + plPgSql.ensureEmptyLine(); + plPgSql.writeLn("end; $$;"); + }); + plPgSql.writeLn(); + } + + private void generateCreateRolesAndGrantsAfterInsert(final StringWriter plPgSql) { + plPgSql.ensureEmptyLine(); + plPgSql.writeLn("if TG_OP = 'INSERT' then"); plPgSql.indented(() -> { @@ -106,8 +115,53 @@ class RolesGrantsAndPermissionsGenerator { generateGrants(plPgSql, PERM_TO_ROLE); }); - plPgSql.writeLn("end; $$;"); - plPgSql.writeLn(); + plPgSql.writeLn("end if;"); +} + + private void generateUpdateRolesAndGrantsAfterUpdate(final StringWriter plPgSql) { + plPgSql.ensureEmptyLine(); + plPgSql.writeLn("if 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) + ";"); + }); + + rbacDef.getEntityAliases().values().stream() + .map(RbacView.EntityAlias::dependsOnColum) + .filter(Objects::nonNull) + .filter(this::isUpdatable) + .map(c -> c.column) + .sorted() + .distinct() + .forEach(columnName -> { + plPgSql.writeLn(); + plPgSql.writeLn("if NEW." + columnName + " <> OLD." + columnName + " then"); + plPgSql.indented(() -> { + updateGrantsDependingOn(plPgSql, columnName); + }); + plPgSql.writeLn("end if;"); + }); + }); + + plPgSql.writeLn("end if;"); + } + + private boolean isUpdatable(final RbacView.Column c) { + return rbacDef.getUpdatableColumns().contains(c); + } + + private void updateGrantsDependingOn(final StringWriter plPgSql, final String columnName) { + rbacDef.getGrantDefs().stream() + .filter(RbacView.RbacGrantDefinition::isToCreate) + .filter(g -> g.dependsOnColumn(columnName)) + .forEach(g -> { + plPgSql.writeLn("-- TODO: " + g); + }); } private void generateGrants(final StringWriter plPgSql, final RbacView.RbacGrantDefinition.GrantType grantType) { -- 2.39.5 From bc33f1fd9da09c9373a279515a084bfca868e9d6 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 29 Feb 2024 11:19:35 +0100 Subject: [PATCH 24/53] only render the update trigger if there are any updatable entity aliases --- .../RolesGrantsAndPermissionsGenerator.java | 152 ++++++++++++------ 1 file changed, 106 insertions(+), 46 deletions(-) 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 653702f7..4347549f 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java @@ -6,8 +6,10 @@ import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.stream.Stream; -import static java.util.stream.Collectors.*; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toSet; import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.NEW; import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.OLD; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.*; @@ -32,7 +34,7 @@ class RolesGrantsAndPermissionsGenerator { .filter(RbacView.RbacGrantDefinition::isToCreate) .collect(toSet())); this.liquibaseTagPrefix = liquibaseTagPrefix; - + entityClass = rbacDef.getRootEntityAlias().entityClass(); simpleEntityVarName = rbacDef.getRootEntityAlias().simpleName(); simpleEntityName = capitalize(simpleEntityVarName); @@ -43,6 +45,9 @@ class RolesGrantsAndPermissionsGenerator { generateHeader(plPgSql); generateTriggerFunction(plPgSql); generateInsertTrigger(plPgSql); + if (hasAnyUpdatableEntityAliases()) { + generateUpdateTrigger(plPgSql); + } generateFooter(plPgSql); } @@ -61,7 +66,7 @@ class RolesGrantsAndPermissionsGenerator { A Creates the roles, grants and permission for the AFTER INSERT TRIGGER. */ - create or replace procedure createRbacRolesFor${simpleEntityName}( + create or replace procedure buildRbacSystemFor${simpleEntityName}( TG_OP text, OLD ${rawTableName}, NEW ${rawTableName} @@ -73,36 +78,45 @@ class RolesGrantsAndPermissionsGenerator { .replace("${rawTableName}", rawTableName)); plPgSql.indented(() -> { - rbacDef.getEntityAliases().values().stream() - .filter((ea) -> !rbacDef.isRootEntityAlias(ea)) - .filter((ea) -> ea.fetchSql() != null) + updatableEntityAliases() .forEach((ea) -> { - plPgSql.writeLn( entityRefVar(NEW, ea) + " " + getRawTableName(ea.entityClass()) + ";"); + plPgSql.writeLn(entityRefVar(NEW, ea) + " " + getRawTableName(ea.entityClass()) + ";"); }); - }); + }); plPgSql.indented(() -> { plPgSql.writeLn("begin"); + generateCreateRolesAndGrantsAfterInsert(plPgSql); - generateUpdateRolesAndGrantsAfterUpdate(plPgSql); + if (hasAnyUpdatableEntityAliases()) { + generateUpdateRolesAndGrantsAfterUpdate(plPgSql); + } + plPgSql.writeLn(""" + else + raise exception 'invalid usage of TRIGGER'; + end if; + """); plPgSql.ensureEmptyLine(); - plPgSql.writeLn("end; $$;"); }); + plPgSql.writeLn("end; $$;"); plPgSql.writeLn(); } + private boolean hasAnyUpdatableEntityAliases() { + return updatableEntityAliases().anyMatch(e -> true); + } + private void generateCreateRolesAndGrantsAfterInsert(final StringWriter plPgSql) { plPgSql.ensureEmptyLine(); plPgSql.writeLn("if TG_OP = 'INSERT' then"); plPgSql.indented(() -> { - rbacDef.getEntityAliases().values().stream() - .filter((ea) -> !rbacDef.isRootEntityAlias(ea)) - .filter((ea) -> ea.fetchSql() != null) + updatableEntityAliases() .forEach((ea) -> { - plPgSql.writeLn( ea.fetchSql().sql.replace("${ref}", NEW.name()) + " into " + entityRefVar(NEW, ea) + ";"); - }); + plPgSql.writeLn( + ea.fetchSql().sql.replace("${ref}", NEW.name()) + " into " + entityRefVar(NEW, ea) + ";"); + }); createRolesWithGrantsSql(plPgSql, OWNER); createRolesWithGrantsSql(plPgSql, ADMIN); @@ -114,13 +128,22 @@ class RolesGrantsAndPermissionsGenerator { generateGrants(plPgSql, ROLE_TO_ROLE); generateGrants(plPgSql, PERM_TO_ROLE); }); + } - plPgSql.writeLn("end if;"); -} + private Stream referencedEntityAliases() { + return rbacDef.getEntityAliases().values().stream() + .filter((ea) -> !rbacDef.isRootEntityAlias(ea)) + .filter((ea) -> ea.fetchSql() != null); + } + + private Stream updatableEntityAliases() { + return referencedEntityAliases() + .filter(ea -> rbacDef.getUpdatableColumns().contains(ea.dependsOnColum().column) ); + } private void generateUpdateRolesAndGrantsAfterUpdate(final StringWriter plPgSql) { plPgSql.ensureEmptyLine(); - plPgSql.writeLn("if TG_OP = 'UPDATE' then"); + plPgSql.writeLn("elsif TG_OP = 'UPDATE' then"); plPgSql.indented(() -> { @@ -128,7 +151,8 @@ class RolesGrantsAndPermissionsGenerator { .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) + ";"); + plPgSql.writeLn( + ea.fetchSql().sql.replace("${ref}", OLD.name()) + " into " + entityRefVar(OLD, ea) + ";"); }); rbacDef.getEntityAliases().values().stream() @@ -147,8 +171,6 @@ class RolesGrantsAndPermissionsGenerator { plPgSql.writeLn("end if;"); }); }); - - plPgSql.writeLn("end if;"); } private boolean isUpdatable(final RbacView.Column c) { @@ -160,7 +182,8 @@ class RolesGrantsAndPermissionsGenerator { .filter(RbacView.RbacGrantDefinition::isToCreate) .filter(g -> g.dependsOnColumn(columnName)) .forEach(g -> { - plPgSql.writeLn("-- TODO: " + g); + plPgSql.writeLn("-- TODO: revoke " + g); + plPgSql.writeLn(generateGrant(g)); }); } @@ -177,11 +200,11 @@ class RolesGrantsAndPermissionsGenerator { return switch (grantDef.grantType()) { case ROLE_TO_USER -> throw new IllegalArgumentException("unexpected grant"); case ROLE_TO_ROLE -> "call grantRoleToRole(${subRoleRef}, ${superRoleRef}));" - .replace("${subRoleRef}", roleRef(NEW, grantDef.getSubRoleDef()) ) - .replace("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef()) ); + .replace("${subRoleRef}", roleRef(NEW, grantDef.getSubRoleDef())) + .replace("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef())); case PERM_TO_ROLE -> "call grantPermissionsToRole(${permRef}, ${superRoleRef}));" - .replace("${permRef}", permRef(NEW, grantDef.getPermDef()) ) - .replace("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef()) ); + .replace("${permRef}", permRef(NEW, grantDef.getPermDef())) + .replace("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef())); }; } @@ -198,10 +221,10 @@ class RolesGrantsAndPermissionsGenerator { } private String roleRef(final PostgresTriggerReference rootRefVar, final RbacView.RbacRoleDefinition roleDef) { - if ( roleDef == null ) { + if (roleDef == null) { System.out.println("null"); } - if ( roleDef.getEntityAlias().isGlobal()) { + if (roleDef.getEntityAlias().isGlobal()) { return "globalAdmin()"; } final String entityRefVar = entityRefVar(rootRefVar, roleDef.getEntityAlias()); @@ -217,16 +240,16 @@ class RolesGrantsAndPermissionsGenerator { private void createRolesWithGrantsSql(final StringWriter plPgSql, final RbacView.Role role) { - final var isToCreate = rbacDef.getRoleDefs().stream() - .filter(roleDef -> rbacDef.isRootEntityAlias(roleDef.getEntityAlias()) && roleDef.getRole() == role ) - .findFirst().map(RbacView.RbacRoleDefinition::isToCreate).orElse(false); + final var isToCreate = rbacDef.getRoleDefs().stream() + .filter(roleDef -> rbacDef.isRootEntityAlias(roleDef.getEntityAlias()) && roleDef.getRole() == role) + .findFirst().map(RbacView.RbacRoleDefinition::isToCreate).orElse(false); if (!isToCreate) { return; } plPgSql.writeLn(); plPgSql.writeLn("perform createRoleWithGrants("); - plPgSql.indented( () -> { + plPgSql.indented(() -> { plPgSql.writeLn("${simpleVarName)${roleSuffix}(NEW)," .replace("${simpleVarName)", simpleEntityVarName) .replace("${roleSuffix}", capitalize(role.roleName()))); @@ -268,7 +291,7 @@ class RolesGrantsAndPermissionsGenerator { .map(RbacView.Permission::permission) .map(p -> "'" + p + "'") .toList(); - plPgSql.indented( () -> + plPgSql.indented(() -> plPgSql.writeLn("permissions => array[" + joinArrayElements(arrayElements, 3) + "],\n")); rbacGrants.removeAll(permissionGrantsForRole); } @@ -306,31 +329,39 @@ class RolesGrantsAndPermissionsGenerator { : arrayElements.stream().collect(joining(",\n\t", "\n\t", "")); } - private Set findPermissionsGrantsForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { + private Set findPermissionsGrantsForRole( + final RbacView.EntityAlias entityAlias, + final RbacView.Role role) { final var roleDef = rbacDef.findRbacRole(entityAlias, role); return rbacGrants.stream() - .filter(g -> g.grantType() == PERM_TO_ROLE && g.getSuperRoleDef()==roleDef ) + .filter(g -> g.grantType() == PERM_TO_ROLE && g.getSuperRoleDef() == roleDef) .collect(toSet()); } - private Set findGrantsToUserForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { + private Set findGrantsToUserForRole( + final RbacView.EntityAlias entityAlias, + final RbacView.Role role) { final var roleDef = rbacDef.findRbacRole(entityAlias, role); return rbacGrants.stream() - .filter(g -> g.grantType() == ROLE_TO_USER && g.getSubRoleDef() == roleDef ) + .filter(g -> g.grantType() == ROLE_TO_USER && g.getSubRoleDef() == roleDef) .collect(toSet()); } - private Set findIncomingSuperRolesForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { + private Set findIncomingSuperRolesForRole( + final RbacView.EntityAlias entityAlias, + final RbacView.Role role) { final var roleDef = rbacDef.findRbacRole(entityAlias, role); return rbacGrants.stream() - .filter(g -> g.grantType() == ROLE_TO_ROLE && g.getSubRoleDef()==roleDef ) + .filter(g -> g.grantType() == ROLE_TO_ROLE && g.getSubRoleDef() == roleDef) .collect(toSet()); } - private Set findOutgoingSuperRolesForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { + private Set findOutgoingSuperRolesForRole( + final RbacView.EntityAlias entityAlias, + final RbacView.Role role) { final var roleDef = rbacDef.findRbacRole(entityAlias, role); return rbacGrants.stream() - .filter(g -> g.grantType() == ROLE_TO_ROLE && g.getSuperRoleDef()==roleDef ) + .filter(g -> g.grantType() == ROLE_TO_ROLE && g.getSuperRoleDef() == roleDef) .filter(g -> g.getSubRoleDef().getEntityAlias() != entityAlias) .collect(toSet()); } @@ -341,20 +372,47 @@ class RolesGrantsAndPermissionsGenerator { An AFTER INSERT TRIGGER which creates the role structure for a new ${simpleEntityName} */ - create or replace function createRbacRolesFor${simpleEntityName}_tf() + create or replace function insertTriggerFor${simpleEntityName}_tf() returns trigger language plpgsql strict as $$ begin - call createRbacRolesFor${simpleEntityName}(TG_OP, OLD, NEW); + call buildRbacSystemFor${simpleEntityName}(TG_OP, OLD, NEW); return NEW; end; $$; - create trigger createRbacRolesFor${simpleEntityName}_tg + create trigger insertTriggerFor${simpleEntityName}_tg after insert on ${rawTableName} for each row - execute procedure createRbacRolesFor${simpleEntityName}_tf(); + execute procedure insertTriggerFor${simpleEntityName}_tf(); + --// + """ + .replace("${simpleEntityName}", simpleEntityName) + .replace("${rawTableName}", rawTableName) + ); + } + + private void generateUpdateTrigger(final StringWriter plPgSql) { + plPgSql.writeLn(""" + /* + An AFTER UPDATE TRIGGER which re-wires the grant structure for an updated ${simpleEntityName} + */ + + create or replace function updateTriggerFor${simpleEntityName}_tf() + returns trigger + language plpgsql + strict as $$ + begin + call buildRbacSystemFor${simpleEntityName}(TG_OP, OLD, NEW); + return NEW; + end; $$; + + create trigger updateTriggerFor${simpleEntityName}_tg + after update + on ${rawTableName} + for each row + execute procedure updateTriggerFor${simpleEntityName}_tf(); --// """ .replace("${simpleEntityName}", simpleEntityName) @@ -385,7 +443,9 @@ class RolesGrantsAndPermissionsGenerator { return uncapitalize(roleDef.getEntityAlias().simpleName()) + capitalize(roleDef.getRole().roleName()); } - private static String toTriggerReference(final PostgresTriggerReference triggerRef, final RbacView.EntityAlias entityAlias) { + private static String toTriggerReference( + final PostgresTriggerReference triggerRef, + final RbacView.EntityAlias entityAlias) { return triggerRef.name().toLowerCase() + capitalize(entityAlias.aliasName()); } } -- 2.39.5 From 17282c857f533907df8cdd600e74824248b9bb6d Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 1 Mar 2024 12:34:02 +0100 Subject: [PATCH 25/53] 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; + } + } } -- 2.39.5 From b187c705b1cf7b5e84395399896555bc3050a546 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 1 Mar 2024 18:46:41 +0100 Subject: [PATCH 26/53] finally working version for UPDATE-trigger-function with separated INSERT+UPDATE trigger functions --- .../hsadminng/rbac/rbacdef/RbacView.java | 8 +- .../rbacdef/RbacViewPostgresGenerator.java | 2 +- .../RolesGrantsAndPermissionsGenerator.java | 150 ++++++++++-------- .../hsadminng/rbac/rbacdef/StringWriter.java | 20 ++- 4 files changed, 103 insertions(+), 77 deletions(-) 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 00e74c95..f193903d 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -503,7 +503,7 @@ public class RbacView { } return switch (fetchSql.part) { case SQL_QUERY -> fetchSql; - case AUTO_FETCH -> SQL.query("SELECT * FROM " + getRawTableName(entityClass) + " 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); }; } @@ -521,10 +521,10 @@ public class RbacView { ? aliasName : uncapitalize(withoutEntitySuffix(entityClass.getSimpleName())); } - } - public static String getRawTableName(final Class entityClass) { - return withoutRvSuffix(entityClass.getAnnotation(Table.class).name()); + String getRawTableName() { + return withoutRvSuffix(entityClass.getAnnotation(Table.class).name()); + } } public static String withoutRvSuffix(final String tableName) { return tableName.substring(0, tableName.length()-"_rv".length()); 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 75333987..5a4fdd48 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java @@ -20,7 +20,7 @@ public class RbacViewPostgresGenerator { public RbacViewPostgresGenerator(final RbacView forRbacDef) { rbacDef = forRbacDef; - liqibaseTagPrefix = rbacDef.getRootEntityAlias().entityClass().getSimpleName(); + liqibaseTagPrefix = rbacDef.getRootEntityAlias().getRawTableName().replace("_", "-"); plPgSql.writeLn(""" --liquibase formatted sql -- This code generated was by ${generator} at ${timestamp}. 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 887328e8..199cef9d 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java @@ -13,7 +13,6 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.NE import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.OLD; 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; @@ -38,36 +37,65 @@ class RolesGrantsAndPermissionsGenerator { entityClass = rbacDef.getRootEntityAlias().entityClass(); simpleEntityVarName = rbacDef.getRootEntityAlias().simpleName(); simpleEntityName = capitalize(simpleEntityVarName); - rawTableName = getRawTableName(entityClass); + rawTableName = rbacDef.getRootEntityAlias().getRawTableName(); } void generateTo(final StringWriter plPgSql) { - generateHeader(plPgSql); - generateTriggerFunction(plPgSql); generateInsertTrigger(plPgSql); if (hasAnyUpdatableEntityAliases()) { generateUpdateTrigger(plPgSql); } - generateFooter(plPgSql); } - private void generateHeader(final StringWriter plPgSql) { + private void generateHeader(final StringWriter plPgSql, final String triggerType) { plPgSql.writeLn(""" -- ============================================================================ - --changeset ${liquibaseTagPrefix}-rbac-CREATE-ROLES-GRANTS-PERMISSIONS:1 endDelimiter:--// + --changeset ${liquibaseTagPrefix}-rbac-${triggerType}-trigger:1 endDelimiter:--// -- ---------------------------------------------------------------------------- - """ - .replace("${liquibaseTagPrefix}", liquibaseTagPrefix)); + """, + with("liquibaseTagPrefix", liquibaseTagPrefix), + with("triggerType", triggerType)); } - private void generateTriggerFunction(final StringWriter plPgSql) { + private void generateInsertTriggerFunction(final StringWriter plPgSql) { plPgSql.writeLn(""" /* A Creates the roles, grants and permission for the AFTER INSERT TRIGGER. */ create or replace procedure buildRbacSystemFor${simpleEntityName}( - TG_OP text, + NEW ${rawTableName} + ) + language plpgsql as $$ + + declare + """ + .replace("${simpleEntityName}", simpleEntityName) + .replace("${rawTableName}", rawTableName)); + + plPgSql.chopEmptyLines(); + plPgSql.indented(() -> { + referencedEntityAliases() + .forEach((ea) -> plPgSql.writeLn(entityRefVar(NEW, ea) + " " + ea.getRawTableName() + ";")); + }); + + plPgSql.writeLn(); + plPgSql.writeLn("begin"); + plPgSql.indented(() -> { + generateCreateRolesAndGrantsAfterInsert(plPgSql); + plPgSql.ensureSingleEmptyLine(); + }); + plPgSql.writeLn("end; $$;"); + plPgSql.writeLn(); + } + + private void generateUpdateTriggerFunction(final StringWriter plPgSql) { + plPgSql.writeLn(""" + /* + Called from the AFTER UPDATE TRIGGER to re-wire the grants. + */ + + create or replace procedure updateRbacGrantsFor${simpleEntityName}( OLD ${rawTableName}, NEW ${rawTableName} ) @@ -80,27 +108,17 @@ class RolesGrantsAndPermissionsGenerator { plPgSql.chopEmptyLines(); plPgSql.indented(() -> { - - referencedEntityAliases() - .forEach((ea) -> plPgSql.writeLn(entityRefVar(NEW, ea) + " " + getRawTableName(ea.entityClass()) + ";")); - updatableEntityAliases() - .forEach((ea) -> plPgSql.writeLn(entityRefVar(OLD, ea) + " " + getRawTableName(ea.entityClass()) + ";")); + .forEach((ea) -> { + plPgSql.writeLn(entityRefVar(OLD, ea) + " " + ea.getRawTableName() + ";"); + plPgSql.writeLn(entityRefVar(NEW, ea) + " " + ea.getRawTableName() + ";"); + }); }); plPgSql.writeLn(); plPgSql.writeLn("begin"); plPgSql.indented(() -> { - - generateCreateRolesAndGrantsAfterInsert(plPgSql); - if (hasAnyUpdatableEntityAliases()) { - generateUpdateRolesAndGrantsAfterUpdate(plPgSql); - } - plPgSql.writeLn(""" - else - raise exception 'invalid usage of TRIGGER'; - end if; - """); + generateUpdateRolesAndGrantsAfterUpdate(plPgSql); plPgSql.ensureSingleEmptyLine(); }); plPgSql.writeLn("end; $$;"); @@ -117,23 +135,15 @@ class RolesGrantsAndPermissionsGenerator { ea.fetchSql().sql + " into " + entityRefVar(NEW, ea) + ";", with("ref", NEW.name()))); - plPgSql.ensureSingleEmptyLine(); - plPgSql.writeLn("if TG_OP = 'INSERT' then"); + createRolesWithGrantsSql(plPgSql, OWNER); + createRolesWithGrantsSql(plPgSql, ADMIN); + createRolesWithGrantsSql(plPgSql, AGENT); + createRolesWithGrantsSql(plPgSql, TENANT); + createRolesWithGrantsSql(plPgSql, REFERRER); - plPgSql.indented(() -> { - - plPgSql.chopEmptyLines(); - createRolesWithGrantsSql(plPgSql, OWNER); - createRolesWithGrantsSql(plPgSql, ADMIN); - createRolesWithGrantsSql(plPgSql, AGENT); - createRolesWithGrantsSql(plPgSql, TENANT); - createRolesWithGrantsSql(plPgSql, REFERRER); - - generateGrants(plPgSql, ROLE_TO_USER); - generateGrants(plPgSql, ROLE_TO_ROLE); - generateGrants(plPgSql, PERM_TO_ROLE); - plPgSql.ensureSingleEmptyLine(); - }); + generateGrants(plPgSql, ROLE_TO_USER); + generateGrants(plPgSql, ROLE_TO_ROLE); + generateGrants(plPgSql, PERM_TO_ROLE); } private Stream referencedEntityAliases() { @@ -151,29 +161,30 @@ class RolesGrantsAndPermissionsGenerator { private void generateUpdateRolesAndGrantsAfterUpdate(final StringWriter plPgSql) { plPgSql.ensureSingleEmptyLine(); - plPgSql.writeLn("elsif TG_OP = 'UPDATE' then"); - plPgSql.indented(() -> { - - updatableEntityAliases() - .forEach((ea) -> plPgSql.writeLn( + updatableEntityAliases() + .forEach((ea) -> { + plPgSql.writeLn( ea.fetchSql().sql + " into " + entityRefVar(OLD, ea) + ";", - with("ref", OLD.name()))); + with("ref", OLD.name())); + plPgSql.writeLn( + ea.fetchSql().sql + " into " + entityRefVar(NEW, ea) + ";", + with("ref", NEW.name())); + }); - updatableEntityAliases() - .map(RbacView.EntityAlias::dependsOnColum) - .map(c -> c.column) - .sorted() - .distinct() - .forEach(columnName -> { - plPgSql.writeLn(); - plPgSql.writeLn("if NEW." + columnName + " <> OLD." + columnName + " then"); - plPgSql.indented(() -> { - updateGrantsDependingOn(plPgSql, columnName); - }); - plPgSql.writeLn("end if;"); + updatableEntityAliases() + .map(RbacView.EntityAlias::dependsOnColum) + .map(c -> c.column) + .sorted() + .distinct() + .forEach(columnName -> { + plPgSql.writeLn(); + plPgSql.writeLn("if NEW." + columnName + " <> OLD." + columnName + " then"); + plPgSql.indented(() -> { + updateGrantsDependingOn(plPgSql, columnName); }); - }); + plPgSql.writeLn("end if;"); + }); } private boolean isUpdatable(final RbacView.Column c) { @@ -386,6 +397,10 @@ class RolesGrantsAndPermissionsGenerator { } private void generateInsertTrigger(final StringWriter plPgSql) { + + generateHeader(plPgSql, "insert"); + generateInsertTriggerFunction(plPgSql); + plPgSql.writeLn(""" /* AFTER INSERT TRIGGER to create the role+grant structure for a new ${rawTableName} row. @@ -405,14 +420,19 @@ class RolesGrantsAndPermissionsGenerator { on ${rawTableName} for each row execute procedure insertTriggerFor${simpleEntityName}_tf(); - --// """ .replace("${simpleEntityName}", simpleEntityName) .replace("${rawTableName}", rawTableName) ); + + generateFooter(plPgSql); } private void generateUpdateTrigger(final StringWriter plPgSql) { + + generateHeader(plPgSql, "update"); + generateUpdateTriggerFunction(plPgSql); + plPgSql.writeLn(""" /* AFTER INSERT TRIGGER to re-wire the grant structure for a new ${rawTableName} row. @@ -423,7 +443,7 @@ class RolesGrantsAndPermissionsGenerator { language plpgsql strict as $$ begin - call buildRbacSystemFor${simpleEntityName}(TG_OP, OLD, NEW); + call buildRbacSystemFor${simpleEntityName}(NEW); return NEW; end; $$; @@ -437,10 +457,12 @@ class RolesGrantsAndPermissionsGenerator { .replace("${simpleEntityName}", simpleEntityName) .replace("${rawTableName}", rawTableName) ); + + generateFooter(plPgSql); } private static void generateFooter(final StringWriter plPgSql) { - plPgSql.writeLn(); + plPgSql.writeLn("--//"); plPgSql.writeLn(); } 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 876527dc..00f684e2 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/StringWriter.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/StringWriter.java @@ -90,14 +90,18 @@ public class StringWriter { 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; + String apply(final String textToAppend) { + try { + text = textToAppend; + stream(varDefs).forEach(varDef -> { + final var pattern = Pattern.compile("\\$\\{" + varDef.name() + "}", Pattern.CASE_INSENSITIVE); + final var matcher = pattern.matcher(text); + text = matcher.replaceAll(varDef.value()); + }); + return text; + } catch (Exception exc) { + throw exc; + } } } } -- 2.39.5 From fa15378fd2081d8dfbd204a9177c38d9d18c2d8f Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sat, 2 Mar 2024 19:53:21 +0100 Subject: [PATCH 27/53] generate identityview, restrictedview and roledescriptors --- .../HsOfficeRelationshipEntity.java | 2 + .../rbacdef/RbacIdentityViewGenerator.java | 34 +++++++++++++++ .../rbacdef/RbacRestrictedViewGenerator.java | 41 +++++++++++++++++++ .../rbacdef/RbacRoleDescriptorsGenerator.java | 31 ++++++++++++++ .../hsadminng/rbac/rbacdef/RbacView.java | 15 ++++++- .../rbacdef/RbacViewPostgresGenerator.java | 17 ++------ .../RolesGrantsAndPermissionsGenerator.java | 3 -- 7 files changed, 125 insertions(+), 18 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacIdentityViewGenerator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacRestrictedViewGenerator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacRoleDescriptorsGenerator.java 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 82356e28..72185b64 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,6 +85,8 @@ public class HsOfficeRelationshipEntity implements HasUuid, Stringifyable { || '-with-' || target.relType || '-' || (select idName from hs_office_person_iv p where p.uuid = relHolderUuid) """)) + .withRestrictedViewOrderedBy(SQL.expression( + "(select idName from hs_office_person_iv p where p.uuid = target.relHolderUuid)")) .withUpdatableColumns("contactUuid") .importEntityAlias("anchorPerson", HsOfficePersonEntity.class, dependsOnColumn("relAnchorUuid"), diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacIdentityViewGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacIdentityViewGenerator.java new file mode 100644 index 00000000..ed51061b --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacIdentityViewGenerator.java @@ -0,0 +1,34 @@ +package net.hostsharing.hsadminng.rbac.rbacdef; + +import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with; + +public class RbacIdentityViewGenerator { + private final RbacView rbacDef; + private final String liquibaseTagPrefix; + private final String simpleEntityVarName; + private final String rawTableName; + + public RbacIdentityViewGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) { + this.rbacDef = rbacDef; + this.liquibaseTagPrefix = liquibaseTagPrefix; + this.simpleEntityVarName = rbacDef.getRootEntityAlias().simpleName(); + this.rawTableName = rbacDef.getRootEntityAlias().getRawTableName(); + } + + void generateTo(final StringWriter plPgSql) { + plPgSql.writeLn(""" + + -- ============================================================================ + --changeset ${liquibaseTagPrefix}-rbac-IDENTITY-VIEW:1 endDelimiter:--// + -- ---------------------------------------------------------------------------- + call generateRbacIdentityView('${rawTableName}', $idName$ + ${identityViewSqlPart} + $idName$); + --// + + """, + with("liquibaseTagPrefix", liquibaseTagPrefix), + with("identityViewSqlPart", rbacDef.getIdentityViewSqlQuery().sql), // TODO: other part types + 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 new file mode 100644 index 00000000..3755b20f --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacRestrictedViewGenerator.java @@ -0,0 +1,41 @@ +package net.hostsharing.hsadminng.rbac.rbacdef; + + +import static java.util.stream.Collectors.joining; +import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with; + +public class RbacRestrictedViewGenerator { + private final RbacView rbacDef; + private final String liquibaseTagPrefix; + private final String simpleEntityVarName; + private final String rawTableName; + + public RbacRestrictedViewGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) { + this.rbacDef = rbacDef; + this.liquibaseTagPrefix = liquibaseTagPrefix; + this.simpleEntityVarName = rbacDef.getRootEntityAlias().simpleName(); + this.rawTableName = rbacDef.getRootEntityAlias().getRawTableName(); + } + + void generateTo(final StringWriter plPgSql) { + plPgSql.writeLn(""" + + -- ============================================================================ + --changeset ${liquibaseTagPrefix}-rbac-RESTRICTED-VIEW:1 endDelimiter:--// + -- ---------------------------------------------------------------------------- + call generateRbacRestrictedView('${rawTableName}', + '${orderBy}', + $updates$ + ${updates} + $updates$); + --// + + """, + with("liquibaseTagPrefix", liquibaseTagPrefix), + with("orderBy", rbacDef.getOrderBySqlExpression().sql), + with("updates", rbacDef.getUpdatableColumns().stream() + .map(c -> c + " = new." + c) + .collect(joining("\n"))), + with("rawTableName", rawTableName)); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacRoleDescriptorsGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacRoleDescriptorsGenerator.java new file mode 100644 index 00000000..661f9091 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacRoleDescriptorsGenerator.java @@ -0,0 +1,31 @@ +package net.hostsharing.hsadminng.rbac.rbacdef; + +import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with; + +public class RbacRoleDescriptorsGenerator { + + private final String liquibaseTagPrefix; + private final String simpleEntityVarName; + private final String rawTableName; + + public RbacRoleDescriptorsGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) { + this.liquibaseTagPrefix = liquibaseTagPrefix; + this.simpleEntityVarName = rbacDef.getRootEntityAlias().simpleName(); + this.rawTableName = rbacDef.getRootEntityAlias().getRawTableName(); + } + + void generateTo(final StringWriter plPgSql) { + plPgSql.writeLn(""" + + -- ============================================================================ + --changeset ${liquibaseTagPrefix}-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--// + -- ---------------------------------------------------------------------------- + call generateRbacRoleDescriptors('${simpleEntityVarName}', '${rawTableName}'); + --// + + """, + with("liquibaseTagPrefix", liquibaseTagPrefix), + with("simpleEntityVarName", simpleEntityVarName), + 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 f193903d..b60a1d29 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -27,7 +27,6 @@ public class RbacView { public static final String GLOBAL = "global"; public static final String OUTPUT_BASEDIR = "src/main/resources/db/changelog"; - private final EntityAlias rootEntityAlias; private final Set userDefs = new LinkedHashSet<>(); @@ -47,6 +46,7 @@ public class RbacView { private final Set grantDefs = new LinkedHashSet<>(); private SQL identityViewSqlQuery; + private SQL orderBySqlExpression; private EntityAlias rootEntityAliasProxy; private RbacRoleDefinition previousRoleDef; @@ -70,6 +70,11 @@ public class RbacView { return this; } + public RbacView withRestrictedViewOrderedBy(final SQL orderBySqlExpression) { + this.orderBySqlExpression = orderBySqlExpression; + return this; + } + public RbacView createRole(final Role role, final Consumer with) { final RbacRoleDefinition newRoleDef = findRbacRole(rootEntityAlias, role).toCreate(); with.accept(newRoleDef); @@ -619,11 +624,17 @@ public class RbacView { return new SQL(projection, Part.SQL_PROJECTION); } + public static SQL expression(final String sqlExpression) { + // TODO: validate + return new SQL(sqlExpression, Part.SQL_EXPRESSION); + } + enum Part { NOOP, SQL_QUERY, AUTO_FETCH, - SQL_PROJECTION + SQL_PROJECTION, + SQL_EXPRESSION } final String sql; 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 5a4fdd48..11c2dc8c 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java @@ -4,7 +4,6 @@ import lombok.SneakyThrows; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.time.LocalDateTime; @@ -13,7 +12,6 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with; public class RbacViewPostgresGenerator { - private final RbacView rbacDef; private final String liqibaseTagPrefix; private final StringWriter plPgSql = new StringWriter(); @@ -29,7 +27,11 @@ public class RbacViewPostgresGenerator { with("timestamp", LocalDateTime.now().toString()), with("ref", NEW.name())); + new RbacObjectGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql); + new RbacRoleDescriptorsGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql); new RolesGrantsAndPermissionsGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql); + new RbacIdentityViewGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql); + new RbacRestrictedViewGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql); } @Override @@ -37,17 +39,6 @@ public class RbacViewPostgresGenerator { return plPgSql.toString(); } - @SneakyThrows - private static void generatePostgres(final RbacView rbac) { - final Path outputPath = Paths.get("doc", rbac.getRootEntityAlias().simpleName() + ".sql"); - Files.writeString( - outputPath, - new RbacViewPostgresGenerator(rbac).toString(), - StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - - System.out.println(outputPath.toAbsolutePath()); - } - @SneakyThrows public void generateToChangeLog(final Path outputPath) { Files.writeString( 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 199cef9d..7f44c84c 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java @@ -22,7 +22,6 @@ class RolesGrantsAndPermissionsGenerator { private final RbacView rbacDef; private final Set rbacGrants = new HashSet<>(); private final String liquibaseTagPrefix; - private final Class entityClass; private final String simpleEntityName; private final String simpleEntityVarName; private final String rawTableName; @@ -34,7 +33,6 @@ class RolesGrantsAndPermissionsGenerator { .collect(toSet())); this.liquibaseTagPrefix = liquibaseTagPrefix; - entityClass = rbacDef.getRootEntityAlias().entityClass(); simpleEntityVarName = rbacDef.getRootEntityAlias().simpleName(); simpleEntityName = capitalize(simpleEntityVarName); rawTableName = rbacDef.getRootEntityAlias().getRawTableName(); @@ -452,7 +450,6 @@ class RolesGrantsAndPermissionsGenerator { on ${rawTableName} for each row execute procedure updateTriggerFor${simpleEntityName}_tf(); - --// """ .replace("${simpleEntityName}", simpleEntityName) .replace("${rawTableName}", rawTableName) -- 2.39.5 From b2cea1e88296ab3f16d2cb05e544d7a54ce4eb66 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 5 Mar 2024 09:26:39 +0100 Subject: [PATCH 28/53] 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 + """); + } +} -- 2.39.5 From 4df5c2606a02566d2bd0d57d8ed09ffe4e2d13b4 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 5 Mar 2024 10:04:15 +0100 Subject: [PATCH 29/53] use identity view projection for restricted view orderBy if none is explicitely specified --- .../rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 7f44c84c..ba01d61f 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java @@ -11,6 +11,7 @@ import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toSet; import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.NEW; import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.OLD; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT; 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.StringWriter.with; @@ -228,9 +229,11 @@ class RolesGrantsAndPermissionsGenerator { 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});" - .replace("${permRef}", permRef(NEW, grantDef.getPermDef())) - .replace("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef())); + case PERM_TO_ROLE -> + grantDef.getPermDef().getPermission() == INSERT ? "" + : "call grantPermissionsToRole(${permRef}, ${superRoleRef});" + .replace("${permRef}", permRef(NEW, grantDef.getPermDef())) + .replace("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef())); }; } -- 2.39.5 From 3cc51855510143766a20f10e6f26580a12c1432d Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 6 Mar 2024 08:44:23 +0100 Subject: [PATCH 30/53] initially working version of generated INSERT-Trigger WIP WIP --- doc/rbac.md | 148 ++++++------ sql/rbac-tests.sql | 10 +- sql/rbac-view-option-experiments.sql | 10 +- .../HsOfficeBankAccountEntity.java | 8 +- .../office/contact/HsOfficeContactEntity.java | 8 +- .../office/debitor/HsOfficeDebitorEntity.java | 8 +- .../partner/HsOfficePartnerDetailsEntity.java | 2 +- .../office/partner/HsOfficePartnerEntity.java | 16 +- .../office/person/HsOfficePersonEntity.java | 8 +- .../HsOfficeRelationshipEntity.java | 8 +- .../HsOfficeSepaMandateEntity.java | 8 +- .../rbac/rbacdef/InsertTriggerGenerator.java | 119 +++++++--- .../hsadminng/rbac/rbacdef/RbacView.java | 16 +- .../RolesGrantsAndPermissionsGenerator.java | 35 ++- .../test/cust/TestCustomerEntity.java | 10 +- .../hsadminng/test/pac/TestPackageEntity.java | 8 +- .../resources/db/changelog/010-context.sql | 9 +- .../resources/db/changelog/050-rbac-base.sql | 90 ++++--- .../db/changelog/051-rbac-user-grant.sql | 19 +- .../db/changelog/057-rbac-role-builder.sql | 5 +- .../db/changelog/058-rbac-generators.sql | 13 +- .../db/changelog/113-test-customer-rbac.sql | 130 ++++------ .../changelog/118-test-customer-test-data.sql | 12 + .../db/changelog/123-test-package-rbac.sql | 223 ++++++++++++++---- .../changelog/128-test-package-test-data.sql | 2 +- .../db/changelog/133-test-domain-rbac.sql | 8 +- .../changelog/203-hs-office-contact-rbac.sql | 6 +- .../changelog/213-hs-office-person-rbac.sql | 8 +- .../223-hs-office-relationship-rbac.sql | 6 +- .../changelog/233-hs-office-partner-rbac.sql | 12 +- .../234-hs-office-partner-details-rbac.sql | 5 +- .../243-hs-office-bankaccount-rbac.sql | 4 +- .../253-hs-office-sepamandate-rbac.sql | 6 +- .../changelog/273-hs-office-debitor-rbac.sql | 6 +- .../303-hs-office-membership-rbac.sql | 6 +- .../313-hs-office-coopshares-rbac.sql | 2 +- .../323-hs-office-coopassets-rbac.sql | 2 +- .../rbac/rbacrole/RawRbacObjectEntity.java | 31 +++ .../rbacrole/RawRbacObjectRepository.java | 11 + .../RbacUserControllerAcceptanceTest.java | 12 +- .../test/cust/TestCustomerEntityTest.java | 9 +- .../test/pac/TestPackageEntityTest.java | 12 +- .../TestPackageRepositoryIntegrationTest.java | 2 +- 43 files changed, 648 insertions(+), 425 deletions(-) create mode 100644 src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RawRbacObjectEntity.java create mode 100644 src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RawRbacObjectRepository.java diff --git a/doc/rbac.md b/doc/rbac.md index 06a6ee7e..e8799d5f 100644 --- a/doc/rbac.md +++ b/doc/rbac.md @@ -11,7 +11,7 @@ Our implementation is based on Role-Based-Access-Management (RBAC) in conjunctio As far as possible, we are using the same terms as defined in the RBAC standard, for our function names though, we chose more expressive names. In RBAC, subjects can be assigned to roles, roles can be hierarchical and eventually have assigned permissions. -A permission allows a specific operation (e.g. view or edit) on a specific (business-) object. +A permission allows a specific operation (e.g. SELECT or UPDATE) on a specific (business-) object. You can find the entity structure as a UML class diagram as follows: @@ -101,13 +101,12 @@ package RBAC { RbacPermission *-- RbacObject enum RbacOperation { - add-package - add-domain - add-domain + INSERT:package + INSERT:domain ... - view - edit - delete + SELECT + UPDATE + DELETE } entity RbacObject { @@ -172,11 +171,10 @@ An *RbacPermission* allows a specific *RbacOperation* on a specific *RbacObject* An *RbacOperation* determines, what an *RbacPermission* allows to do. It can be one of: -- **'add-...'** - permits creating new instances of specific entity types underneath the object specified by the permission, e.g. "add-package" -- **'view'** - permits reading the contents of the object specified by the permission -- **'edit'** - change the contents of the object specified by the permission -- **'delete'** - delete the object specified by the permission -- **'\*'** +- **'INSERT'** - permits inserting new rows related to the row, to which the permission belongs, in the table which is specified an extra column +- **'SELECT'** - permits selecting the row specified by the permission +- **'UPDATE'** - permits updating (only the updatable columns of) the row specified by the permission +- **'DELETE'** - permits deleting the row specified by the permission This list is extensible according to the needs of the access rule system. @@ -212,7 +210,7 @@ E.g. for a new *customer* it would be granted to 'administrators' and for a new Whoever has the owner-role assigned can do everything with the related business-object, including deleting (or deactivating) it. -In most cases, the permissions to other operations than 'delete' are granted through the 'admin' role. +In most cases, the permissions to other operations than 'DELETE' are granted through the 'admin' role. By this, all roles ob sub-objects, which are assigned to the 'admin' role, are also granted to the 'owner'. #### admin @@ -220,14 +218,14 @@ By this, all roles ob sub-objects, which are assigned to the 'admin' role, are a The admin-role is granted to a role of those subjects who manage the business object. E.g. a 'package' is manged by the admin of the customer. -Whoever has the admin-role assigned, can usually edit the related business-object but not deleting (or deactivating) it. +Whoever has the admin-role assigned, can usually update the related business-object but not delete (or deactivating) it. -The admin-role also comprises lesser roles, through which the view-permission is granted. +The admin-role also comprises lesser roles, through which the SELECT-permission is granted. #### agent The agent-role is not used in the examples of this document, because it's for more complex cases. -It's usually granted to those roles and users who represent the related business-object, but are not allowed to edit it. +It's usually granted to those roles and users who represent the related business-object, but are not allowed to update it. Other than the tenant-role, it usually offers broader visibility of sub-business-objects (joined entities). E.g. a package-admin is allowed to see the related debitor-business-object, @@ -235,19 +233,19 @@ but not its banking data. #### tenant -The tenant-role is granted to everybody who needs to be able to view the business-object and (probably some) related business-objects. +The tenant-role is granted to everybody who needs to be able to select the business-object and (probably some) related business-objects. Usually all owners, admins and tenants of sub-objects get this role granted. -Some business-objects only have very limited data directly in the main business-object and store more sensitive data in special sub-objects (e.g. 'customer-details') to which tenants of sub-objects of the main-object (e.g. package admins) do not get view permission. +Some business-objects only have very limited data directly in the main business-object and store more sensitive data in special sub-objects (e.g. 'customer-details') to which tenants of sub-objects of the main-object (e.g. package admins) do not get SELECT permission. #### guest Like the agent-role, the guest-role too is not used in the examples of this document, because it's for more complex cases. -If the guest-role exists, the view-permission is granted to it, instead of to the tenant-role. +If the guest-role exists, the SELECT-permission is granted to it, instead of to the tenant-role. Other than the tenant-role, the guest-roles does never grant any roles of related objects. -Also, if the guest-role exists, the tenant-role receives the view-permission through the guest-role. +Also, if the guest-role exists, the tenant-role receives the SELECT-permission through the guest-role. ### Referenced Business Objects and Role-Depreciation @@ -263,7 +261,7 @@ The admin-role of one object could be granted visibility to another object throu But not in all cases role-depreciation takes place. E.g. often a tenant-role is granted another tenant-role, -because it should be again allowed to view sub-objects. +because it should be again allowed to select sub-objects. The same for the agent-role, often it is granted another agent-role. @@ -297,14 +295,14 @@ package RbacRoles { RbacUsers -[hidden]> RbacRoles package RbacPermissions { - object PermCustXyz_View - object PermCustXyz_Edit - object PermCustXyz_Delete - object PermCustXyz_AddPackage - object PermPackXyz00_View - object PermPackXyz00_Edit - object PermPackXyz00_Delete - object PermPackXyz00_AddUser + object PermCustXyz_SELECT + object PermCustXyz_UPDATE + object PermCustXyz_DELETE + object PermCustXyz_INSERT:Package + object PermPackXyz00_SELECT + object PermPackXyz00_EDIT + object PermPackXyz00_DELETE + object PermPackXyz00_INSERT:USER } RbacRoles -[hidden]> RbacPermissions @@ -322,23 +320,23 @@ RoleAdministrators o..> RoleCustXyz_Owner RoleCustXyz_Owner o-> RoleCustXyz_Admin RoleCustXyz_Admin o-> RolePackXyz00_Owner -RoleCustXyz_Owner o--> PermCustXyz_Edit -RoleCustXyz_Owner o--> PermCustXyz_Delete -RoleCustXyz_Admin o--> PermCustXyz_View -RoleCustXyz_Admin o--> PermCustXyz_AddPackage -RolePackXyz00_Owner o--> PermPackXyz00_View -RolePackXyz00_Owner o--> PermPackXyz00_Edit -RolePackXyz00_Owner o--> PermPackXyz00_Delete -RolePackXyz00_Owner o--> PermPackXyz00_AddUser +RoleCustXyz_Owner o--> PermCustXyz_UPDATE +RoleCustXyz_Owner o--> PermCustXyz_DELETE +RoleCustXyz_Admin o--> PermCustXyz_SELECT +RoleCustXyz_Admin o--> PermCustXyz_INSERT:Package +RolePackXyz00_Owner o--> PermPackXyz00_SELECT +RolePackXyz00_Owner o--> PermPackXyz00_UPDATE +RolePackXyz00_Owner o--> PermPackXyz00_DELETE +RolePackXyz00_Owner o--> PermPackXyz00_INSERT:User -PermCustXyz_View o--> CustXyz -PermCustXyz_Edit o--> CustXyz -PermCustXyz_Delete o--> CustXyz -PermCustXyz_AddPackage o--> CustXyz -PermPackXyz00_View o--> PackXyz00 -PermPackXyz00_Edit o--> PackXyz00 -PermPackXyz00_Delete o--> PackXyz00 -PermPackXyz00_AddUser o--> PackXyz00 +PermCustXyz_SELECT o--> CustXyz +PermCustXyz_UPDATE o--> CustXyz +PermCustXyz_DELETE o--> CustXyz +PermCustXyz_INSERT:Package o--> CustXyz +PermPackXyz00_SELECT o--> PackXyz00 +PermPackXyz00_UPDATE o--> PackXyz00 +PermPackXyz00_DELETE o--> PackXyz00 +PermPackXyz00_INSERT:User o--> PackXyz00 @enduml ``` @@ -353,12 +351,12 @@ To support the RBAC system, for each business-object-table, some more artifacts Not yet implemented, but planned are these actions: -- an `ON DELETE ... DO INSTEAD` rule to allow `SQL DELETE` if applicable for the business-object-table and the user has 'delete' permission, -- an `ON UPDATE ... DO INSTEAD` rule to allow `SQL UPDATE` if the user has 'edit' right, -- an `ON INSERT ... DO INSTEAD` rule to allow `SQL INSERT` if the user has 'add-..' right to the parent-business-object. +- an `ON DELETE ... DO INSTEAD` rule to allow `SQL DELETE` if applicable for the business-object-table and the user has 'DELETE' permission, +- an `ON UPDATE ... DO INSTEAD` rule to allow `SQL UPDATE` if the user has 'UPDATE' right, +- an `ON INSERT ... DO INSTEAD` rule to allow `SQL INSERT` if the user has the 'INSERT' right for the parent-business-object. The restricted view takes the current user from a session property and applies the hierarchy of its roles all the way down to the permissions related to the respective business-object-table. -This way, each user can only view the data they have 'view'-permission for, only create those they have 'add-...'-permission, only update those they have 'edit'- and only delete those they have 'delete'-permission to. +This way, each user can only select the data they have 'SELECT'-permission for, only create those they have 'add-...'-permission, only update those they have 'UPDATE'- and only delete those they have 'DELETE'-permission to. ### Current User @@ -458,26 +456,26 @@ allow_mixing entity "BObj customer#xyz" as boCustXyz together { - entity "Perm customer#xyz *" as permCustomerXyzAll - permCustomerXyzAll --> boCustXyz + entity "Perm customer#xyz *" as permCustomerXyzDELETE + permCustomerXyzDELETE --> boCustXyz - entity "Perm customer#xyz add-package" as permCustomerXyzAddPack - permCustomerXyzAddPack --> boCustXyz + entity "Perm customer#xyz INSERT:package" as permCustomerXyzINSERT:package + permCustomerXyzINSERT:package --> boCustXyz - entity "Perm customer#xyz view" as permCustomerXyzView - permCustomerXyzView --> boCustXyz + entity "Perm customer#xyz SELECT" as permCustomerXyzSELECT + permCustomerXyzSELECT--> boCustXyz } entity "Role customer#xyz.tenant" as roleCustXyzTenant -roleCustXyzTenant --> permCustomerXyzView +roleCustXyzTenant --> permCustomerXyzSELECT entity "Role customer#xyz.admin" as roleCustXyzAdmin roleCustXyzAdmin --> roleCustXyzTenant -roleCustXyzAdmin --> permCustomerXyzAddPack +roleCustXyzAdmin --> permCustomerXyzINSERT:package entity "Role customer#xyz.owner" as roleCustXyzOwner roleCustXyzOwner ..> roleCustXyzAdmin -roleCustXyzOwner --> permCustomerXyzAll +roleCustXyzOwner --> permCustomerXyzDELETE actor "Customer XYZ Admin" as actorCustXyzAdmin actorCustXyzAdmin --> roleCustXyzAdmin @@ -487,8 +485,6 @@ roleAdmins --> roleCustXyzOwner actor "Any Hostmaster" as actorHostmaster actorHostmaster --> roleAdmins - - @enduml ``` @@ -527,17 +523,17 @@ allow_mixing entity "BObj package#xyz00" as boPacXyz00 together { - entity "Perm package#xyz00 *" as permPackageXyzAll - permPackageXyzAll --> boPacXyz00 + entity "Perm package#xyz00 *" as permPackageXyzDELETE + permPackageXyzDELETE --> boPacXyz00 - entity "Perm package#xyz00 add-domain" as permPacXyz00AddUser - permPacXyz00AddUser --> boPacXyz00 + entity "Perm package#xyz00 INSERT:domain" as permPacXyz00INSERT:user + permPacXyz00INSERT:user --> boPacXyz00 - entity "Perm package#xyz00 edit" as permPacXyz00Edit - permPacXyz00Edit --> boPacXyz00 + entity "Perm package#xyz00 UPDATE" as permPacXyz00UPDATE + permPacXyz00UPDATE --> boPacXyz00 - entity "Perm package#xyz00 view" as permPacXyz00View - permPacXyz00View --> boPacXyz00 + entity "Perm package#xyz00 SELECT" as permPacXyz00SELECT + permPacXyz00SELECT --> boPacXyz00 } package { @@ -552,11 +548,11 @@ package { entity "Role package#xyz00.tenant" as rolePacXyz00Tenant } -rolePacXyz00Tenant --> permPacXyz00View +rolePacXyz00Tenant --> permPacXyz00SELECT rolePacXyz00Tenant --> roleCustXyzTenant rolePacXyz00Owner --> rolePacXyz00Admin -rolePacXyz00Owner --> permPackageXyzAll +rolePacXyz00Owner --> permPackageXyzDELETE roleCustXyzAdmin --> rolePacXyz00Owner roleCustXyzAdmin --> roleCustXyzTenant @@ -564,8 +560,8 @@ roleCustXyzAdmin --> roleCustXyzTenant roleCustXyzOwner ..> roleCustXyzAdmin rolePacXyz00Admin --> rolePacXyz00Tenant -rolePacXyz00Admin --> permPacXyz00AddUser -rolePacXyz00Admin --> permPacXyz00Edit +rolePacXyz00Admin --> permPacXyz00INSERT:user +rolePacXyz00Admin --> permPacXyz00UPDATE actor "Package XYZ00 Admin" as actorPacXyzAdmin actorPacXyzAdmin -l-> rolePacXyz00Admin @@ -624,10 +620,10 @@ Let's have a look at the two view queries: WHERE target.uuid IN ( SELECT uuid FROM queryAccessibleObjectUuidsOfSubjectIds( - 'view', 'customer', currentSubjectsUuids())); + 'SELECTÄ, 'customer', currentSubjectsUuids())); This view should be automatically updatable. -Where, for updates, we actually have to check for 'edit' instead of 'view' operation, which makes it a bit more complicated. +Where, for updates, we actually have to check for 'UPDATE' instead of 'SELECTÄ operation, which makes it a bit more complicated. With the larger dataset, the test suite initially needed over 7 seconds with this view query. At this point the second variant was tried. @@ -642,7 +638,7 @@ Looks like the query optimizer needed some statistics to find the best path. SELECT DISTINCT target.* FROM customer AS target JOIN queryAccessibleObjectUuidsOfSubjectIds( - 'view', 'customer', currentSubjectsUuids()) AS allowedObjId + 'SELECTÄ, 'customer', currentSubjectsUuids()) AS allowedObjId ON target.uuid = allowedObjId; This view cannot is not updatable automatically, @@ -688,7 +684,7 @@ Otherwise, it would not be possible to assign roles to new users. All roles are system-defined and cannot be created or modified by any external API. -Users can view only the roles to which they are assigned. +Users can view only the roles to which are granted to them. ## RbacGrant diff --git a/sql/rbac-tests.sql b/sql/rbac-tests.sql index 4e179dee..ad017189 100644 --- a/sql/rbac-tests.sql +++ b/sql/rbac-tests.sql @@ -19,13 +19,13 @@ select * FROM queryAllPermissionsOfSubjectId(findRbacUser('rosa@example.com')); select * -FROM queryAllRbacUsersWithPermissionsFor(findEffectivePermissionId('customer', +FROM queryAllRbacUsersWithPermissionsFor(findPermissionId('customer', (SELECT uuid FROM RbacObject WHERE objectTable = 'customer' LIMIT 1), 'add-package')); select * -FROM queryAllRbacUsersWithPermissionsFor(findEffectivePermissionId('package', +FROM queryAllRbacUsersWithPermissionsFor(findPermissionId('package', (SELECT uuid FROM RbacObject WHERE objectTable = 'package' LIMIT 1), - 'delete')); + 'DELETE')); DO LANGUAGE plpgsql $$ @@ -34,12 +34,12 @@ $$ result bool; BEGIN userId = findRbacUser('superuser-alex@hostsharing.net'); - result = (SELECT * FROM isPermissionGrantedToSubject(findEffectivePermissionId('package', 94928, 'add-package'), userId)); + result = (SELECT * FROM isPermissionGrantedToSubject(findPermissionId('package', 94928, 'add-package'), userId)); IF (result) THEN RAISE EXCEPTION 'expected permission NOT to be granted, but it is'; end if; - result = (SELECT * FROM isPermissionGrantedToSubject(findEffectivePermissionId('package', 94928, 'view'), userId)); + result = (SELECT * FROM isPermissionGrantedToSubject(findPermissionId('package', 94928, 'SELECT'), userId)); IF (NOT result) THEN RAISE EXCEPTION 'expected permission to be granted, but it is NOT'; end if; diff --git a/sql/rbac-view-option-experiments.sql b/sql/rbac-view-option-experiments.sql index d3ef736a..2c4508ae 100644 --- a/sql/rbac-view-option-experiments.sql +++ b/sql/rbac-view-option-experiments.sql @@ -20,7 +20,7 @@ CREATE POLICY customer_policy ON customer TO restricted USING ( -- id=1000 - isPermissionGrantedToSubject(findEffectivePermissionId('test_customer', id, 'view'), currentUserUuid()) + isPermissionGrantedToSubject(findPermissionId('test_customer', id, 'SELECT'), currentUserUuid()) ); SET SESSION AUTHORIZATION restricted; @@ -35,7 +35,7 @@ SELECT * FROM customer; CREATE OR REPLACE RULE "_RETURN" AS ON SELECT TO cust_view DO INSTEAD - SELECT * FROM customer WHERE isPermissionGrantedToSubject(findEffectivePermissionId('test_customer', id, 'view'), currentUserUuid()); + SELECT * FROM customer WHERE isPermissionGrantedToSubject(findPermissionId('test_customer', id, 'SELECT'), currentUserUuid()); SELECT * from cust_view LIMIT 10; select queryAllPermissionsOfSubjectId(findRbacUser('superuser-alex@hostsharing.net')); @@ -52,7 +52,7 @@ CREATE OR REPLACE RULE "_RETURN" AS DO INSTEAD SELECT c.uuid, c.reference, c.prefix FROM customer AS c JOIN queryAllPermissionsOfSubjectId(currentUserUuid()) AS p - ON p.objectTable='test_customer' AND p.objectUuid=c.uuid AND p.op in ('*', 'view'); + ON p.objectTable='test_customer' AND p.objectUuid=c.uuid AND p.op = 'SELECT'; GRANT ALL PRIVILEGES ON cust_view TO restricted; SET SESSION SESSION AUTHORIZATION restricted; @@ -68,7 +68,7 @@ CREATE OR REPLACE VIEW cust_view AS SELECT c.uuid, c.reference, c.prefix FROM customer AS c JOIN queryAllPermissionsOfSubjectId(currentUserUuid()) AS p - ON p.objectUuid=c.uuid AND p.op in ('*', 'view'); + ON p.objectUuid=c.uuid AND p.op = 'SELECT'; GRANT ALL PRIVILEGES ON cust_view TO restricted; SET SESSION SESSION AUTHORIZATION restricted; @@ -81,7 +81,7 @@ select rr.uuid, rr.type from RbacGrants g join RbacReference RR on g.ascendantUuid = RR.uuid where g.descendantUuid in ( select uuid from queryAllPermissionsOfSubjectId(findRbacUser('alex@example.com')) - where objectTable='test_customer' and op in ('*', 'view')); + where objectTable='test_customer' and op = 'SELECT'); call grantRoleToUser(findRoleId('test_customer#aaa.admin'), findRbacUser('aaaaouq@example.com')); 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 24258251..de256ca1 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 @@ -64,17 +64,17 @@ public class HsOfficeBankAccountEntity implements HasUuid, Stringifyable { .createRole(OWNER, (with) -> { with.owningUser(CREATOR); with.incomingSuperRole(GLOBAL, ADMIN); - with.permission(ALL); + with.permission(DELETE); }) .createSubRole(ADMIN, (with) -> { - with.permission(EDIT); + with.permission(UPDATE); }) .createSubRole(REFERRER, (with) -> { - with.permission(VIEW); + with.permission(SELECT); }); } public static void main(String[] args) throws IOException { - rbac().generateWithBaseFileName("243-hs-office-bankaccount-rbac"); + rbac().generateWithBaseFileName("243-hs-office-bankaccount-rbac-generated"); } } 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 7d655fc3..406b232c 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 @@ -68,17 +68,17 @@ public class HsOfficeContactEntity implements Stringifyable, HasUuid { .createRole(OWNER, (with) -> { with.owningUser(CREATOR); with.incomingSuperRole(GLOBAL, ADMIN); - with.permission(ALL); + with.permission(DELETE); }) .createSubRole(ADMIN, (with) -> { - with.permission(EDIT); + with.permission(UPDATE); }) .createSubRole(REFERRER, (with) -> { - with.permission(VIEW); + with.permission(SELECT); }); } public static void main(String[] args) throws IOException { - rbac().generateWithBaseFileName("203-hs-office-contact-rbac"); + rbac().generateWithBaseFileName("203-hs-office-contact-rbac-generated"); } } 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 89dcf05d..29a9452d 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 @@ -140,9 +140,9 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable { WHERE r.relType = 'ACCOUNTING' AND r.relHolderUuid = ${REF}.debitorRelUuid """), dependsOnColumn("debitorRelUuid")) - .createPermission(ALL).grantedTo("debitorRel", OWNER) - .createPermission(EDIT).grantedTo("debitorRel", ADMIN) - .createPermission(VIEW).grantedTo("debitorRel", TENANT) + .createPermission(DELETE).grantedTo("debitorRel", OWNER) + .createPermission(UPDATE).grantedTo("debitorRel", ADMIN) + .createPermission(SELECT).grantedTo("debitorRel", TENANT) .importEntityAlias("refundBankAccount", HsOfficeBankAccountEntity.class, dependsOnColumn("refundBankAccountUuid"), fetchedBySql(""" @@ -171,6 +171,6 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable { } public static void main(String[] args) throws IOException { - rbac().generateWithBaseFileName("273-hs-office-debitor-rbac"); + rbac().generateWithBaseFileName("273-hs-office-debitor-rbac-generated"); } } 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 dbc6c17c..e557f9ae 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 @@ -103,6 +103,6 @@ public class HsOfficePartnerDetailsEntity implements HasUuid, Stringifyable { } public static void main(String[] args) throws IOException { - rbac().generateWithBaseFileName("234-hs-office-partner-details-rbac"); + rbac().generateWithBaseFileName("234-hs-office-partner-details-rbac-generated"); } } 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 cbd184c6..aa000f67 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 @@ -20,7 +20,7 @@ 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.Permission.VIEW; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.SELECT; 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; @@ -95,19 +95,19 @@ public class HsOfficePartnerEntity implements Stringifyable, HasUuid { .importRootEntityAliasProxy("partnerRel", HsOfficeRelationshipEntity.class, fetchedBySql("SELECT * FROM hs_office_relationship AS r WHERE r.uuid = ${ref}.partnerRoleUuid"), dependsOnColumn("partnerRelUuid")) - .createPermission(ALL).grantedTo("partnerRel", ADMIN) - .createPermission(EDIT).grantedTo("partnerRel", AGENT) - .createPermission(VIEW).grantedTo("partnerRel", TENANT) + .createPermission(DELETE).grantedTo("partnerRel", ADMIN) + .createPermission(UPDATE).grantedTo("partnerRel", AGENT) + .createPermission(SELECT).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); + .createPermission("partnerDetails", DELETE).grantedTo("partnerRel", ADMIN) + .createPermission("partnerDetails", UPDATE).grantedTo("partnerRel", AGENT) + .createPermission("partnerDetails", SELECT).grantedTo("partnerRel", AGENT); } public static void main(String[] args) throws IOException { - rbac().generateWithBaseFileName("233-hs-office-partner-rbac"); + rbac().generateWithBaseFileName("233-hs-office-partner-rbac-generated"); } } 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 37874df3..fcc89dde 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 @@ -70,20 +70,20 @@ public class HsOfficePersonEntity implements HasUuid, Stringifyable { .withIdentityView(SQL.projection("concat(tradeName, familyName, givenName)")) .withUpdatableColumns("personType", "tradeName", "givenName", "familyName") .createRole(OWNER, (with) -> { - with.permission(ALL); + with.permission(DELETE); with.owningUser(CREATOR); with.incomingSuperRole(GLOBAL, ADMIN); }) .createSubRole(ADMIN, (with) -> { - with.permission(EDIT); + with.permission(UPDATE); }) .createSubRole(REFERRER, (with) -> { - with.permission(VIEW); + with.permission(SELECT); }); } public static void main(String[] args) throws IOException { - rbac().generateWithBaseFileName("213-hs-office-person-rbac"); + rbac().generateWithBaseFileName("213-hs-office-person-rbac-generated"); } } 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 bb555162..1ec9fd74 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 @@ -103,11 +103,11 @@ public class HsOfficeRelationshipEntity implements HasUuid, Stringifyable { .createRole(OWNER, (with) -> { with.owningUser(CREATOR); with.incomingSuperRole(GLOBAL, ADMIN); - with.permission(ALL); + with.permission(DELETE); }) .createSubRole(ADMIN, (with) -> { with.incomingSuperRole("anchorPerson", ADMIN); - with.permission(EDIT); + with.permission(UPDATE); }) .createSubRole(AGENT, (with) -> { with.incomingSuperRole("holderPerson", ADMIN); @@ -118,11 +118,11 @@ public class HsOfficeRelationshipEntity implements HasUuid, Stringifyable { with.outgoingSubRole("anchorPerson", REFERRER); with.outgoingSubRole("holderPerson", REFERRER); with.outgoingSubRole("contact", REFERRER); - with.permission(VIEW); + with.permission(SELECT); }); } public static void main(String[] args) throws IOException { - rbac().generateWithBaseFileName("223-hs-office-relationship-rbac"); + rbac().generateWithBaseFileName("223-hs-office-relationship-rbac-generated"); } } 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 ac0d6f99..7fcef622 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 @@ -105,10 +105,10 @@ public class HsOfficeSepaMandateEntity implements Stringifyable, HasUuid { .createRole(OWNER, (with) -> { with.owningUser(CREATOR); with.incomingSuperRole(GLOBAL, ADMIN); - with.permission(ALL); + with.permission(DELETE); }) .createSubRole(ADMIN, (with) -> { - with.permission(EDIT); + with.permission(UPDATE); }) .createSubRole(AGENT, (with) -> { with.outgoingSubRole("bankAccount", REFERRER); @@ -118,11 +118,11 @@ public class HsOfficeSepaMandateEntity implements Stringifyable, HasUuid { with.incomingSuperRole("bankAccount", ADMIN); with.incomingSuperRole("debitorRel", AGENT); with.outgoingSubRole("debitorRel", TENANT); - with.permission(VIEW); + with.permission(SELECT); }); } public static void main(String[] args) throws IOException { - rbac().generateWithBaseFileName("253-hs-office-sepamandate-rbac"); + rbac().generateWithBaseFileName("253-hs-office-sepamandate-rbac-generated"); } } diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java index bff58ce5..adcb1c36 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java @@ -1,7 +1,12 @@ package net.hostsharing.hsadminng.rbac.rbacdef; +import java.util.Optional; + +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.PERM_TO_ROLE; 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; public class InsertTriggerGenerator { @@ -16,25 +21,8 @@ public class InsertTriggerGenerator { 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() )); - }); - + generateInsertPermissionGrantTrigger(plPgSql); + generateInsertCheckTrigger(plPgSql); plPgSql.writeLn("--//"); } @@ -48,29 +36,104 @@ public class InsertTriggerGenerator { } private void generateGrantInsertRoleToExistingCustomers(final StringWriter plPgSql) { - plPgSql.writeLn(""" + getOptionalInsertSuperRole().ifPresent( superRoleDef -> { + plPgSql.writeLn(""" /* - Creates an INSERT INTO ${rawSubTableName} permission for the related ${rawSuperTableName} row. + Creates INSERT INTO ${rawSubTableName} permissions for the related ${rawSuperTableName} rows. */ do language plpgsql $$ declare row ${rawSuperTableName}; - permissionUuids uuid[]; + permissionUuid uuid; roleUuid uuid; begin + call defineContext('generated Liquibase: create INSERT INTO ${rawSubTableName} permissions for the related ${rawSuperTableName} rows'); + FOR row IN SELECT * FROM ${rawSuperTableName} LOOP - roleUuid := ${rawSuperRoleDescriptor}(row); - permissionUuids := createPermissions(row.uuid, array ['INSERT:${rawSubTableName}']); - call grantPermissionsToRole(roleUuid, permissionUuids); + roleUuid := findRoleId(${rawSuperRoleDescriptor}(row)); + permissionUuid := createPermission(row.uuid, 'INSERT', '${rawSubTableName}'); + call grantPermissionToRole(roleUuid, permissionUuid); END LOOP; END; $$; """, - with("rawSubTableName", "test_package"), // TODO - with("rawSuperTableName", "test_customer"), // TODO - with("rawSuperRoleDescriptor", "testCustomerAdmin") // TODO + with("rawSubTableName", rbacDef.getRootEntityAlias().getRawTableName()), + with("rawSuperTableName", superRoleDef.getEntityAlias().getRawTableName()), + with("rawSuperRoleDescriptor", toVar(superRoleDef)) ); + }); + } + + private void generateInsertPermissionGrantTrigger(final StringWriter plPgSql) { + getOptionalInsertSuperRole().ifPresent( superRoleDef -> { + plPgSql.writeLn(""" + /** + Adds ${rawSubTableName} INSERT permission to specified role of new ${rawSuperTableName} rows. + */ + create or replace function ${rawSubTableName}_${rawSuperTableName}_insert_tf() + returns trigger + language plpgsql + strict as $$ + begin + call grantPermissionToRole( + ${rawSuperRoleDescriptor}(NEW), + createPermission(NEW.uuid, 'INSERT', '${rawSubTableName}')); + return NEW; + end; $$; + + create trigger ${rawSubTableName}_${rawSuperTableName}_insert_tg + after insert on ${rawSuperTableName} + for each row + execute procedure ${rawSubTableName}_${rawSuperTableName}_insert_tf(); + """, + with("rawSubTableName", rbacDef.getRootEntityAlias().getRawTableName()), + with("rawSuperTableName", superRoleDef.getEntityAlias().getRawTableName()), + with("rawSuperRoleDescriptor", toVar(superRoleDef)) + ); + }); + } + + private void generateInsertCheckTrigger(final StringWriter plPgSql) { + rbacDef.getGrantDefs().stream() + .filter(g -> g.isToCreate() && g.grantType() == PERM_TO_ROLE && + g.getPermDef().getPermission() == INSERT ) + .forEach(g -> { + plPgSql.writeLn(""" + /** + Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable}. + */ + create or replace function ${rawSubTable}_insert_permission_missing_tf() + returns trigger + language plpgsql as $$ + begin + raise exception 'insert into ${rawSubTable} not allowed for current subjects %', currentSubjectsUuids(); + end; $$; + + create trigger ${rawSubTable}_insert_permission_check_tg + before insert on ${rawSubTable} + for each row + when ( not hasInsertPermission(NEW.${referenceColumn}, 'INSERT', '${rawSubTable}') ) + execute procedure ${rawSubTable}_insert_permission_missing_tf(); + """, + with("rawSubTable", g.getPermDef().entityAlias.getRawTableName()), + with("referenceColumn", g.getSuperRoleDef().getEntityAlias().dependsOnColumName() )); + }); + } + + private Optional getOptionalInsertSuperRole() { + return rbacDef.getGrantDefs().stream() + .filter(g -> g.grantType() == PERM_TO_ROLE) + .filter(g -> g.getPermDef().toCreate && g.getPermDef().getPermission() == INSERT) + .map(RbacView.RbacGrantDefinition::getSuperRoleDef) + .reduce((x, y) -> { + throw new IllegalStateException("only a single INSERT permission grant allowed"); + }); + } + + + private static String toVar(final RbacView.RbacRoleDefinition roleDef) { + return uncapitalize(roleDef.getEntityAlias().simpleName()) + capitalize(roleDef.getRole().roleName()); } } 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 b92f8f38..064a7350 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -75,6 +75,7 @@ public class RbacView { public RbacView withUpdatableColumns(final String... columnNames) { Collections.addAll(updatableColumns, columnNames); + // TODO: automatically add @Version column, otherwise optimistic locking won't work return this; } @@ -249,8 +250,8 @@ public class RbacView { } 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")); + new RbacViewMermaidFlowchart(this).generateToMarkdownFile(Path.of(OUTPUT_BASEDIR, baseFileName + ".md")); + new RbacViewPostgresGenerator(this).generateToChangeLog(Path.of(OUTPUT_BASEDIR, baseFileName + ".sql")); } public class RbacGrantBuilder { @@ -465,6 +466,7 @@ public class RbacView { public class RbacUserReference { public enum UserRole { + GLOBAL_ADMIN, CREATOR } @@ -628,10 +630,10 @@ public class RbacView { 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"); + public static final Permission INSERT = new Permission("INSERT"); + public static final Permission DELETE = new Permission("DELETE"); + public static final Permission UPDATE = new Permission("UPDATE"); + public static final Permission SELECT = new Permission("SELECT"); public static Permission custom(final String permission) { return new Permission(permission); @@ -671,7 +673,7 @@ public class RbacView { } /** - * DSL method to specify there there is no SQL query specified. + * DSL method to explicitly specify that there is no SQL query. * * @return a wrapped SQL definition object representing a noop query */ 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 ba01d61f..624ee471 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java @@ -81,8 +81,10 @@ class RolesGrantsAndPermissionsGenerator { plPgSql.writeLn(); plPgSql.writeLn("begin"); plPgSql.indented(() -> { + plPgSql.writeLn("call enterTriggerForObjectUuid(NEW.uuid);"); generateCreateRolesAndGrantsAfterInsert(plPgSql); plPgSql.ensureSingleEmptyLine(); + plPgSql.writeLn("call leaveTriggerForObjectUuid(NEW.uuid);"); }); plPgSql.writeLn("end; $$;"); plPgSql.writeLn(); @@ -94,7 +96,7 @@ class RolesGrantsAndPermissionsGenerator { Called from the AFTER UPDATE TRIGGER to re-wire the grants. */ - create or replace procedure updateRbacGrantsFor${simpleEntityName}( + create or replace procedure updateRbacRulesFor${simpleEntityName}( OLD ${rawTableName}, NEW ${rawTableName} ) @@ -117,8 +119,10 @@ class RolesGrantsAndPermissionsGenerator { plPgSql.writeLn(); plPgSql.writeLn("begin"); plPgSql.indented(() -> { + plPgSql.writeLn("call enterTriggerForObjectUuid(NEW.uuid);"); generateUpdateRolesAndGrantsAfterUpdate(plPgSql); plPgSql.ensureSingleEmptyLine(); + plPgSql.writeLn("call leaveTriggerForObjectUuid(NEW.uuid);"); }); plPgSql.writeLn("end; $$;"); plPgSql.writeLn(); @@ -218,7 +222,7 @@ class RolesGrantsAndPermissionsGenerator { .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("${permRef}", findPerm(OLD, grantDef.getPermDef())) .replace("${superRoleRef}", roleRef(OLD, grantDef.getSuperRoleDef())); }; } @@ -231,14 +235,23 @@ class RolesGrantsAndPermissionsGenerator { .replace("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef())); case PERM_TO_ROLE -> grantDef.getPermDef().getPermission() == INSERT ? "" - : "call grantPermissionsToRole(${permRef}, ${superRoleRef});" - .replace("${permRef}", permRef(NEW, grantDef.getPermDef())) + : "call grantPermissionToRole(${permRef}, ${superRoleRef});" + .replace("${permRef}", createPerm(NEW, grantDef.getPermDef())) .replace("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef())); }; } - private String permRef(final PostgresTriggerReference ref, final RbacPermissionDefinition permDef) { - return "createPermissions(${entityRef}.uuid, array ['${perm}'])" + private String findPerm(final PostgresTriggerReference ref, final RbacPermissionDefinition permDef) { + return permRef("findPermissionId", ref, permDef); + } + + private String createPerm(final PostgresTriggerReference ref, final RbacPermissionDefinition permDef) { + return permRef("createPermission", ref, permDef); + } + + private String permRef(final String functionName, final PostgresTriggerReference ref, final RbacPermissionDefinition permDef) { + return "${prefix}(${entityRef}.uuid, '${perm}')" + .replace("${prefix}", functionName) .replace("${entityRef}", rbacDef.isRootEntityAlias(permDef.entityAlias) ? ref.name() : refVarName(ref, permDef.entityAlias)) @@ -412,13 +425,12 @@ class RolesGrantsAndPermissionsGenerator { language plpgsql strict as $$ begin - call buildRbacSystemFor${simpleEntityName}(TG_OP, OLD, NEW); + call buildRbacSystemFor${simpleEntityName}(NEW); return NEW; end; $$; create trigger insertTriggerFor${simpleEntityName}_tg - after insert - on ${rawTableName} + after insert on ${rawTableName} for each row execute procedure insertTriggerFor${simpleEntityName}_tf(); """ @@ -444,13 +456,12 @@ class RolesGrantsAndPermissionsGenerator { language plpgsql strict as $$ begin - call buildRbacSystemFor${simpleEntityName}(NEW); + call updateRbacRulesFor${simpleEntityName}(OLD, NEW); return NEW; end; $$; create trigger updateTriggerFor${simpleEntityName}_tg - after update - on ${rawTableName} + after update on ${rawTableName} for each row execute procedure updateTriggerFor${simpleEntityName}_tf(); """ 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 57e29475..8101286b 100644 --- a/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java @@ -43,13 +43,15 @@ public class TestCustomerEntity implements HasUuid { .withUpdatableColumns("reference", "prefix", "adminUserName") .createRole(OWNER, (with) -> { - with.owningUser(CREATOR); + // with.owningUser(CREATOR); TODO: needs assumed role with.incomingSuperRole(GLOBAL, ADMIN); - with.permission(ALL); + with.permission(DELETE); + }) + .createSubRole(ADMIN, (with) -> { + with.permission(UPDATE); }) - .createSubRole(ADMIN) .createSubRole(TENANT, (with) -> { - with.permission(VIEW); + with.permission(SELECT); }); } 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 ceb88ec8..81d577bc 100644 --- a/src/main/java/net/hostsharing/hsadminng/test/pac/TestPackageEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/test/pac/TestPackageEntity.java @@ -47,7 +47,7 @@ public class TestPackageEntity implements HasUuid { public static RbacView rbac() { return rbacViewFor("package", TestPackageEntity.class) .withIdentityView(SQL.projection("name")) - .withUpdatableColumns("customerUuid", "description") + .withUpdatableColumns("version", "customerUuid", "description") .importEntityAlias("customer", TestCustomerEntity.class, dependsOnColumn("customerUuid"), @@ -60,13 +60,13 @@ public class TestPackageEntity implements HasUuid { .createRole(OWNER, (with) -> { with.owningUser(CREATOR); with.incomingSuperRole("customer", ADMIN).unassumed(); - with.permission(ALL); - with.permission(EDIT); + with.permission(DELETE); + with.permission(UPDATE); }) .createSubRole(ADMIN) .createSubRole(TENANT, (with) -> { with.outgoingSubRole("customer", TENANT); - with.permission(VIEW); + with.permission(SELECT); }); } diff --git a/src/main/resources/db/changelog/010-context.sql b/src/main/resources/db/changelog/010-context.sql index 4820cf9c..11d52f50 100644 --- a/src/main/resources/db/changelog/010-context.sql +++ b/src/main/resources/db/changelog/010-context.sql @@ -66,10 +66,11 @@ begin when others then currentTask := null; end; - if (currentTask is null or currentTask = '') then - raise exception '[401] currentTask must be defined, please call `defineContext(...)`'; - end if; - return currentTask; +-- TODO: uncomment +-- if (currentTask is null or currentTask = '') then +-- raise exception '[401] currentTask must be defined, please call `defineContext(...)`'; +-- end if; + return 'unknown'; -- TODO: currentTask; end; $$; --// diff --git a/src/main/resources/db/changelog/050-rbac-base.sql b/src/main/resources/db/changelog/050-rbac-base.sql index 5e2726f7..9a8926c6 100644 --- a/src/main/resources/db/changelog/050-rbac-base.sql +++ b/src/main/resources/db/changelog/050-rbac-base.sql @@ -365,16 +365,18 @@ create trigger deleteRbacRolesOfRbacObject_Trigger /* */ -create domain RbacOp as varchar(67) - check ( - VALUE = '*' - or VALUE = 'delete' - or VALUE = 'edit' - or VALUE = 'view' - or VALUE = 'assume' - or VALUE ~ '^add-[a-z]+$' - or VALUE ~ '^new-[a-z-]+$' - ); +create domain RbacOp as varchar(67) -- TODO: shorten to 8, once the deprecated values are gone +-- check ( +-- VALUE = 'INSERT' or +-- VALUE = 'DELETE' or +-- VALUE = 'UPDATE' or +-- VALUE = 'SELECT' or +-- VALUE = 'ASSUME' or +-- -- TODO: all values below are deprecated, use insert with table +-- VALUE ~ '^add-[a-z]+$' or +-- VALUE ~ '^new-[a-z-]+$' +-- ); +; create table RbacPermission ( @@ -394,37 +396,38 @@ select exists( select op from RbacPermission p where p.objectUuid = forObjectUuid - and p.op in ('*', forOp) + and p.op = forOp ); $$; -create or replace function createPermissions(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null) - returns uuid[] +create or replace function createPermission(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null) + returns uuid language plpgsql as $$ declare - permissionId uuid; + permissionUuid uuid; begin if (forObjectUuid is null) then raise exception 'forObjectUuid must not be null'; end if; - if (forOp = 'INSERT' && forOpTableName is null) then + if (forOp = 'INSERT' and forOpTableName is null) then raise exception 'INSERT permissions needs forOpTableName'; end if; - if (forOp <> 'INSERT' && forOpTableName is not null) then + if (forOp <> 'INSERT' and 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 + permissionUuid = (select uuid from RbacPermission where objectUuid = forObjectUuid and op = forOp and opTableName = forOpTableName); + if (permissionUuid is null) then insert into RbacReference ("type") values ('RbacPermission') - returning uuid into permissionId; + returning uuid into permissionUuid; + raise warning 'for values (%, %, %, %)', permissionUuid, forObjectUuid, forOp, forOpTableName; -- TODO: remove insert into RbacPermission (uuid, objectUuid, op, opTableName) - values (permissionId, forObjectUuid, forOp, opTableName); + values (permissionUuid, forObjectUuid, forOp, forOpTableName); end if; - return permissionId; + return permissionUuid; end; $$; @@ -439,9 +442,6 @@ begin if (forObjectUuid is null) then raise exception 'forObjectUuid must not be null'; end if; - if (array_length(permitOps, 1) > 1 and '*' = any (permitOps)) then - raise exception '"*" operation must not be assigned along with other operations: %', permitOps; - end if; for i in array_lower(permitOps, 1)..array_upper(permitOps, 1) loop @@ -462,7 +462,7 @@ begin end; $$; -create or replace function findPermissionId(forObjectUuid uuid, forOp RbacOp, opTableName text = null ) +create or replace function findPermissionId(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null ) returns uuid returns null on null input stable -- leakproof @@ -471,24 +471,9 @@ select uuid from RbacPermission p where p.objectUuid = forObjectUuid and p.op = forOp - and p.opTableName = opTableName + and p.opTableName = forOpTableName $$; -create or replace function findEffectivePermissionId(forObjectUuid uuid, forOp RbacOp) - returns uuid - returns null on null input - stable -- leakproof - language plpgsql as $$ -declare - permissionId uuid; -begin - permissionId := findPermissionId(forObjectUuid, forOp); - if permissionId is null and forOp <> '*' then - permissionId := findPermissionId(forObjectUuid, '*'); - end if; - return permissionId; -end $$; - --// -- ============================================================================ @@ -592,8 +577,8 @@ create or replace function hasInsertPermission(objectUuid uuid, forOp RbacOp, ta declare permissionUuid uuid; begin - permissionUuid = findPermissionId(objectUuid, forOp); - + permissionUuid = findPermissionId(objectUuid, forOp, tableName); + return permissionUuid is not null; end; $$; @@ -611,6 +596,20 @@ select exists( ); $$; +create or replace procedure grantPermissionToRole(roleUuid uuid, permissionUuid uuid) + language plpgsql as $$ +begin + perform assertReferenceType('roleId (ascendant)', roleUuid, 'RbacRole'); + perform assertReferenceType('permissionId (descendant)', permissionUuid, 'RbacPermission'); + + insert + into RbacGrants (grantedByTriggerOf, ascendantUuid, descendantUuid, assumed) + values (currentTriggerObjectUuid(), roleUuid, permissionUuid, true) + on conflict do nothing; -- allow granting multiple times +end; +$$; + +-- TODO: deprecated, remove and use grantPermissionToRole(...) create or replace procedure grantPermissionsToRole(roleUuid uuid, permissionIds uuid[]) language plpgsql as $$ begin @@ -742,7 +741,7 @@ begin select descendantUuid from grants) as granted join RbacPermission perm - on granted.descendantUuid = perm.uuid and perm.op in ('*', requiredOp) + on granted.descendantUuid = perm.uuid and perm.op = requiredOp join RbacObject obj on obj.uuid = perm.objectUuid and obj.objectTable = forObjectTable limit maxObjects + 1; @@ -834,6 +833,5 @@ do $$ create role restricted; grant all privileges on all tables in schema public to restricted; end if; - end $$ + end $$; --// - diff --git a/src/main/resources/db/changelog/051-rbac-user-grant.sql b/src/main/resources/db/changelog/051-rbac-user-grant.sql index 23dcbdd4..05332ed9 100644 --- a/src/main/resources/db/changelog/051-rbac-user-grant.sql +++ b/src/main/resources/db/changelog/051-rbac-user-grant.sql @@ -30,7 +30,7 @@ begin insert into RbacGrants (grantedByRoleUuid, ascendantUuid, descendantUuid, assumed) values (grantedByRoleUuid, userUuid, roleUuid, doAssume); - -- TODO.spec: What should happen on mupltiple grants? What if options (doAssume) are not the same? + -- TODO.spec: What should happen on multiple grants? What if options (doAssume) are not the same? -- Most powerful or latest grant wins? What about managed? -- on conflict do nothing; -- allow granting multiple times end; $$; @@ -99,4 +99,19 @@ begin where g.ascendantUuid = userUuid and g.descendantUuid = grantedRoleUuid and g.grantedByRoleUuid = revokeRoleFromUser.grantedByRoleUuid; end; $$; ---/ +--// + +-- ============================================================================ +--changeset rbac-user-grant-REVOKE-PERMISSION-FROM-ROLE:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +create or replace procedure revokePermissionFromRole(permissionUuid uuid, superRoleUuid uuid) + language plpgsql as $$ +begin + -- TODO: call checkRevokeRoleFromUserPreconditions(grantedByRoleUuid, grantedRoleUuid, userUuid); + + raise INFO 'delete from RbacGrants where ascendantUuid = % and descendantUuid = %', superRoleUuid, permissionUuid; + delete from RbacGrants as g + where g.ascendantUuid = superRoleUuid and g.descendantUuid = permissionUuid; +end; $$; +--// diff --git a/src/main/resources/db/changelog/057-rbac-role-builder.sql b/src/main/resources/db/changelog/057-rbac-role-builder.sql index 81a81590..93b36909 100644 --- a/src/main/resources/db/changelog/057-rbac-role-builder.sql +++ b/src/main/resources/db/changelog/057-rbac-role-builder.sql @@ -73,9 +73,10 @@ begin if cardinality(userUuids) > 0 then if grantedByRole is null then - raise exception 'to directly assign users to roles, grantingRole has to be given'; + grantedByRoleUuid := roleUuid; + else + grantedByRoleUuid := getRoleId(grantedByRole, 'fail'); end if; - grantedByRoleUuid := getRoleId(grantedByRole, 'fail'); foreach userUuid in array userUuids loop call grantRoleToUserUnchecked(grantedByRoleUuid, roleUuid, userUuid); diff --git a/src/main/resources/db/changelog/058-rbac-generators.sql b/src/main/resources/db/changelog/058-rbac-generators.sql index fa198308..d77d1f20 100644 --- a/src/main/resources/db/changelog/058-rbac-generators.sql +++ b/src/main/resources/db/changelog/058-rbac-generators.sql @@ -13,8 +13,7 @@ declare begin createInsertTriggerSQL = format($sql$ create trigger createRbacObjectFor_%s_Trigger - before insert - on %s + before insert on %s for each row execute procedure insertRelatedRbacObject(); $sql$, targetTable, targetTable); @@ -145,13 +144,13 @@ begin targetTable := lower(targetTable); /* - Creates a restricted view based on the 'view' permission of the current subject. + Creates a restricted view based on the 'SELECT' permission of the current subject. */ sql := format($sql$ set session session authorization default; create view %1$s_rv as with accessibleObjects as ( - select queryAccessibleObjectUuidsOfSubjectIds('view', '%1$s', currentSubjectsUuids()) + select queryAccessibleObjectUuidsOfSubjectIds('SELECT', '%1$s', currentSubjectsUuids()) ) select target.* from %1$s as target @@ -200,7 +199,7 @@ begin returns trigger language plpgsql as $f$ begin - if old.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('delete', '%1$s', currentSubjectsUuids())) then + if old.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('DELETE', '%1$s', currentSubjectsUuids())) then delete from %1$s p where p.uuid = old.uuid; return old; end if; @@ -223,7 +222,7 @@ begin /** Instead of update trigger function for the restricted view - based on the 'edit' permission of the current subject. + based on the 'UPDATE' permission of the current subject. */ if columnUpdates is not null then sql := format($sql$ @@ -231,7 +230,7 @@ begin returns trigger language plpgsql as $f$ begin - if old.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('edit', '%1$s', currentSubjectsUuids())) then + if old.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('UPDATE', '%1$s', currentSubjectsUuids())) then update %1$s set %2$s where uuid = old.uuid; diff --git a/src/main/resources/db/changelog/113-test-customer-rbac.sql b/src/main/resources/db/changelog/113-test-customer-rbac.sql index d7682cc1..1e5573cc 100644 --- a/src/main/resources/db/changelog/113-test-customer-rbac.sql +++ b/src/main/resources/db/changelog/113-test-customer-rbac.sql @@ -1,4 +1,6 @@ --liquibase formatted sql +-- This code generated was by RbacViewPostgresGenerator at 2024-03-06T15:40:13.239729250. + -- ============================================================================ --changeset test-customer-rbac-OBJECT:1 endDelimiter:--// @@ -7,6 +9,7 @@ call generateRelatedRbacObject('test_customer'); --// + -- ============================================================================ --changeset test-customer-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--// -- ---------------------------------------------------------------------------- @@ -15,82 +18,85 @@ call generateRbacRoleDescriptors('testCustomer', 'test_customer'); -- ============================================================================ ---changeset test-customer-rbac-ROLES-CREATION:1 endDelimiter:--// +--changeset test-customer-rbac-insert-trigger:1 endDelimiter:--// -- ---------------------------------------------------------------------------- /* - Creates the roles and their assignments for a new customer for the AFTER INSERT TRIGGER. + A Creates the roles, grants and permission for the AFTER INSERT TRIGGER. */ -create or replace function createRbacRolesForTestCustomer() - returns trigger - language plpgsql - strict as $$ -declare - testCustomerOwnerUuid uuid; - customerAdminUuid uuid; -begin - if TG_OP <> 'INSERT' then - raise exception 'invalid usage of TRIGGER AFTER INSERT'; - end if; +create or replace procedure buildRbacSystemForTestCustomer( + NEW test_customer +) + language plpgsql as $$ +declare + +begin call enterTriggerForObjectUuid(NEW.uuid); - -- the owner role with full access for Hostsharing administrators - testCustomerOwnerUuid = createRoleWithGrants( + perform createRoleWithGrants( testCustomerOwner(NEW), - permissions => array['*'], - incomingSuperRoles => array[globalAdmin()] - ); + permissions => array['DELETE'], + incomingSuperRoles => array[globalAdmin()] + ); - -- the admin role for the customer's admins, who can view and add products - customerAdminUuid = createRoleWithGrants( + perform createRoleWithGrants( testCustomerAdmin(NEW), - permissions => array['view', 'add-package'], - -- NO auto assume for customer owner to avoid exploding permissions for administrators - userUuids => array[getRbacUserId(NEW.adminUserName, 'create')], -- implicitly ignored if null - grantedByRole => globalAdmin() - ); + permissions => array['UPDATE'], + incomingSuperRoles => array[testCustomerOwner(NEW)] + ); - -- allow the customer owner role (thus administrators) to assume the customer admin role - call grantRoleToRole(customerAdminUuid, testCustomerOwnerUuid, false); - - -- the tenant role which later can be used by owners+admins of sub-objects perform createRoleWithGrants( testCustomerTenant(NEW), - permissions => array['view'] - ); + permissions => array['SELECT'], + incomingSuperRoles => array[testCustomerAdmin(NEW)] + ); call leaveTriggerForObjectUuid(NEW.uuid); - return NEW; end; $$; /* - An AFTER INSERT TRIGGER which creates the role structure for a new customer. + AFTER INSERT TRIGGER to create the role+grant structure for a new test_customer row. */ -drop trigger if exists createRbacRolesForTestCustomer_Trigger on test_customer; -create trigger createRbacRolesForTestCustomer_Trigger - after insert - on test_customer +create or replace function insertTriggerForTestCustomer_tf() + returns trigger + language plpgsql + strict as $$ +begin + call buildRbacSystemForTestCustomer(NEW); + return NEW; +end; $$; + +create trigger insertTriggerForTestCustomer_tg + after insert on test_customer for each row -execute procedure createRbacRolesForTestCustomer(); +execute procedure insertTriggerForTestCustomer_tf(); + --// +-- ============================================================================ +--changeset test-customer-rbac-INSERT:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +--// -- ============================================================================ --changeset test-customer-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- call generateRbacIdentityView('test_customer', $idName$ - target.prefix + prefix $idName$); --// + -- ============================================================================ --changeset test-customer-rbac-RESTRICTED-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacRestrictedView('test_customer', 'target.prefix', +call generateRbacRestrictedView('test_customer', + 'reference', $updates$ reference = new.reference, prefix = new.prefix, @@ -99,47 +105,3 @@ call generateRbacRestrictedView('test_customer', 'target.prefix', --// --- ============================================================================ ---changeset test-customer-rbac-ADD-CUSTOMER:1 endDelimiter:--// --- ---------------------------------------------------------------------------- -/* - Creates a global permission for add-customer and assigns it to the hostsharing admins role. - */ -do language plpgsql $$ - declare - addCustomerPermissions uuid[]; - globalObjectUuid uuid; - globalAdminRoleUuid uuid ; - begin - call defineContext('granting global add-customer permission to global admin role', null, null, null); - - globalAdminRoleUuid := findRoleId(globalAdmin()); - globalObjectUuid := (select uuid from global); - addCustomerPermissions := createPermissions(globalObjectUuid, array ['add-customer']); - call grantPermissionsToRole(globalAdminRoleUuid, addCustomerPermissions); - end; -$$; - -/** - Used by the trigger to prevent the add-customer to current user respectively assumed roles. - */ -create or replace function addTestCustomerNotAllowedForCurrentSubjects() - returns trigger - language PLPGSQL -as $$ -begin - raise exception '[403] add-customer not permitted for %', - array_to_string(currentSubjects(), ';', 'null'); -end; $$; - -/** - Checks if the user or assumed roles are allowed to add a new customer. - */ -create trigger test_customer_insert_trigger - before insert - on test_customer - for each row - when ( not hasGlobalPermission('add-customer') ) -execute procedure addTestCustomerNotAllowedForCurrentSubjects(); ---// - diff --git a/src/main/resources/db/changelog/118-test-customer-test-data.sql b/src/main/resources/db/changelog/118-test-customer-test-data.sql index 353b8f59..47c6e6aa 100644 --- a/src/main/resources/db/changelog/118-test-customer-test-data.sql +++ b/src/main/resources/db/changelog/118-test-customer-test-data.sql @@ -28,6 +28,8 @@ declare currentTask varchar; custRowId uuid; custAdminName varchar; + custAdminUuid uuid; + newCust test_customer; begin currentTask = 'creating RBAC test customer #' || custReference || '/' || custPrefix; call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global.admin'); @@ -35,10 +37,20 @@ begin custRowId = uuid_generate_v4(); custAdminName = 'customer-admin@' || custPrefix || '.example.com'; + custAdminUuid = createRbacUser(custAdminName); insert into test_customer (reference, prefix, adminUserName) values (custReference, custPrefix, custAdminName); + + select * into newCust + from test_customer where reference=custReference; +-- call grantRoleToUser( +-- getRoleId(testCustomerAdmin(newCust), 'fail'), +-- findRoleId(testCustomerOwner(newCust)), +-- custAd +-- minUuid, +-- true); end; $$; --// diff --git a/src/main/resources/db/changelog/123-test-package-rbac.sql b/src/main/resources/db/changelog/123-test-package-rbac.sql index 9e68468c..fadb2562 100644 --- a/src/main/resources/db/changelog/123-test-package-rbac.sql +++ b/src/main/resources/db/changelog/123-test-package-rbac.sql @@ -1,4 +1,6 @@ --liquibase formatted sql +-- This code generated was by RbacViewPostgresGenerator at 2024-03-06T15:40:13.277446553. + -- ============================================================================ --changeset test-package-rbac-OBJECT:1 endDelimiter:--// @@ -7,6 +9,7 @@ call generateRelatedRbacObject('test_package'); --// + -- ============================================================================ --changeset test-package-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--// -- ---------------------------------------------------------------------------- @@ -15,95 +18,213 @@ call generateRbacRoleDescriptors('testPackage', 'test_package'); -- ============================================================================ ---changeset test-package-rbac-ROLES-CREATION:1 endDelimiter:--// +--changeset test-package-rbac-insert-trigger:1 endDelimiter:--// -- ---------------------------------------------------------------------------- + /* - Creates the roles and their assignments for a new package for the AFTER INSERT TRIGGER. + A Creates the roles, grants and permission for the AFTER INSERT TRIGGER. */ -create or replace function createRbacRolesForTestPackage() - returns trigger - language plpgsql - strict as $$ + +create or replace procedure buildRbacSystemForTestPackage( + NEW test_package +) + language plpgsql as $$ + declare - parentCustomer test_customer; + newCustomer test_customer; + begin - if TG_OP <> 'INSERT' then - raise exception 'invalid usage of TRIGGER AFTER INSERT'; - end if; - call enterTriggerForObjectUuid(NEW.uuid); + SELECT * FROM test_customer c + WHERE c.uuid= NEW.customerUuid + into newCustomer; - select * from test_customer as c where c.uuid = NEW.customerUuid into parentCustomer; - - -- an owner role is created and assigned to the customer's admin role perform createRoleWithGrants( - testPackageOwner(NEW), - permissions => array ['*'], - incomingSuperRoles => array[testCustomerAdmin(parentCustomer)] - ); + testPackageOwner(NEW), + permissions => array['DELETE', 'UPDATE'], + userUuids => array[currentUserUuid()], + incomingSuperRoles => array[testCustomerAdmin(newCustomer)] + ); - -- an owner role is created and assigned to the package owner role perform createRoleWithGrants( - testPackageAdmin(NEW), - permissions => array ['add-domain'], + testPackageAdmin(NEW), incomingSuperRoles => array[testPackageOwner(NEW)] - ); + ); - -- and a package tenant role is created and assigned to the package admin as well perform createRoleWithGrants( - testPackageTenant(NEW), - permissions => array['view'], - incomingsuperroles => array[testPackageAdmin(NEW)], - outgoingSubRoles => array[testCustomerTenant(parentCustomer)] - ); + testPackageTenant(NEW), + permissions => array['SELECT'], + incomingSuperRoles => array[testPackageAdmin(NEW)], + outgoingSubRoles => array[testCustomerTenant(newCustomer)] + ); call leaveTriggerForObjectUuid(NEW.uuid); - return NEW; end; $$; /* - An AFTER INSERT TRIGGER which creates the role structure for a new package. + AFTER INSERT TRIGGER to create the role+grant structure for a new test_package row. */ -create trigger createRbacRolesForTestPackage_Trigger - after insert - on test_package +create or replace function insertTriggerForTestPackage_tf() + returns trigger + language plpgsql + strict as $$ +begin + call buildRbacSystemForTestPackage(NEW); + return NEW; +end; $$; + +create trigger insertTriggerForTestPackage_tg + after insert on test_package for each row -execute procedure createRbacRolesForTestPackage(); +execute procedure insertTriggerForTestPackage_tf(); + --// +-- ============================================================================ +--changeset test-package-rbac-update-trigger:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +/* + Called from the AFTER UPDATE TRIGGER to re-wire the grants. + */ + +create or replace procedure updateRbacRulesForTestPackage( + OLD test_package, + NEW test_package +) + language plpgsql as $$ + +declare + oldCustomer test_customer; + newCustomer test_customer; + +begin + call enterTriggerForObjectUuid(NEW.uuid); + + SELECT * FROM test_customer c + WHERE c.uuid= OLD.customerUuid + into oldCustomer; + SELECT * FROM test_customer c + WHERE c.uuid= NEW.customerUuid + into newCustomer; + + if NEW.customerUuid <> OLD.customerUuid then + + call revokePermissionFromRole(findPermissionId(OLD.uuid, 'INSERT'), testCustomerAdmin(oldCustomer)); + + call revokeRoleFromRole(testPackageOwner(OLD), testCustomerAdmin(oldCustomer)); + call grantRoleToRole(testPackageOwner(NEW), testCustomerAdmin(newCustomer)); + + call revokeRoleFromRole(testCustomerTenant(oldCustomer), testPackageTenant(OLD)); + call grantRoleToRole(testCustomerTenant(newCustomer), testPackageTenant(NEW)); + + end if; + + call leaveTriggerForObjectUuid(NEW.uuid); +end; $$; + +/* + AFTER INSERT TRIGGER to re-wire the grant structure for a new test_package row. + */ + +create or replace function updateTriggerForTestPackage_tf() + returns trigger + language plpgsql + strict as $$ +begin + call updateRbacRulesForTestPackage(OLD, NEW); + return NEW; +end; $$; + +create trigger updateTriggerForTestPackage_tg + after update on test_package + for each row +execute procedure updateTriggerForTestPackage_tf(); + +--// + +-- ============================================================================ +--changeset test-package-rbac-INSERT:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +/* + Creates INSERT INTO test_package permissions for the related test_customer rows. + */ +do language plpgsql $$ + declare + row test_customer; + permissionUuid uuid; + roleUuid uuid; + begin + call defineContext('generated Liquibase: create INSERT INTO test_package permissions for the related test_customer rows'); + + FOR row IN SELECT * FROM test_customer + LOOP + roleUuid := findRoleId(testCustomerAdmin(row)); + permissionUuid := createPermission(row.uuid, 'INSERT', 'test_package'); + call grantPermissionToRole(roleUuid, permissionUuid); + END LOOP; + END; +$$; + +/** + Adds test_package INSERT permission to specified role of new test_customer rows. +*/ +create or replace function test_package_test_customer_insert_tf() + returns trigger + language plpgsql + strict as $$ +begin + call grantPermissionToRole( + testCustomerAdmin(NEW), + createPermission(NEW.uuid, 'INSERT', 'test_package')); + return NEW; +end; $$; + +create trigger test_package_test_customer_insert_tg + after insert on test_customer + for each row +execute procedure test_package_test_customer_insert_tf(); + +/** + Checks if the user or assumed roles are allowed to insert a row to test_package. +*/ +create or replace function test_package_insert_permission_missing_tf() + returns trigger + language plpgsql as $$ +begin + raise exception 'insert into test_package not allowed for current subjects %', currentSubjectsUuids(); +end; $$; + +create trigger test_package_insert_permission_check_tg + before insert on test_package + for each row + when ( not hasInsertPermission(NEW.customerUuid, 'INSERT', 'test_package') ) + execute procedure test_package_insert_permission_missing_tf(); + +--// -- ============================================================================ --changeset test-package-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityView('test_package', 'target.name'); +call generateRbacIdentityView('test_package', $idName$ + name + $idName$); --// + -- ============================================================================ --changeset test-package-rbac-RESTRICTED-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- - -/* - Creates a view to the customer main table which maps the identifying name - (in this case, the prefix) to the objectUuid. - */ --- drop view if exists test_package_rv; --- create or replace view test_package_rv as --- select target.* --- from test_package as target --- where target.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('view', 'test_package', currentSubjectsUuids())) --- order by target.name; --- grant all privileges on test_package_rv to ${HSADMINNG_POSTGRES_RESTRICTED_USERNAME}; - -call generateRbacRestrictedView('test_package', 'target.name', +call generateRbacRestrictedView('test_package', + 'name', $updates$ version = new.version, customerUuid = new.customerUuid, - name = new.name, description = new.description $updates$); - --// diff --git a/src/main/resources/db/changelog/128-test-package-test-data.sql b/src/main/resources/db/changelog/128-test-package-test-data.sql index 4667b742..8c6568f3 100644 --- a/src/main/resources/db/changelog/128-test-package-test-data.sql +++ b/src/main/resources/db/changelog/128-test-package-test-data.sql @@ -26,7 +26,7 @@ begin custAdminUser = 'customer-admin@' || cust.prefix || '.example.com'; custAdminRole = 'test_customer#' || cust.prefix || '.admin'; - call defineContext(currentTask, null, custAdminUser, custAdminRole); + call defineContext(currentTask, null, 'superuser-fran@hostsharing.net', custAdminRole); raise notice 'task: % by % as %', currentTask, custAdminUser, custAdminRole; insert diff --git a/src/main/resources/db/changelog/133-test-domain-rbac.sql b/src/main/resources/db/changelog/133-test-domain-rbac.sql index a78bfb5f..e890d267 100644 --- a/src/main/resources/db/changelog/133-test-domain-rbac.sql +++ b/src/main/resources/db/changelog/133-test-domain-rbac.sql @@ -28,7 +28,7 @@ begin return createRoleWithGrants( domainTenantRoleDesc, - permissions => array['view'], + permissions => array['SELECT'], incomingSuperRoles => array[testdomainAdmin(domain)] ); end; $$; @@ -60,14 +60,14 @@ begin -- an owner role is created and assigned to the package's admin group perform createRoleWithGrants( testDomainOwner(NEW), - permissions => array['*'], + permissions => array['DELETE'], incomingSuperRoles => array[testPackageAdmin(parentPackage)] ); -- and a domain admin role is created and assigned to the domain owner as well perform createRoleWithGrants( testDomainAdmin(NEW), - permissions => array['edit'], + permissions => array['UPDATE'], incomingSuperRoles => array[testDomainOwner(NEW)], outgoingSubRoles => array[testPackageTenant(parentPackage)] ); @@ -112,6 +112,6 @@ drop view if exists test_domain_rv; create or replace view test_domain_rv as select target.* from test_domain as target - where target.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('view', 'domain', currentSubjectsUuids())); + where target.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('SELECT', 'domain', currentSubjectsUuids())); grant all privileges on test_domain_rv to ${HSADMINNG_POSTGRES_RESTRICTED_USERNAME}; --// diff --git a/src/main/resources/db/changelog/203-hs-office-contact-rbac.sql b/src/main/resources/db/changelog/203-hs-office-contact-rbac.sql index 7ba7891b..dc51efa3 100644 --- a/src/main/resources/db/changelog/203-hs-office-contact-rbac.sql +++ b/src/main/resources/db/changelog/203-hs-office-contact-rbac.sql @@ -33,7 +33,7 @@ begin perform createRoleWithGrants( hsOfficeContactOwner(NEW), - permissions => array['*'], + permissions => array['DELETE'], incomingSuperRoles => array[globalAdmin()], userUuids => array[currentUserUuid()], grantedByRole => globalAdmin() @@ -41,7 +41,7 @@ begin perform createRoleWithGrants( hsOfficeContactAdmin(NEW), - permissions => array['edit'], + permissions => array['UPDATE'], incomingSuperRoles => array[hsOfficeContactOwner(NEW)] ); @@ -52,7 +52,7 @@ begin perform createRoleWithGrants( hsOfficeContactGuest(NEW), - permissions => array['view'], + permissions => array['SELECT'], incomingSuperRoles => array[hsOfficeContactTenant(NEW)] ); diff --git a/src/main/resources/db/changelog/213-hs-office-person-rbac.sql b/src/main/resources/db/changelog/213-hs-office-person-rbac.sql index 42eacf2f..c903e086 100644 --- a/src/main/resources/db/changelog/213-hs-office-person-rbac.sql +++ b/src/main/resources/db/changelog/213-hs-office-person-rbac.sql @@ -31,16 +31,16 @@ begin perform createRoleWithGrants( hsOfficePersonOwner(NEW), - permissions => array['*'], + permissions => array['DELETE'], incomingSuperRoles => array[globalAdmin()], userUuids => array[currentUserUuid()], grantedByRole => globalAdmin() ); - -- TODO: who is admin? the person itself? is it allowed for the person itself or a representative to edit the data? + -- TODO: who is admin? the person itself? is it allowed for the person itself or a representative to update the data? perform createRoleWithGrants( hsOfficePersonAdmin(NEW), - permissions => array['edit'], + permissions => array['UPDATE'], incomingSuperRoles => array[hsOfficePersonOwner(NEW)] ); @@ -51,7 +51,7 @@ begin perform createRoleWithGrants( hsOfficePersonGuest(NEW), - permissions => array['view'], + permissions => array['SELECT'], incomingSuperRoles => array[hsOfficePersonTenant(NEW)] ); diff --git a/src/main/resources/db/changelog/223-hs-office-relationship-rbac.sql b/src/main/resources/db/changelog/223-hs-office-relationship-rbac.sql index 928af48c..34d23793 100644 --- a/src/main/resources/db/changelog/223-hs-office-relationship-rbac.sql +++ b/src/main/resources/db/changelog/223-hs-office-relationship-rbac.sql @@ -45,7 +45,7 @@ begin perform createRoleWithGrants( hsOfficeRelationshipOwner(NEW), - permissions => array['*'], + permissions => array['DELETE'], incomingSuperRoles => array[ globalAdmin(), hsOfficePersonAdmin(newRelAnchor)] @@ -53,14 +53,14 @@ begin perform createRoleWithGrants( hsOfficeRelationshipAdmin(NEW), - permissions => array['edit'], + permissions => array['UPDATE'], incomingSuperRoles => array[hsOfficeRelationshipOwner(NEW)] ); -- the tenant role for those related users who can view the data perform createRoleWithGrants( hsOfficeRelationshipTenant, - permissions => array['view'], + permissions => array['SELECT'], incomingSuperRoles => array[ hsOfficeRelationshipAdmin(NEW), hsOfficePersonAdmin(newRelAnchor), diff --git a/src/main/resources/db/changelog/233-hs-office-partner-rbac.sql b/src/main/resources/db/changelog/233-hs-office-partner-rbac.sql index 4b4da009..a6ad3733 100644 --- a/src/main/resources/db/changelog/233-hs-office-partner-rbac.sql +++ b/src/main/resources/db/changelog/233-hs-office-partner-rbac.sql @@ -48,13 +48,13 @@ begin perform createRoleWithGrants( hsOfficePartnerOwner(NEW), - permissions => array['*'], + permissions => array['DELETE'], incomingSuperRoles => array[globalAdmin()] ); perform createRoleWithGrants( hsOfficePartnerAdmin(NEW), - permissions => array['edit'], + permissions => array['UPDATE'], incomingSuperRoles => array[ hsOfficePartnerOwner(NEW)], outgoingSubRoles => array[ @@ -84,7 +84,7 @@ begin perform createRoleWithGrants( hsOfficePartnerGuest(NEW), - permissions => array['view'], + permissions => array['SELECT'], incomingSuperRoles => array[hsOfficePartnerTenant(NEW)] ); @@ -99,12 +99,12 @@ begin call grantPermissionsToRole( getRoleId(hsOfficePartnerOwner(NEW), 'fail'), - createPermissions(NEW.detailsUuid, array ['*']) + createPermissions(NEW.detailsUuid, array ['DELETE']) ); call grantPermissionsToRole( getRoleId(hsOfficePartnerAdmin(NEW), 'fail'), - createPermissions(NEW.detailsUuid, array ['edit']) + createPermissions(NEW.detailsUuid, array ['UPDATE']) ); call grantPermissionsToRole( @@ -112,7 +112,7 @@ begin -- Do NOT grant view permission on partner-details to hsOfficePartnerTENANT! -- Otherwise package-admins etc. would be able to read the data. getRoleId(hsOfficePartnerAgent(NEW), 'fail'), - createPermissions(NEW.detailsUuid, array ['view']) + createPermissions(NEW.detailsUuid, array ['SELECT']) ); diff --git a/src/main/resources/db/changelog/234-hs-office-partner-details-rbac.sql b/src/main/resources/db/changelog/234-hs-office-partner-details-rbac.sql index ab94481e..7cd72003 100644 --- a/src/main/resources/db/changelog/234-hs-office-partner-details-rbac.sql +++ b/src/main/resources/db/changelog/234-hs-office-partner-details-rbac.sql @@ -7,9 +7,6 @@ call generateRelatedRbacObject('hs_office_partner_details'); --// - - - -- ============================================================================ --changeset hs-office-partner-details-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- @@ -38,7 +35,7 @@ call generateRbacRestrictedView('hs_office_partner_details', -- ============================================================================ ---changeset hs-office-partner-details-rbac-NEW-CONTACT:1 endDelimiter:--// +--changeset hs-office-partner-details-rbac-NEW-PARTNER-DETAILS:1 endDelimiter:--// -- ---------------------------------------------------------------------------- /* Creates a global permission for new-partner-details and assigns it to the hostsharing admins role. diff --git a/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.sql b/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.sql index 148e0ee2..5b1ae81f 100644 --- a/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.sql +++ b/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.sql @@ -33,7 +33,7 @@ begin perform createRoleWithGrants( hsOfficeBankAccountOwner(NEW), - permissions => array['delete'], + permissions => array['DELETE'], incomingSuperRoles => array[globalAdmin()], userUuids => array[currentUserUuid()], grantedByRole => globalAdmin() @@ -51,7 +51,7 @@ begin perform createRoleWithGrants( hsOfficeBankAccountGuest(NEW), - permissions => array['view'], + permissions => array['SELECT'], incomingSuperRoles => array[hsOfficeBankAccountTenant(NEW)] ); diff --git a/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.sql b/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.sql index 02895c48..44815f32 100644 --- a/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.sql +++ b/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.sql @@ -41,13 +41,13 @@ begin perform createRoleWithGrants( hsOfficeSepaMandateOwner(NEW), - permissions => array['*'], + permissions => array['DELETE'], incomingSuperRoles => array[globalAdmin()] ); perform createRoleWithGrants( hsOfficeSepaMandateAdmin(NEW), - permissions => array['edit'], + permissions => array['UPDATE'], incomingSuperRoles => array[hsOfficeSepaMandateOwner(NEW)], outgoingSubRoles => array[hsOfficeBankAccountTenant(newHsOfficeBankAccount)] ); @@ -66,7 +66,7 @@ begin perform createRoleWithGrants( hsOfficeSepaMandateGuest(NEW), - permissions => array['view'], + permissions => array['SELECT'], incomingSuperRoles => array[hsOfficeSepaMandateTenant(NEW)] ); diff --git a/src/main/resources/db/changelog/273-hs-office-debitor-rbac.sql b/src/main/resources/db/changelog/273-hs-office-debitor-rbac.sql index 30573125..48109078 100644 --- a/src/main/resources/db/changelog/273-hs-office-debitor-rbac.sql +++ b/src/main/resources/db/changelog/273-hs-office-debitor-rbac.sql @@ -49,7 +49,7 @@ begin perform createRoleWithGrants( hsOfficeDebitorOwner(NEW), - permissions => array['*'], + permissions => array['DELETE'], incomingSuperRoles => array[globalAdmin()], userUuids => array[currentUserUuid()], grantedByRole => globalAdmin() @@ -57,7 +57,7 @@ begin perform createRoleWithGrants( hsOfficeDebitorAdmin(NEW), - permissions => array['edit'], + permissions => array['UPDATE'], incomingSuperRoles => array[hsOfficeDebitorOwner(NEW)] ); @@ -85,7 +85,7 @@ begin perform createRoleWithGrants( hsOfficeDebitorGuest(NEW), - permissions => array['view'], + permissions => array['SELECT'], incomingSuperRoles => array[ hsOfficeDebitorTenant(NEW)] ); diff --git a/src/main/resources/db/changelog/303-hs-office-membership-rbac.sql b/src/main/resources/db/changelog/303-hs-office-membership-rbac.sql index 949f939c..10125d69 100644 --- a/src/main/resources/db/changelog/303-hs-office-membership-rbac.sql +++ b/src/main/resources/db/changelog/303-hs-office-membership-rbac.sql @@ -41,13 +41,13 @@ begin perform createRoleWithGrants( hsOfficeMembershipOwner(NEW), - permissions => array['*'], + permissions => array['DELETE'], incomingSuperRoles => array[globalAdmin()] ); perform createRoleWithGrants( hsOfficeMembershipAdmin(NEW), - permissions => array['edit'], + permissions => array['UPDATE'], incomingSuperRoles => array[hsOfficeMembershipOwner(NEW)] ); @@ -65,7 +65,7 @@ begin perform createRoleWithGrants( hsOfficeMembershipGuest(NEW), - permissions => array['view'], + permissions => array['SELECT'], incomingSuperRoles => array[hsOfficeMembershipTenant(NEW), hsOfficePartnerTenant(newHsOfficePartner), hsOfficeDebitorTenant(newHsOfficeDebitor)] ); diff --git a/src/main/resources/db/changelog/313-hs-office-coopshares-rbac.sql b/src/main/resources/db/changelog/313-hs-office-coopshares-rbac.sql index dd465d9f..a79d354e 100644 --- a/src/main/resources/db/changelog/313-hs-office-coopshares-rbac.sql +++ b/src/main/resources/db/changelog/313-hs-office-coopshares-rbac.sql @@ -43,7 +43,7 @@ begin -- coopsharestransactions cannot be edited nor deleted, just created+viewed call grantPermissionsToRole( getRoleId(hsOfficeMembershipTenant(newHsOfficeMembership), 'fail'), - createPermissions(NEW.uuid, array ['view']) + createPermissions(NEW.uuid, array ['SELECT']) ); else diff --git a/src/main/resources/db/changelog/323-hs-office-coopassets-rbac.sql b/src/main/resources/db/changelog/323-hs-office-coopassets-rbac.sql index ac65c141..38fec4ff 100644 --- a/src/main/resources/db/changelog/323-hs-office-coopassets-rbac.sql +++ b/src/main/resources/db/changelog/323-hs-office-coopassets-rbac.sql @@ -43,7 +43,7 @@ begin -- coopassetstransactions cannot be edited nor deleted, just created+viewed call grantPermissionsToRole( getRoleId(hsOfficeMembershipTenant(newHsOfficeMembership), 'fail'), - createPermissions(NEW.uuid, array ['view']) + createPermissions(NEW.uuid, array ['SELECT']) ); else diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RawRbacObjectEntity.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RawRbacObjectEntity.java new file mode 100644 index 00000000..d4256e56 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RawRbacObjectEntity.java @@ -0,0 +1,31 @@ +package net.hostsharing.hsadminng.rbac.rbacrole; + +import lombok.*; +import org.jetbrains.annotations.NotNull; +import org.springframework.data.annotation.Immutable; + +import jakarta.persistence.*; +import java.util.List; +import java.util.UUID; + +@Entity +@Table(name = "rbacobject") // TODO: create view rbacobject_ev +@Getter +@Setter +@ToString +@Immutable +@NoArgsConstructor +@AllArgsConstructor +public class RawRbacObjectEntity { + + @Id + private UUID uuid; + + @Column(name="objecttable") + private String objectTable; + + @NotNull + public static List objectDisplaysOf(@NotNull final List roles) { + return roles.stream().map(e -> e.objectTable+ "#" + e.uuid).sorted().toList(); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RawRbacObjectRepository.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RawRbacObjectRepository.java new file mode 100644 index 00000000..ab645316 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RawRbacObjectRepository.java @@ -0,0 +1,11 @@ +package net.hostsharing.hsadminng.rbac.rbacrole; + +import org.springframework.data.repository.Repository; + +import java.util.List; +import java.util.UUID; + +public interface RawRbacObjectRepository extends Repository { + + List findAll(); +} diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerAcceptanceTest.java index b13bcb76..aca26fe4 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerAcceptanceTest.java @@ -288,7 +288,7 @@ class RbacUserControllerAcceptanceTest { .body("", hasItem( allOf( hasEntry("roleName", "test_customer#yyy.tenant"), - hasEntry("op", "view")) + hasEntry("op", "select")) )) .body("", hasItem( allOf( @@ -298,7 +298,7 @@ class RbacUserControllerAcceptanceTest { .body("", hasItem( allOf( hasEntry("roleName", "test_domain#yyy00-aaaa.owner"), - hasEntry("op", "*")) + hasEntry("op", "delete")) )) .body("size()", is(7)); // @formatter:on @@ -323,7 +323,7 @@ class RbacUserControllerAcceptanceTest { .body("", hasItem( allOf( hasEntry("roleName", "test_customer#yyy.tenant"), - hasEntry("op", "view")) + hasEntry("op", "select")) )) .body("", hasItem( allOf( @@ -333,7 +333,7 @@ class RbacUserControllerAcceptanceTest { .body("", hasItem( allOf( hasEntry("roleName", "test_domain#yyy00-aaaa.owner"), - hasEntry("op", "*")) + hasEntry("op", "delete")) )) .body("size()", is(7)); // @formatter:on @@ -357,7 +357,7 @@ class RbacUserControllerAcceptanceTest { .body("", hasItem( allOf( hasEntry("roleName", "test_customer#yyy.tenant"), - hasEntry("op", "view")) + hasEntry("op", "select")) )) .body("", hasItem( allOf( @@ -367,7 +367,7 @@ class RbacUserControllerAcceptanceTest { .body("", hasItem( allOf( hasEntry("roleName", "test_domain#yyy00-aaaa.owner"), - hasEntry("op", "*")) + hasEntry("op", "delete")) )) .body("size()", is(7)); // @formatter:on 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 ab5d76a5..abec1250 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityTest.java @@ -29,8 +29,9 @@ class TestCustomerEntityTest { subgraph customer:permissions[ ] style customer:permissions fill:#dd4901,stroke:white - perm:customer:*{{customer:*}} - perm:customer:view{{customer:view}} + perm:customer:delete{{customer:delete}} + perm:customer:update{{customer:update}} + perm:customer:select{{customer:select}} end end @@ -43,9 +44,9 @@ class TestCustomerEntityTest { role:customer:admin ==> role:customer:tenant %% granting permissions to roles - role:customer:owner ==> perm:customer:* + role:customer:owner ==> perm:customer:delete role:customer:admin ==> perm:customer:add-package - role:customer:tenant ==> perm:customer:view + role:customer:tenant ==> perm:customer:select """); } } diff --git a/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityTest.java b/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityTest.java index 1bb88ffb..3cda6d74 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityTest.java @@ -30,9 +30,9 @@ class TestPackageEntityTest { 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}} + perm:package:delete{{package:delete}} + perm:package:update{{package:update}} + perm:package:select{{package:select}} end end @@ -63,9 +63,9 @@ class TestPackageEntityTest { %% 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 + role:package:owner ==> perm:package:delete + role:package:owner ==> perm:package:update + role:package:tenant ==> perm:package:select """); } } diff --git a/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageRepositoryIntegrationTest.java index 53d28e0c..89c6f993 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageRepositoryIntegrationTest.java @@ -89,7 +89,7 @@ class TestPackageRepositoryIntegrationTest { class OptimisticLocking { @Test - public void supportsOptimisticLocking() throws InterruptedException { + public void supportsOptimisticLocking() { // given globalAdminWithAssumedRole("test_package#xxx00.admin"); final var pac = testPackageRepository.findAllByOptionalNameLike("%").get(0); -- 2.39.5 From 0a9fd9f83bf358bbd28e9d1618a3aa918d478a9f Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 6 Mar 2024 15:45:39 +0100 Subject: [PATCH 31/53] add RbacGrantsDiagramService --- .../rbac/rbacgrant/RawRbacGrantEntity.java | 9 +- .../rbacgrant/RawRbacGrantRepository.java | 4 + .../rbacgrant/RbacGrantsDiagramService.java | 204 ++++++++++++++++++ ...acGrantsDiagramServiceIntegrationTest.java | 136 ++++++++++++ 4 files changed, 351 insertions(+), 2 deletions(-) rename src/{test => main}/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantEntity.java (89%) rename src/{test => main}/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantRepository.java (67%) create mode 100644 src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java create mode 100644 src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramServiceIntegrationTest.java diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantEntity.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantEntity.java similarity index 89% rename from src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantEntity.java rename to src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantEntity.java index 6dc8d1ce..f7b3cdf4 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantEntity.java @@ -1,13 +1,13 @@ package net.hostsharing.hsadminng.rbac.rbacgrant; import lombok.*; -import org.jetbrains.annotations.NotNull; import org.springframework.data.annotation.Immutable; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; import java.util.List; import java.util.UUID; @@ -20,7 +20,7 @@ import java.util.UUID; @Immutable @NoArgsConstructor @AllArgsConstructor -public class RawRbacGrantEntity { +public class RawRbacGrantEntity implements Comparable { @Id private UUID uuid; @@ -64,4 +64,9 @@ public class RawRbacGrantEntity { // TODO: remove .distinct() once partner.person + partner.contact are removed return roles.stream().map(RawRbacGrantEntity::toDisplay).sorted().distinct().toList(); } + + @Override + public int compareTo(final Object o) { + return uuid.compareTo(((RawRbacGrantEntity)o).uuid); + } } diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantRepository.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantRepository.java similarity index 67% rename from src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantRepository.java rename to src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantRepository.java index c7ac60ab..37828bdf 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantRepository.java @@ -8,4 +8,8 @@ import java.util.UUID; public interface RawRbacGrantRepository extends Repository { List findAll(); + + List findByAscendingUuid(UUID ascendingUuid); + + List findByDescendantUuid(UUID refUuid); } diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java new file mode 100644 index 00000000..68189137 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java @@ -0,0 +1,204 @@ +package net.hostsharing.hsadminng.rbac.rbacgrant; + +import net.hostsharing.hsadminng.context.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.validation.constraints.NotNull; +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; +import java.util.*; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.joining; +import static net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService.Include.*; + +// TODO: cleanup - this code was 'hacked' to quickly fix a specific problem, needs refactoring +@Service +public class RbacGrantsDiagramService { + + public static void writeToFile(final String title, final String graph, final String fileName) { + + try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))) { + writer.write(""" + ### all grants to %s + + ```mermaid + %s + ``` + """.formatted(title, graph)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public enum Include { + DETAILS, + USERS, + PERMISSIONS, + NOT_ASSUMED, + TEST_ENTITIES, + NON_TEST_ENTITIES + } + + @Autowired + private Context context; + + @Autowired + private RawRbacGrantRepository rawGrantRepo; + + @PersistenceContext + private EntityManager em; + + public String allGrantsToCurrentUser(final EnumSet includes) { + final var graph = new HashSet(); + for ( UUID subjectUuid: context.currentSubjectsUuids() ) { + traverseGrantsTo(graph, subjectUuid, includes); + } + return toMermaidFlowchart(graph, includes); + } + + private void traverseGrantsTo(final Set graph, final UUID refUuid, final EnumSet includes) { + final var grants = rawGrantRepo.findByAscendingUuid(refUuid); + grants.forEach(g -> { + if (!includes.contains(PERMISSIONS) && g.getDescendantIdName().startsWith("perm ")) { + return; + } + if (!includes.contains(TEST_ENTITIES) && g.getDescendantIdName().contains(" test_")) { + return; + } + if (!includes.contains(NON_TEST_ENTITIES) && !g.getDescendantIdName().contains(" test_")) { + return; + } + graph.add(g); + if (includes.contains(NOT_ASSUMED) || g.isAssumed()) { + traverseGrantsTo(graph, g.getDescendantUuid(), includes); + } + }); + } + + public String allGrantsFrom(final UUID targetObject, final String op, final EnumSet includes) { + final var refUuid = (UUID) em.createNativeQuery("SELECT uuid FROM rbacpermission WHERE objectuuid=:targetObject AND op=:op") + .setParameter("targetObject", targetObject) + .setParameter("op", op) + .getSingleResult(); + final var graph = new HashSet(); + traverseGrantsFrom(graph, refUuid, includes); + return toMermaidFlowchart(graph, includes); + } + + private void traverseGrantsFrom(final Set graph, final UUID refUuid, final EnumSet option) { + final var grants = rawGrantRepo.findByDescendantUuid(refUuid); + grants.forEach(g -> { + if (!option.contains(USERS) && g.getAscendantIdName().startsWith("user ")) { + return; + } + graph.add(g); + if (option.contains(NOT_ASSUMED) || g.isAssumed()) { + traverseGrantsFrom(graph, g.getAscendingUuid(), option); + } + }); + } + + private String toMermaidFlowchart(final HashSet graph, final EnumSet includes) { + final var entities = + includes.contains(DETAILS) + ? graph.stream() + .flatMap(g -> Stream.of( + new Node(g.getAscendantIdName(), g.getAscendingUuid()), + new Node(g.getDescendantIdName(), g.getDescendantUuid())) + ) + .collect(groupingBy(RbacGrantsDiagramService::renderEntityIdName)) + .entrySet().stream() + .map(entity -> "subgraph " + quoted(entity.getKey()) + renderSubgraph(entity.getKey()) + "\n\n " + + entity.getValue().stream() + .map(n -> renderNode(n.idName(), n.uuid()).replace("\n", "\n ")) + .sorted() + .distinct() + .collect(joining("\n\n "))) + .collect(joining("\n\nend\n\n")) + + "\n\nend\n\n" + : ""; + + final var grants = graph.stream() + .map(g -> quoted(g.getAscendantIdName()) + + (g.isAssumed() ? " --> " : " -.-> ") + + quoted(g.getDescendantIdName())) + .sorted() + .collect(joining("\n")); + + final var avoidCroppedNodeLabels = "%%{init:{'flowchart':{'htmlLabels':false}}}%%\n\n"; + return (includes.contains(DETAILS) ? avoidCroppedNodeLabels : "") + + "flowchart TB\n\n" + + entities + + grants; + } + + private String renderSubgraph(final String entityId) { + // this does not work according to Mermaid bug https://github.com/mermaid-js/mermaid/issues/3806 + // if (entityId.contains("#")) { + // final var parts = entityId.split("#"); + // final var table = parts[0]; + // final var entity = parts[1]; + // if (table.equals("entity")) { + // return "[" + entity "]"; + // } + // return "[" + table + "\n" + entity + "]"; + // } + return "[" + entityId + "]"; + } + + private static String renderEntityIdName(final Node node) { + final var refType = refType(node.idName()); + if (refType.equals("user")) { + return "users"; + } + if (refType.equals("perm")) { + return node.idName().split(" ", 4)[3]; + } + if (refType.equals("role")) { + final var withoutRolePrefix = node.idName().substring("role:".length()); + return withoutRolePrefix.substring(0, withoutRolePrefix.lastIndexOf('.')); + } + throw new IllegalArgumentException("unknown refType '" + refType + "' in '" + node.idName() + "'"); + } + + private String renderNode(final String idName, final UUID uuid) { + return quoted(idName) + renderNodeContent(idName, uuid); + } + + private String renderNodeContent(final String idName, final UUID uuid) { + final var refType = refType(idName); + + if (refType.equals("user")) { + final var displayName = idName.substring(refType.length()+1); + return "(" + displayName + "\nref:" + uuid + ")"; + } + if (refType.equals("role")) { + final var roleType = idName.substring(idName.lastIndexOf('.') + 1); + return "[" + roleType + "\nref:" + uuid + "]"; + } + if (refType.equals("perm")) { + final var roleType = idName.split(" ")[1]; + return "{{" + roleType + "\nref:" + uuid + "}}"; + } + return ""; + } + + private static String refType(final String idName) { + return idName.split(" ", 2)[0]; + } + + @NotNull + private static String quoted(final String idName) { + return idName.replace(" ", ":").replaceAll("@.*", ""); + } +} + +record Node(String idName, UUID uuid) { + +} diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramServiceIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramServiceIntegrationTest.java new file mode 100644 index 00000000..442f4979 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramServiceIntegrationTest.java @@ -0,0 +1,136 @@ +package net.hostsharing.hsadminng.rbac.rbacgrant; + +import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.hsadminng.hs.office.test.ContextBasedTestWithCleanup; +import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService.Include; +import net.hostsharing.test.JpaAttempt; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.EnumSet; +import java.util.UUID; + +import static java.lang.String.join; +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@Import( { Context.class, JpaAttempt.class, RbacGrantsDiagramService.class}) +class RbacGrantsDiagramServiceIntegrationTest extends ContextBasedTestWithCleanup { + + @Autowired + RbacGrantsDiagramService grantsMermaidService; + + @MockBean + HttpServletRequest request; + + @Test + void allGrantsToCurrentUser() { + context("superuser-alex@hostsharing.net", "test_domain#xxx00-aaaa.owner"); + final var graph = grantsMermaidService.allGrantsToCurrentUser(EnumSet.of(Include.TEST_ENTITIES)); + + assertThat(graph).isEqualTo(""" + flowchart TB + + role:test_package#xxx00.tenant[ + test_package + xxx00.t + tenant] --> role:test_customer#xxx.tenant[ + test_customer + xxx.t + tenant] + role:test_domain#xxx00-aaaa.owner[ + test_domain + xxx00-aaaa.o + owner] --> role:test_domain#xxx00-aaaa.admin[ + test_domain + xxx00-aaaa.a + admin] + role:test_domain#xxx00-aaaa.admin[ + test_domain + xxx00-aaaa.a + admin] --> role:test_package#xxx00.tenant[ + test_package + xxx00.t + tenant] + """.trim()); + } + + @Test + void allGrantsToCurrentUserIncludingPermissions() { + context("superuser-alex@hostsharing.net", "test_domain#xxx00-aaaa.owner"); + final var graph = grantsMermaidService.allGrantsToCurrentUser(EnumSet.of(Include.TEST_ENTITIES, Include.PERMISSIONS)); + + assertThat(graph).isEqualTo(""" + flowchart TB + + role:test_domain#xxx00-aaaa.owner[ + test_domain + xxx00-aaaa.o + owner] --> perm:*:on:test_domain#xxx00-aaaa{{ + test_domain + xxx00-aaaa + *}} + role:test_customer#xxx.tenant[ + test_customer + xxx.t + tenant] --> perm:view:on:test_customer#xxx{{ + test_customer + xxx + view}} + role:test_domain#xxx00-aaaa.admin[ + test_domain + xxx00-aaaa.a + admin] --> perm:edit:on:test_domain#xxx00-aaaa{{ + test_domain + xxx00-aaaa + edit}} + role:test_package#xxx00.tenant[ + test_package + xxx00.t + tenant] --> role:test_customer#xxx.tenant[ + test_customer + xxx.t + tenant] + role:test_domain#xxx00-aaaa.owner[ + test_domain + xxx00-aaaa.o + owner] --> role:test_domain#xxx00-aaaa.admin[ + test_domain + xxx00-aaaa.a + admin] + role:test_package#xxx00.tenant[ + test_package + xxx00.t + tenant] --> perm:view:on:test_package#xxx00{{ + test_package + xxx00 + view}} + role:test_domain#xxx00-aaaa.admin[ + test_domain + xxx00-aaaa.a + admin] --> role:test_package#xxx00.tenant[ + test_package + xxx00.t + tenant] + """.trim()); + } + + @Test +// @Disabled + void print() throws IOException { + //context("superuser-alex@hostsharing.net", "hs_office_person#FirbySusan.admin"); + context("superuser-alex@hostsharing.net"); + + //final var graph = grantsMermaidService.allGrantsToCurrentUser(EnumSet.of(Include.NON_TEST_ENTITIES, Include.PERMISSIONS)); + + final var targetObject = (UUID) em.createNativeQuery("SELECT uuid FROM hs_office_coopassetstransaction WHERE reference='ref 1000101-1'").getSingleResult(); + final var graph = grantsMermaidService.allGrantsFrom(targetObject, "view", EnumSet.of(Include.USERS)); + + RbacGrantsDiagramService.writeToFile(join(";", context.getAssumedRoles()), graph, "doc/all-grants.md"); + } +} -- 2.39.5 From 18ce4fd8e919b20cfd0dc00f59561b89ce592de0 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 6 Mar 2024 16:04:34 +0100 Subject: [PATCH 32/53] WIP --- .../resources/db/changelog/050-rbac-base.sql | 7 +++++++ .../test/cust/TestCustomerEntityTest.java | 12 ++++++------ .../TestCustomerRepositoryIntegrationTest.java | 1 - .../test/pac/TestPackageEntityTest.java | 16 ++++++++-------- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/main/resources/db/changelog/050-rbac-base.sql b/src/main/resources/db/changelog/050-rbac-base.sql index 9a8926c6..2eeff958 100644 --- a/src/main/resources/db/changelog/050-rbac-base.sql +++ b/src/main/resources/db/changelog/050-rbac-base.sql @@ -609,6 +609,13 @@ begin end; $$; +create or replace procedure grantPermissionToRole(roleDesc RbacRoleDescriptor, permissionUuid uuid) + language plpgsql as $$ +begin + call grantPermissionToRole(findRoleId(roleDesc), permissionUuid); +end; +$$; + -- TODO: deprecated, remove and use grantPermissionToRole(...) create or replace procedure grantPermissionsToRole(roleUuid uuid, permissionIds uuid[]) language plpgsql as $$ 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 abec1250..4ff123d5 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityTest.java @@ -29,9 +29,9 @@ class TestCustomerEntityTest { subgraph customer:permissions[ ] style customer:permissions fill:#dd4901,stroke:white - perm:customer:delete{{customer:delete}} - perm:customer:update{{customer:update}} - perm:customer:select{{customer:select}} + perm:customer:DELETE{{customer:DELETE}} + perm:customer:UPDATE{{customer:UPDATE}} + perm:customer:SELECT{{customer:SELECT}} end end @@ -44,9 +44,9 @@ class TestCustomerEntityTest { role:customer:admin ==> role:customer:tenant %% granting permissions to roles - role:customer:owner ==> perm:customer:delete - role:customer:admin ==> perm:customer:add-package - role:customer:tenant ==> perm:customer:select + role:customer:owner ==> perm:customer:DELETE + role:customer:admin ==> perm:customer:UPDATE + role:customer:tenant ==> perm:customer:SELECT """); } } diff --git a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java index ca535142..018adc72 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java @@ -43,7 +43,6 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest { final var count = testCustomerRepository.count(); // when - final var result = attempt(em, () -> { final var newCustomer = new TestCustomerEntity( UUID.randomUUID(), "www", 90001, "customer-admin@www.example.com"); diff --git a/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityTest.java b/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityTest.java index 3cda6d74..534da710 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityTest.java @@ -29,10 +29,10 @@ class TestPackageEntityTest { subgraph package:permissions[ ] style package:permissions fill:#dd4901,stroke:white - perm:package:insert{{package:insert}} - perm:package:delete{{package:delete}} - perm:package:update{{package:update}} - perm:package:select{{package:select}} + perm:package:INSERT{{package:INSERT}} + perm:package:DELETE{{package:DELETE}} + perm:package:UPDATE{{package:UPDATE}} + perm:package:SELECT{{package:SELECT}} end end @@ -62,10 +62,10 @@ class TestPackageEntityTest { role:package:tenant ==> role:customer:tenant %% granting permissions to roles - role:customer:admin ==> perm:package:insert - role:package:owner ==> perm:package:delete - role:package:owner ==> perm:package:update - role:package:tenant ==> perm:package:select + role:customer:admin ==> perm:package:INSERT + role:package:owner ==> perm:package:DELETE + role:package:owner ==> perm:package:UPDATE + role:package:tenant ==> perm:package:SELECT """); } } -- 2.39.5 From 4e2b17a216ec2f3152d068ec8db232f44b0646c5 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 7 Mar 2024 08:27:00 +0100 Subject: [PATCH 33/53] integrate RbacGrantsDiagramService in ContextBasedTest and TestCustomerRepositoryIntegrationTest --- .../rbac/rbacgrant/RbacGrantsDiagramService.java | 12 +++++++----- .../db/changelog/118-test-customer-test-data.sql | 11 +++++------ .../hsadminng/context/ContextBasedTest.java | 6 ++++++ .../cust/TestCustomerRepositoryIntegrationTest.java | 11 +++++++++++ 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java index 68189137..57f86ded 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java @@ -68,11 +68,13 @@ public class RbacGrantsDiagramService { if (!includes.contains(PERMISSIONS) && g.getDescendantIdName().startsWith("perm ")) { return; } - if (!includes.contains(TEST_ENTITIES) && g.getDescendantIdName().contains(" test_")) { - return; - } - if (!includes.contains(NON_TEST_ENTITIES) && !g.getDescendantIdName().contains(" test_")) { - return; + if ( !g.getDescendantIdName().startsWith("role global")) { + if (!includes.contains(TEST_ENTITIES) && g.getDescendantIdName().contains(" test_")) { + return; + } + if (!includes.contains(NON_TEST_ENTITIES) && !g.getDescendantIdName().contains(" test_")) { + return; + } } graph.add(g); if (includes.contains(NOT_ASSUMED) || g.isAssumed()) { diff --git a/src/main/resources/db/changelog/118-test-customer-test-data.sql b/src/main/resources/db/changelog/118-test-customer-test-data.sql index 47c6e6aa..643022fe 100644 --- a/src/main/resources/db/changelog/118-test-customer-test-data.sql +++ b/src/main/resources/db/changelog/118-test-customer-test-data.sql @@ -45,12 +45,11 @@ begin select * into newCust from test_customer where reference=custReference; --- call grantRoleToUser( --- getRoleId(testCustomerAdmin(newCust), 'fail'), --- findRoleId(testCustomerOwner(newCust)), --- custAd --- minUuid, --- true); + call grantRoleToUser( + getRoleId(testCustomerAdmin(newCust), 'fail'), + findRoleId(testCustomerOwner(newCust)), + custAdminUuid, + true); end; $$; --// diff --git a/src/test/java/net/hostsharing/hsadminng/context/ContextBasedTest.java b/src/test/java/net/hostsharing/hsadminng/context/ContextBasedTest.java index 1069fa5f..a1f4cfbc 100644 --- a/src/test/java/net/hostsharing/hsadminng/context/ContextBasedTest.java +++ b/src/test/java/net/hostsharing/hsadminng/context/ContextBasedTest.java @@ -1,14 +1,20 @@ package net.hostsharing.hsadminng.context; +import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestInfo; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +@Import(RbacGrantsDiagramService.class) public abstract class ContextBasedTest { @Autowired protected Context context; + @Autowired + protected RbacGrantsDiagramService diagramService; + TestInfo test; @BeforeEach diff --git a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java index 018adc72..55c958d5 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java @@ -2,6 +2,8 @@ package net.hostsharing.hsadminng.test.cust; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.ContextBasedTest; +import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService; +import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService.Include; import net.hostsharing.test.JpaAttempt; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -14,9 +16,11 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceException; import jakarta.servlet.http.HttpServletRequest; +import java.util.EnumSet; import java.util.List; import java.util.UUID; +import static java.util.EnumSet.of; import static net.hostsharing.test.JpaAttempt.attempt; import static org.assertj.core.api.Assertions.assertThat; @@ -140,6 +144,13 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest { @Test public void customerAdmin_withAssumedOwnedPackageAdminRole_canViewOnlyItsOwnCustomer() { + context("customer-admin@xxx.example.com"); + RbacGrantsDiagramService.writeToFile( + "customerAdmin_withAssumedOwnedPackageAdminRole_canViewOnlyItsOwnCustomer", + diagramService.allGrantsToCurrentUser(of(Include.USERS, Include.TEST_ENTITIES, Include.NOT_ASSUMED, Include.DETAILS, Include.PERMISSIONS)), + "doc/customerAdmin_withAssumedOwnedPackageAdminRole_canViewOnlyItsOwnCustomer.md" + ); + context("customer-admin@xxx.example.com", "test_package#xxx00.admin"); final var result = testCustomerRepository.findCustomerByOptionalPrefixLike(null); -- 2.39.5 From 20de9ba7a4801dd018c74e0e261bd6d75d388549 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 7 Mar 2024 11:27:21 +0100 Subject: [PATCH 34/53] fixes and improvements after self-review --- doc/rbac.md | 14 +++++----- sql/rbac-view-option-experiments.sql | 10 +++---- .../test/cust/TestCustomerEntity.java | 3 +-- .../resources/db/changelog/010-context.sql | 4 +-- .../resources/db/changelog/050-rbac-base.sql | 27 ++++++++++--------- .../db/changelog/051-rbac-user-grant.sql | 19 ++++++++++--- .../changelog/118-test-customer-test-data.sql | 2 +- ...TestCustomerRepositoryIntegrationTest.java | 1 - src/test/resources/application.yml | 4 +-- 9 files changed, 47 insertions(+), 37 deletions(-) diff --git a/doc/rbac.md b/doc/rbac.md index e8799d5f..9aa4b024 100644 --- a/doc/rbac.md +++ b/doc/rbac.md @@ -171,10 +171,10 @@ An *RbacPermission* allows a specific *RbacOperation* on a specific *RbacObject* An *RbacOperation* determines, what an *RbacPermission* allows to do. It can be one of: -- **'INSERT'** - permits inserting new rows related to the row, to which the permission belongs, in the table which is specified an extra column -- **'SELECT'** - permits selecting the row specified by the permission -- **'UPDATE'** - permits updating (only the updatable columns of) the row specified by the permission -- **'DELETE'** - permits deleting the row specified by the permission +- **'INSERT'** - permits inserting new rows related to the row, to which the permission belongs, in the table which is specified an extra column, includes 'SELECT' +- **'SELECT'** - permits selecting the row specified by the permission, is included in all other permissions +- **'UPDATE'** - permits updating (only the updatable columns of) the row specified by the permission, includes 'SELECT' +- **'DELETE'** - permits deleting the row specified by the permission, includes 'SELECT' This list is extensible according to the needs of the access rule system. @@ -620,10 +620,10 @@ Let's have a look at the two view queries: WHERE target.uuid IN ( SELECT uuid FROM queryAccessibleObjectUuidsOfSubjectIds( - 'SELECTÄ, 'customer', currentSubjectsUuids())); + 'SELECT, 'customer', currentSubjectsUuids())); This view should be automatically updatable. -Where, for updates, we actually have to check for 'UPDATE' instead of 'SELECTÄ operation, which makes it a bit more complicated. +Where, for updates, we actually have to check for 'UPDATE' instead of 'SELECT' operation, which makes it a bit more complicated. With the larger dataset, the test suite initially needed over 7 seconds with this view query. At this point the second variant was tried. @@ -638,7 +638,7 @@ Looks like the query optimizer needed some statistics to find the best path. SELECT DISTINCT target.* FROM customer AS target JOIN queryAccessibleObjectUuidsOfSubjectIds( - 'SELECTÄ, 'customer', currentSubjectsUuids()) AS allowedObjId + 'SELECT, 'customer', currentSubjectsUuids()) AS allowedObjId ON target.uuid = allowedObjId; This view cannot is not updatable automatically, diff --git a/sql/rbac-view-option-experiments.sql b/sql/rbac-view-option-experiments.sql index 2c4508ae..f6e80e10 100644 --- a/sql/rbac-view-option-experiments.sql +++ b/sql/rbac-view-option-experiments.sql @@ -20,7 +20,7 @@ CREATE POLICY customer_policy ON customer TO restricted USING ( -- id=1000 - isPermissionGrantedToSubject(findPermissionId('test_customer', id, 'SELECT'), currentUserUuid()) + isPermissionGrantedToSubject(findEffectivePermissionId('test_customer', id, 'SELECT'), currentUserUuid()) ); SET SESSION AUTHORIZATION restricted; @@ -35,7 +35,7 @@ SELECT * FROM customer; CREATE OR REPLACE RULE "_RETURN" AS ON SELECT TO cust_view DO INSTEAD - SELECT * FROM customer WHERE isPermissionGrantedToSubject(findPermissionId('test_customer', id, 'SELECT'), currentUserUuid()); + SELECT * FROM customer WHERE isPermissionGrantedToSubject(findEffectivePermissionId('test_customer', id, 'SELECT'), currentUserUuid()); SELECT * from cust_view LIMIT 10; select queryAllPermissionsOfSubjectId(findRbacUser('superuser-alex@hostsharing.net')); @@ -52,7 +52,7 @@ CREATE OR REPLACE RULE "_RETURN" AS DO INSTEAD SELECT c.uuid, c.reference, c.prefix FROM customer AS c JOIN queryAllPermissionsOfSubjectId(currentUserUuid()) AS p - ON p.objectTable='test_customer' AND p.objectUuid=c.uuid AND p.op = 'SELECT'; + ON p.objectTable='test_customer' AND p.objectUuid=c.uuid; GRANT ALL PRIVILEGES ON cust_view TO restricted; SET SESSION SESSION AUTHORIZATION restricted; @@ -68,7 +68,7 @@ CREATE OR REPLACE VIEW cust_view AS SELECT c.uuid, c.reference, c.prefix FROM customer AS c JOIN queryAllPermissionsOfSubjectId(currentUserUuid()) AS p - ON p.objectUuid=c.uuid AND p.op = 'SELECT'; + ON p.objectUuid=c.uuid; GRANT ALL PRIVILEGES ON cust_view TO restricted; SET SESSION SESSION AUTHORIZATION restricted; @@ -81,7 +81,7 @@ select rr.uuid, rr.type from RbacGrants g join RbacReference RR on g.ascendantUuid = RR.uuid where g.descendantUuid in ( select uuid from queryAllPermissionsOfSubjectId(findRbacUser('alex@example.com')) - where objectTable='test_customer' and op = 'SELECT'); + where objectTable='test_customer'); call grantRoleToUser(findRoleId('test_customer#aaa.admin'), findRbacUser('aaaaouq@example.com')); 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 8101286b..d419806e 100644 --- a/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java @@ -14,7 +14,6 @@ import java.util.UUID; 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.rbacViewFor; @@ -43,7 +42,7 @@ public class TestCustomerEntity implements HasUuid { .withUpdatableColumns("reference", "prefix", "adminUserName") .createRole(OWNER, (with) -> { - // with.owningUser(CREATOR); TODO: needs assumed role + // with.owningUser(CREATOR); FIXME: needs assumed role, was: getRbacUserId(NEW.adminUserName, 'create') with.incomingSuperRole(GLOBAL, ADMIN); with.permission(DELETE); }) diff --git a/src/main/resources/db/changelog/010-context.sql b/src/main/resources/db/changelog/010-context.sql index 11d52f50..521e4812 100644 --- a/src/main/resources/db/changelog/010-context.sql +++ b/src/main/resources/db/changelog/010-context.sql @@ -66,11 +66,11 @@ begin when others then currentTask := null; end; --- TODO: uncomment +-- FIXME: uncomment -- if (currentTask is null or currentTask = '') then -- raise exception '[401] currentTask must be defined, please call `defineContext(...)`'; -- end if; - return 'unknown'; -- TODO: currentTask; + return 'unknown'; -- FIXME: currentTask; end; $$; --// diff --git a/src/main/resources/db/changelog/050-rbac-base.sql b/src/main/resources/db/changelog/050-rbac-base.sql index 2eeff958..c477cf3b 100644 --- a/src/main/resources/db/changelog/050-rbac-base.sql +++ b/src/main/resources/db/changelog/050-rbac-base.sql @@ -366,6 +366,7 @@ create trigger deleteRbacRolesOfRbacObject_Trigger */ create domain RbacOp as varchar(67) -- TODO: shorten to 8, once the deprecated values are gone +-- FIXME: uncomment check -- check ( -- VALUE = 'INSERT' or -- VALUE = 'DELETE' or @@ -389,17 +390,6 @@ create table RbacPermission call create_journal('RbacPermission'); -create or replace function permissionExists(forObjectUuid uuid, forOp RbacOp) - returns bool - language sql as $$ -select exists( - select op - from RbacPermission p - where p.objectUuid = forObjectUuid - and p.op = forOp - ); -$$; - create or replace function createPermission(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null) returns uuid language plpgsql as $$ @@ -462,7 +452,7 @@ begin end; $$; -create or replace function findPermissionId(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null ) +create or replace function findPermissionId(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null) returns uuid returns null on null input stable -- leakproof @@ -474,6 +464,17 @@ select uuid and p.opTableName = forOpTableName $$; +create or replace function findEffectivePermissionId(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null) + returns uuid + returns null on null input + stable -- leakproof + language sql as $$ +select uuid + from RbacPermission p + where p.objectUuid = forObjectUuid + and (forOp = 'SELECT' or p.op = forOp) -- all other RbacOp include 'SELECT' + and p.opTableName = forOpTableName +$$; --// -- ============================================================================ @@ -748,7 +749,7 @@ begin select descendantUuid from grants) as granted join RbacPermission perm - on granted.descendantUuid = perm.uuid and perm.op = requiredOp + on granted.descendantUuid = perm.uuid and (requiredOp = 'SELECT' or perm.op = requiredOp) join RbacObject obj on obj.uuid = perm.objectUuid and obj.objectTable = forObjectTable limit maxObjects + 1; diff --git a/src/main/resources/db/changelog/051-rbac-user-grant.sql b/src/main/resources/db/changelog/051-rbac-user-grant.sql index 05332ed9..b71869f9 100644 --- a/src/main/resources/db/changelog/051-rbac-user-grant.sql +++ b/src/main/resources/db/changelog/051-rbac-user-grant.sql @@ -37,17 +37,28 @@ end; $$; create or replace procedure grantRoleToUser(grantedByRoleUuid uuid, grantedRoleUuid uuid, userUuid uuid, doAssume boolean = true) language plpgsql as $$ +declare + grantedByRoleIdName text; + grantedRoleIdName text; begin perform assertReferenceType('grantingRoleUuid', grantedByRoleUuid, 'RbacRole'); perform assertReferenceType('grantedRoleUuid (descendant)', grantedRoleUuid, 'RbacRole'); perform assertReferenceType('userUuid (ascendant)', userUuid, 'RbacUser'); - if NOT isGranted(currentSubjectsUuids(), grantedByRoleUuid) then - raise exception '[403] Access to granted-by-role % forbidden for %', grantedByRoleUuid, currentSubjects(); - end if; + assert grantedByRoleUuid is not null, 'grantedByRoleUuid must not be null'; + assert grantedRoleUuid is not null, 'grantedRoleUuid must not be null'; + assert userUuid is not null, 'userUuid must not be null'; + if NOT isGranted(currentSubjectsUuids(), grantedByRoleUuid) then + select roleIdName from rbacRole_ev where uuid=grantedByRoleUuid into grantedByRoleIdName; + raise exception '[403] Access to granted-by-role % (%) forbidden for % (%)', + grantedByRoleIdName, grantedByRoleUuid, currentSubjects(), currentSubjectsUuids(); + end if; if NOT isGranted(grantedByRoleUuid, grantedRoleUuid) then - raise exception '[403] Access to granted role % forbidden for %', grantedRoleUuid, currentSubjects(); + select roleIdName from rbacRole_ev where uuid=grantedByRoleUuid into grantedByRoleIdName; + select roleIdName from rbacRole_ev where uuid=grantedRoleUuid into grantedRoleIdName; + raise exception '[403] Access to granted role % (%) forbidden for % (%)', + grantedRoleIdName, grantedRoleUuid, grantedByRoleUuid, grantedByRoleIdName; end if; insert diff --git a/src/main/resources/db/changelog/118-test-customer-test-data.sql b/src/main/resources/db/changelog/118-test-customer-test-data.sql index 643022fe..1e239001 100644 --- a/src/main/resources/db/changelog/118-test-customer-test-data.sql +++ b/src/main/resources/db/changelog/118-test-customer-test-data.sql @@ -46,8 +46,8 @@ begin select * into newCust from test_customer where reference=custReference; call grantRoleToUser( + getRoleId(testCustomerOwner(newCust), 'fail'), getRoleId(testCustomerAdmin(newCust), 'fail'), - findRoleId(testCustomerOwner(newCust)), custAdminUuid, true); end; $$; diff --git a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java index 55c958d5..b21f2d5d 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java @@ -16,7 +16,6 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceException; import jakarta.servlet.http.HttpServletRequest; -import java.util.EnumSet; import java.util.List; import java.util.UUID; diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index a4f570f9..01e283b9 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -4,8 +4,8 @@ spring: platform: postgres datasource: - url: jdbc:tc:postgresql:15.5-bookworm:///spring_boot_testcontainers - url-local: jdbc:postgresql://localhost:5432/postgres + url-tc: jdbc:tc:postgresql:15.5-bookworm:///spring_boot_testcontainers + url: jdbc:postgresql://localhost:5432/postgres username: postgres password: password -- 2.39.5 From b37e8044b2ade10f7d3ec37140a51d874ec1aa15 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 7 Mar 2024 12:26:07 +0100 Subject: [PATCH 35/53] implement insert trigger if no explicit grant rule is specified --- .../rbac/rbacdef/InsertTriggerGenerator.java | 60 +++++++++++++------ .../RolesGrantsAndPermissionsGenerator.java | 1 + .../db/changelog/080-rbac-global.sql | 13 ++++ .../db/changelog/113-test-customer-rbac.sql | 21 ++++++- .../db/changelog/123-test-package-rbac.sql | 5 +- ...TestCustomerRepositoryIntegrationTest.java | 4 +- 6 files changed, 81 insertions(+), 23 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java index adcb1c36..d5266ae9 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java @@ -1,6 +1,7 @@ package net.hostsharing.hsadminng.rbac.rbacdef; import java.util.Optional; +import java.util.stream.Stream; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.PERM_TO_ROLE; @@ -95,11 +96,7 @@ public class InsertTriggerGenerator { } private void generateInsertCheckTrigger(final StringWriter plPgSql) { - rbacDef.getGrantDefs().stream() - .filter(g -> g.isToCreate() && g.grantType() == PERM_TO_ROLE && - g.getPermDef().getPermission() == INSERT ) - .forEach(g -> { - plPgSql.writeLn(""" + plPgSql.writeLn(""" /** Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable}. */ @@ -107,24 +104,51 @@ public class InsertTriggerGenerator { returns trigger language plpgsql as $$ begin - raise exception 'insert into ${rawSubTable} not allowed for current subjects %', currentSubjectsUuids(); + raise exception 'insert into ${rawSubTable} not allowed for current subjects % (%)', + currentSubjects(), currentSubjectsUuids(); end; $$; - - create trigger ${rawSubTable}_insert_permission_check_tg - before insert on ${rawSubTable} - for each row - when ( not hasInsertPermission(NEW.${referenceColumn}, 'INSERT', '${rawSubTable}') ) - execute procedure ${rawSubTable}_insert_permission_missing_tf(); """, - with("rawSubTable", g.getPermDef().entityAlias.getRawTableName()), - with("referenceColumn", g.getSuperRoleDef().getEntityAlias().dependsOnColumName() )); - }); + with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName())); + getOptionalInsertGrant().ifPresentOrElse(g -> { + plPgSql.writeLn(""" + create trigger ${rawSubTable}_insert_permission_check_tg + before insert on ${rawSubTable} + for each row + when ( not hasInsertPermission(NEW.${referenceColumn}, 'INSERT', '${rawSubTable}') ) + execute procedure ${rawSubTable}_insert_permission_missing_tf(); + """, + with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()), + with("referenceColumn", g.getSuperRoleDef().getEntityAlias().dependsOnColumName() )); + }, + () -> { + plPgSql.writeLn(""" + create trigger ${rawSubTable}_insert_permission_check_tg + before insert on ${rawSubTable} + for each row + -- As there is no explicit INSERT grant specified for this table, + -- only global admins are allowed to insert any rows. + when ( not isGlobalAdmin() ) + execute procedure ${rawSubTable}_insert_permission_missing_tf(); + """, + with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName())); + }); + } + + private Stream getInsertGrants() { + return rbacDef.getGrantDefs().stream() + .filter(g -> g.grantType() == PERM_TO_ROLE) + .filter(g -> g.getPermDef().toCreate && g.getPermDef().getPermission() == INSERT); + } + + private Optional getOptionalInsertGrant() { + return getInsertGrants() + .reduce((x, y) -> { + throw new IllegalStateException("only a single INSERT permission grant allowed"); + }); } private Optional getOptionalInsertSuperRole() { - return rbacDef.getGrantDefs().stream() - .filter(g -> g.grantType() == PERM_TO_ROLE) - .filter(g -> g.getPermDef().toCreate && g.getPermDef().getPermission() == INSERT) + return getInsertGrants() .map(RbacView.RbacGrantDefinition::getSuperRoleDef) .reduce((x, y) -> { throw new IllegalStateException("only a single INSERT permission grant allowed"); 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 624ee471..4f1bffe3 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java @@ -334,6 +334,7 @@ class RolesGrantsAndPermissionsGenerator { .map(RbacPermissionDefinition::getPermission) .map(RbacView.Permission::permission) .map(p -> "'" + p + "'") + .sorted() .toList(); plPgSql.indented(() -> plPgSql.writeLn("permissions => array[" + joinArrayElements(arrayElements, 3) + "],\n")); diff --git a/src/main/resources/db/changelog/080-rbac-global.sql b/src/main/resources/db/changelog/080-rbac-global.sql index 034400fa..a262791c 100644 --- a/src/main/resources/db/changelog/080-rbac-global.sql +++ b/src/main/resources/db/changelog/080-rbac-global.sql @@ -22,6 +22,19 @@ grant select on global to ${HSADMINNG_POSTGRES_RESTRICTED_USERNAME}; --// +-- ============================================================================ +--changeset rbac-global-IS-GLOBAL-ADMIN:1 endDelimiter:--// +-- ------------------------------------------------------------------ + +create or replace function isGlobalAdmin() + returns boolean + language plpgsql as $$ +begin + return isGranted(currentSubjectsUuids(), findRoleId(globalAdmin())); +end; $$; +--// + + -- ============================================================================ --changeset rbac-global-HAS-GLOBAL-PERMISSION:1 endDelimiter:--// -- ------------------------------------------------------------------ diff --git a/src/main/resources/db/changelog/113-test-customer-rbac.sql b/src/main/resources/db/changelog/113-test-customer-rbac.sql index 1e5573cc..3149ccc9 100644 --- a/src/main/resources/db/changelog/113-test-customer-rbac.sql +++ b/src/main/resources/db/changelog/113-test-customer-rbac.sql @@ -1,5 +1,5 @@ --liquibase formatted sql --- This code generated was by RbacViewPostgresGenerator at 2024-03-06T15:40:13.239729250. +-- This code generated was by RbacViewPostgresGenerator at 2024-03-07T12:25:36.376742633. -- ============================================================================ @@ -80,6 +80,25 @@ execute procedure insertTriggerForTestCustomer_tf(); --changeset test-customer-rbac-INSERT:1 endDelimiter:--// -- ---------------------------------------------------------------------------- +/** + Checks if the user or assumed roles are allowed to insert a row to test_customer. +*/ +create or replace function test_customer_insert_permission_missing_tf() + returns trigger + language plpgsql as $$ +begin + raise exception 'insert into test_customer not allowed for current subjects % (%)', + currentSubjects(), currentSubjectsUuids(); +end; $$; + +create trigger test_customer_insert_permission_check_tg + before insert on test_customer + for each row + -- As there is no explicit INSERT grant specified for this table, + -- only global admins are allowed to insert any rows. + when ( not isGlobalAdmin() ) + execute procedure test_customer_insert_permission_missing_tf(); + --// -- ============================================================================ diff --git a/src/main/resources/db/changelog/123-test-package-rbac.sql b/src/main/resources/db/changelog/123-test-package-rbac.sql index fadb2562..b32a98dc 100644 --- a/src/main/resources/db/changelog/123-test-package-rbac.sql +++ b/src/main/resources/db/changelog/123-test-package-rbac.sql @@ -1,5 +1,5 @@ --liquibase formatted sql --- This code generated was by RbacViewPostgresGenerator at 2024-03-06T15:40:13.277446553. +-- This code generated was by RbacViewPostgresGenerator at 2024-03-07T12:25:36.422351715. -- ============================================================================ @@ -194,7 +194,8 @@ create or replace function test_package_insert_permission_missing_tf() returns trigger language plpgsql as $$ begin - raise exception 'insert into test_package not allowed for current subjects %', currentSubjectsUuids(); + raise exception 'insert into test_package not allowed for current subjects % (%)', + currentSubjects(), currentSubjectsUuids(); end; $$; create trigger test_package_insert_permission_check_tg diff --git a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java index b21f2d5d..01f09d26 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java @@ -74,7 +74,7 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest { // then result.assertExceptionWithRootCauseMessage( PersistenceException.class, - "add-customer not permitted for test_customer#xxx.admin"); + "ERROR: insert into test_customer not allowed for current subjects {test_customer#xxx.admin}"); } @Test @@ -92,7 +92,7 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest { // then result.assertExceptionWithRootCauseMessage( PersistenceException.class, - "add-customer not permitted for customer-admin@xxx.example.com"); + "ERROR: insert into test_customer not allowed for current subjects {customer-admin@xxx.example.com}"); } -- 2.39.5 From 9ecfdc722adf5d21e9423a0ade0682c004770b94 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 7 Mar 2024 14:42:25 +0100 Subject: [PATCH 36/53] fix currentContext resp. define Context and fix related fixme --- .../rbac/rbacdef/InsertTriggerGenerator.java | 2 +- src/main/resources/db/changelog/010-context.sql | 15 ++++++++------- .../db/changelog/113-test-customer-rbac.sql | 2 +- .../db/changelog/123-test-package-rbac.sql | 4 ++-- src/test/resources/application.yml | 4 ++-- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java index d5266ae9..1f48d045 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java @@ -48,7 +48,7 @@ public class InsertTriggerGenerator { permissionUuid uuid; roleUuid uuid; begin - call defineContext('generated Liquibase: create INSERT INTO ${rawSubTableName} permissions for the related ${rawSuperTableName} rows'); + call defineContext('create INSERT INTO ${rawSubTableName} permissions for the related ${rawSuperTableName} rows'); FOR row IN SELECT * FROM ${rawSuperTableName} LOOP diff --git a/src/main/resources/db/changelog/010-context.sql b/src/main/resources/db/changelog/010-context.sql index 521e4812..d1129184 100644 --- a/src/main/resources/db/changelog/010-context.sql +++ b/src/main/resources/db/changelog/010-context.sql @@ -23,13 +23,15 @@ end; $$; Defines the transaction context. */ create or replace procedure defineContext( - currentTask varchar, - currentRequest varchar = null, + currentTask varchar(96), + currentRequest varchar(512) = null, currentUser varchar = null, assumedRoles varchar = null ) language plpgsql as $$ begin + assert length(currentTask) <= 96, 'currentTask must not be longer than 96 characters'; + assert length(currentTask) > 8, 'currentTask must be at least 8 characters long'; execute format('set local hsadminng.currentTask to %L', currentTask); currentRequest := coalesce(currentRequest, ''); @@ -66,11 +68,10 @@ begin when others then currentTask := null; end; --- FIXME: uncomment --- if (currentTask is null or currentTask = '') then --- raise exception '[401] currentTask must be defined, please call `defineContext(...)`'; --- end if; - return 'unknown'; -- FIXME: currentTask; + if (currentTask is null or currentTask = '') then + raise exception '[401] currentTask must be defined, please call `defineContext(...)`'; + end if; + return currentTask; end; $$; --// diff --git a/src/main/resources/db/changelog/113-test-customer-rbac.sql b/src/main/resources/db/changelog/113-test-customer-rbac.sql index 3149ccc9..f62be84b 100644 --- a/src/main/resources/db/changelog/113-test-customer-rbac.sql +++ b/src/main/resources/db/changelog/113-test-customer-rbac.sql @@ -1,5 +1,5 @@ --liquibase formatted sql --- This code generated was by RbacViewPostgresGenerator at 2024-03-07T12:25:36.376742633. +-- This code generated was by RbacViewPostgresGenerator at 2024-03-07T14:39:25.446629076. -- ============================================================================ diff --git a/src/main/resources/db/changelog/123-test-package-rbac.sql b/src/main/resources/db/changelog/123-test-package-rbac.sql index b32a98dc..2d4ac417 100644 --- a/src/main/resources/db/changelog/123-test-package-rbac.sql +++ b/src/main/resources/db/changelog/123-test-package-rbac.sql @@ -1,5 +1,5 @@ --liquibase formatted sql --- This code generated was by RbacViewPostgresGenerator at 2024-03-07T12:25:36.422351715. +-- This code generated was by RbacViewPostgresGenerator at 2024-03-07T14:39:25.488573238. -- ============================================================================ @@ -157,7 +157,7 @@ do language plpgsql $$ permissionUuid uuid; roleUuid uuid; begin - call defineContext('generated Liquibase: create INSERT INTO test_package permissions for the related test_customer rows'); + call defineContext('create INSERT INTO test_package permissions for the related test_customer rows'); FOR row IN SELECT * FROM test_customer LOOP diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 01e283b9..a4f570f9 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -4,8 +4,8 @@ spring: platform: postgres datasource: - url-tc: jdbc:tc:postgresql:15.5-bookworm:///spring_boot_testcontainers - url: jdbc:postgresql://localhost:5432/postgres + url: jdbc:tc:postgresql:15.5-bookworm:///spring_boot_testcontainers + url-local: jdbc:postgresql://localhost:5432/postgres username: postgres password: password -- 2.39.5 From 20fc37da2202ba0af33a54fcac11326c99762bb2 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 7 Mar 2024 15:54:22 +0100 Subject: [PATCH 37/53] better error message for failing insert of rbacpermission, but leaving RbacOp domain check commented for now --- .../resources/db/changelog/050-rbac-base.sql | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/main/resources/db/changelog/050-rbac-base.sql b/src/main/resources/db/changelog/050-rbac-base.sql index c477cf3b..44e5cba9 100644 --- a/src/main/resources/db/changelog/050-rbac-base.sql +++ b/src/main/resources/db/changelog/050-rbac-base.sql @@ -366,17 +366,17 @@ create trigger deleteRbacRolesOfRbacObject_Trigger */ create domain RbacOp as varchar(67) -- TODO: shorten to 8, once the deprecated values are gone --- FIXME: uncomment check +-- FIXME: -- check ( --- VALUE = 'INSERT' or --- VALUE = 'DELETE' or --- VALUE = 'UPDATE' or --- VALUE = 'SELECT' or --- VALUE = 'ASSUME' or --- -- TODO: all values below are deprecated, use insert with table --- VALUE ~ '^add-[a-z]+$' or --- VALUE ~ '^new-[a-z-]+$' --- ); +-- VALUE = 'DELETE' +-- or VALUE = 'UPDATE' +-- or VALUE = 'SELECT' +-- or VALUE = 'INSERT' +-- or VALUE = 'ASSUME' +-- -- TODO: all values below are deprecated, use insert with table +-- or VALUE ~ '^add-[a-z]+$' +-- or VALUE ~ '^new-[a-z-]+$' +-- ) ; create table RbacPermission @@ -408,18 +408,20 @@ begin permissionUuid = (select uuid from RbacPermission where objectUuid = forObjectUuid and op = forOp and opTableName = forOpTableName); if (permissionUuid is null) then - insert - into RbacReference ("type") + insert into RbacReference ("type") values ('RbacPermission') returning uuid into permissionUuid; - raise warning 'for values (%, %, %, %)', permissionUuid, forObjectUuid, forOp, forOpTableName; -- TODO: remove - insert - into RbacPermission (uuid, objectUuid, op, opTableName) - values (permissionUuid, forObjectUuid, forOp, forOpTableName); + begin + insert into RbacPermission (uuid, objectUuid, op, opTableName) + values (permissionUuid, forObjectUuid, forOp, forOpTableName); + exception + when others then + raise exception 'insert into RbacPermission (uuid, objectUuid, op, opTableName) + values (%, %, %, %);', permissionUuid, forObjectUuid, forOp, forOpTableName; + end; end if; return permissionUuid; -end; -$$; +end; $$; -- TODO: deprecated, remove and amend all usages to createPermission create or replace function createPermissions(forObjectUuid uuid, permitOps RbacOp[]) -- 2.39.5 From 1fb1dcce50b63503d42a5325cba0278354188675 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 7 Mar 2024 16:03:44 +0100 Subject: [PATCH 38/53] .createRole().with.owningUser(CREATOR) is not working --- .../hostsharing/hsadminng/test/cust/TestCustomerEntity.java | 3 ++- src/main/resources/db/changelog/113-test-customer-rbac.sql | 3 ++- src/main/resources/db/changelog/123-test-package-rbac.sql | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) 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 d419806e..21a5f650 100644 --- a/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java @@ -14,6 +14,7 @@ import java.util.UUID; 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.rbacViewFor; @@ -42,7 +43,7 @@ public class TestCustomerEntity implements HasUuid { .withUpdatableColumns("reference", "prefix", "adminUserName") .createRole(OWNER, (with) -> { - // with.owningUser(CREATOR); FIXME: needs assumed role, was: getRbacUserId(NEW.adminUserName, 'create') + with.owningUser(CREATOR); with.incomingSuperRole(GLOBAL, ADMIN); with.permission(DELETE); }) diff --git a/src/main/resources/db/changelog/113-test-customer-rbac.sql b/src/main/resources/db/changelog/113-test-customer-rbac.sql index f62be84b..bdd01a62 100644 --- a/src/main/resources/db/changelog/113-test-customer-rbac.sql +++ b/src/main/resources/db/changelog/113-test-customer-rbac.sql @@ -1,5 +1,5 @@ --liquibase formatted sql --- This code generated was by RbacViewPostgresGenerator at 2024-03-07T14:39:25.446629076. +-- This code generated was by RbacViewPostgresGenerator at 2024-03-07T15:57:25.487712422. -- ============================================================================ @@ -38,6 +38,7 @@ begin perform createRoleWithGrants( testCustomerOwner(NEW), permissions => array['DELETE'], + userUuids => array[currentUserUuid()], incomingSuperRoles => array[globalAdmin()] ); diff --git a/src/main/resources/db/changelog/123-test-package-rbac.sql b/src/main/resources/db/changelog/123-test-package-rbac.sql index 2d4ac417..f36eacee 100644 --- a/src/main/resources/db/changelog/123-test-package-rbac.sql +++ b/src/main/resources/db/changelog/123-test-package-rbac.sql @@ -1,5 +1,5 @@ --liquibase formatted sql --- This code generated was by RbacViewPostgresGenerator at 2024-03-07T14:39:25.488573238. +-- This code generated was by RbacViewPostgresGenerator at 2024-03-07T15:57:25.536171618. -- ============================================================================ -- 2.39.5 From eb7dea54b57c3bfd4d2924b25b26be1181d3f2cd Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 7 Mar 2024 18:12:33 +0100 Subject: [PATCH 39/53] fix TestCustomerControllerAcceptanceTest --- .../hsadminng/rbac/rbacdef/InsertTriggerGenerator.java | 2 +- .../hsadminng/test/cust/TestCustomerController.java | 7 ++++++- src/main/resources/db/changelog/113-test-customer-rbac.sql | 4 ++-- src/main/resources/db/changelog/123-test-package-rbac.sql | 4 ++-- .../test/cust/TestCustomerControllerAcceptanceTest.java | 6 +++--- .../test/cust/TestCustomerRepositoryIntegrationTest.java | 4 ++-- 6 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java index 1f48d045..7afd1941 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java @@ -104,7 +104,7 @@ public class InsertTriggerGenerator { returns trigger language plpgsql as $$ begin - raise exception 'insert into ${rawSubTable} not allowed for current subjects % (%)', + raise exception '[403] insert into ${rawSubTable} not allowed for current subjects % (%)', currentSubjects(), currentSubjectsUuids(); end; $$; """, diff --git a/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerController.java b/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerController.java index 1bd000ba..78752d9d 100644 --- a/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerController.java +++ b/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerController.java @@ -10,6 +10,8 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; import java.util.List; @RestController @@ -24,6 +26,9 @@ public class TestCustomerController implements TestCustomersApi { @Autowired private TestCustomerRepository testCustomerRepository; + @PersistenceContext + EntityManager em; + @Override @Transactional(readOnly = true) public ResponseEntity> listCustomers( @@ -48,7 +53,7 @@ public class TestCustomerController implements TestCustomersApi { context.define(currentUser, assumedRoles); final var saved = testCustomerRepository.save(mapper.map(customer, TestCustomerEntity.class)); - + em.flush(); final var uri = MvcUriComponentsBuilder.fromController(getClass()) .path("/api/test/customers/{id}") diff --git a/src/main/resources/db/changelog/113-test-customer-rbac.sql b/src/main/resources/db/changelog/113-test-customer-rbac.sql index bdd01a62..a082d1ed 100644 --- a/src/main/resources/db/changelog/113-test-customer-rbac.sql +++ b/src/main/resources/db/changelog/113-test-customer-rbac.sql @@ -1,5 +1,5 @@ --liquibase formatted sql --- This code generated was by RbacViewPostgresGenerator at 2024-03-07T15:57:25.487712422. +-- This code generated was by RbacViewPostgresGenerator at 2024-03-07T18:03:21.967830771. -- ============================================================================ @@ -88,7 +88,7 @@ create or replace function test_customer_insert_permission_missing_tf() returns trigger language plpgsql as $$ begin - raise exception 'insert into test_customer not allowed for current subjects % (%)', + raise exception '[403] insert into test_customer not allowed for current subjects % (%)', currentSubjects(), currentSubjectsUuids(); end; $$; diff --git a/src/main/resources/db/changelog/123-test-package-rbac.sql b/src/main/resources/db/changelog/123-test-package-rbac.sql index f36eacee..676ec6c0 100644 --- a/src/main/resources/db/changelog/123-test-package-rbac.sql +++ b/src/main/resources/db/changelog/123-test-package-rbac.sql @@ -1,5 +1,5 @@ --liquibase formatted sql --- This code generated was by RbacViewPostgresGenerator at 2024-03-07T15:57:25.536171618. +-- This code generated was by RbacViewPostgresGenerator at 2024-03-07T18:03:22.000977525. -- ============================================================================ @@ -194,7 +194,7 @@ create or replace function test_package_insert_permission_missing_tf() returns trigger language plpgsql as $$ begin - raise exception 'insert into test_package not allowed for current subjects % (%)', + raise exception '[403] insert into test_package not allowed for current subjects % (%)', currentSubjects(), currentSubjectsUuids(); end; $$; diff --git a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerControllerAcceptanceTest.java index 6c695caa..942351c0 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerControllerAcceptanceTest.java @@ -148,7 +148,7 @@ class TestCustomerControllerAcceptanceTest { // finally, the new customer can be viewed by its own admin final var newUserUuid = UUID.fromString( location.substring(location.lastIndexOf('/') + 1)); - context.define("customer-admin@uuu.example.com"); + context.define("superuser-fran@hostsharing.net", "test_customer#uuu.admin"); assertThat(testCustomerRepository.findByUuid(newUserUuid)) .hasValueSatisfying(c -> assertThat(c.getPrefix()).isEqualTo("uuu")); } @@ -175,7 +175,7 @@ class TestCustomerControllerAcceptanceTest { .statusCode(403) .contentType(ContentType.JSON) .statusCode(403) - .body("message", containsString("add-customer not permitted for test_customer#xxx.admin")); + .body("message", containsString("insert into test_customer not allowed for current subjects {test_customer#xxx.admin}")); // @formatter:on // finally, the new customer was not created @@ -204,7 +204,7 @@ class TestCustomerControllerAcceptanceTest { .statusCode(403) .contentType(ContentType.JSON) .statusCode(403) - .body("message", containsString("add-customer not permitted for customer-admin@yyy.example.com")); + .body("message", containsString("insert into test_customer not allowed for current subjects {customer-admin@yyy.example.com}")); // @formatter:on // finally, the new customer was not created diff --git a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java index 01f09d26..01aa0760 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java @@ -74,7 +74,7 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest { // then result.assertExceptionWithRootCauseMessage( PersistenceException.class, - "ERROR: insert into test_customer not allowed for current subjects {test_customer#xxx.admin}"); + "ERROR: [403] insert into test_customer not allowed for current subjects {test_customer#xxx.admin}"); } @Test @@ -92,7 +92,7 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest { // then result.assertExceptionWithRootCauseMessage( PersistenceException.class, - "ERROR: insert into test_customer not allowed for current subjects {customer-admin@xxx.example.com}"); + "ERROR: [403] insert into test_customer not allowed for current subjects {customer-admin@xxx.example.com}"); } -- 2.39.5 From 86c0bb3e76d1dc15180454e154dbf3113f9d8571 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 8 Mar 2024 08:53:28 +0100 Subject: [PATCH 40/53] some minor amendments after self-code-review --- .../rbac/rbacdef/RbacIdentityViewGenerator.java | 1 - .../hsadminng/rbac/rbacdef/RbacObjectGenerator.java | 1 - .../rbac/rbacdef/RbacRestrictedViewGenerator.java | 1 - .../rbac/rbacdef/RbacRoleDescriptorsGenerator.java | 1 - .../rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java | 2 +- .../hsadminng/test/pac/TestPackageEntity.java | 1 - .../resources/db/changelog/113-test-customer-rbac.sql | 8 ++------ .../resources/db/changelog/123-test-package-rbac.sql | 9 ++------- 8 files changed, 5 insertions(+), 19 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacIdentityViewGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacIdentityViewGenerator.java index ed51061b..9eba4a68 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacIdentityViewGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacIdentityViewGenerator.java @@ -17,7 +17,6 @@ public class RbacIdentityViewGenerator { void generateTo(final StringWriter plPgSql) { plPgSql.writeLn(""" - -- ============================================================================ --changeset ${liquibaseTagPrefix}-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacObjectGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacObjectGenerator.java index 9c1579af..a7377301 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacObjectGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacObjectGenerator.java @@ -14,7 +14,6 @@ public class RbacObjectGenerator { void generateTo(final StringWriter plPgSql) { plPgSql.writeLn(""" - -- ============================================================================ --changeset ${liquibaseTagPrefix}-rbac-OBJECT:1 endDelimiter:--// -- ---------------------------------------------------------------------------- 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 32f2d8e0..f8f6e890 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacRestrictedViewGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacRestrictedViewGenerator.java @@ -20,7 +20,6 @@ public class RbacRestrictedViewGenerator { void generateTo(final StringWriter plPgSql) { plPgSql.writeLn(""" - -- ============================================================================ --changeset ${liquibaseTagPrefix}-rbac-RESTRICTED-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacRoleDescriptorsGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacRoleDescriptorsGenerator.java index 661f9091..dab3ab01 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacRoleDescriptorsGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacRoleDescriptorsGenerator.java @@ -16,7 +16,6 @@ public class RbacRoleDescriptorsGenerator { void generateTo(final StringWriter plPgSql) { plPgSql.writeLn(""" - -- ============================================================================ --changeset ${liquibaseTagPrefix}-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--// -- ---------------------------------------------------------------------------- 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 4f1bffe3..20377ac4 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java @@ -59,7 +59,7 @@ class RolesGrantsAndPermissionsGenerator { private void generateInsertTriggerFunction(final StringWriter plPgSql) { plPgSql.writeLn(""" /* - A Creates the roles, grants and permission for the AFTER INSERT TRIGGER. + Creates the roles, grants and permission for the AFTER INSERT TRIGGER. */ create or replace procedure buildRbacSystemFor${simpleEntityName}( 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 81d577bc..acbbc6ec 100644 --- a/src/main/java/net/hostsharing/hsadminng/test/pac/TestPackageEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/test/pac/TestPackageEntity.java @@ -58,7 +58,6 @@ public class TestPackageEntity implements HasUuid { .toRole("customer", ADMIN).grantPermission("package", INSERT) .createRole(OWNER, (with) -> { - with.owningUser(CREATOR); with.incomingSuperRole("customer", ADMIN).unassumed(); with.permission(DELETE); with.permission(UPDATE); diff --git a/src/main/resources/db/changelog/113-test-customer-rbac.sql b/src/main/resources/db/changelog/113-test-customer-rbac.sql index a082d1ed..da24ae34 100644 --- a/src/main/resources/db/changelog/113-test-customer-rbac.sql +++ b/src/main/resources/db/changelog/113-test-customer-rbac.sql @@ -1,6 +1,5 @@ --liquibase formatted sql --- This code generated was by RbacViewPostgresGenerator at 2024-03-07T18:03:21.967830771. - +-- This code generated was by RbacViewPostgresGenerator at 2024-03-08T08:48:56.112505380. -- ============================================================================ --changeset test-customer-rbac-OBJECT:1 endDelimiter:--// @@ -9,7 +8,6 @@ call generateRelatedRbacObject('test_customer'); --// - -- ============================================================================ --changeset test-customer-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--// -- ---------------------------------------------------------------------------- @@ -22,7 +20,7 @@ call generateRbacRoleDescriptors('testCustomer', 'test_customer'); -- ---------------------------------------------------------------------------- /* - A Creates the roles, grants and permission for the AFTER INSERT TRIGGER. + Creates the roles, grants and permission for the AFTER INSERT TRIGGER. */ create or replace procedure buildRbacSystemForTestCustomer( @@ -101,7 +99,6 @@ create trigger test_customer_insert_permission_check_tg execute procedure test_customer_insert_permission_missing_tf(); --// - -- ============================================================================ --changeset test-customer-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- @@ -111,7 +108,6 @@ call generateRbacIdentityView('test_customer', $idName$ --// - -- ============================================================================ --changeset test-customer-rbac-RESTRICTED-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- diff --git a/src/main/resources/db/changelog/123-test-package-rbac.sql b/src/main/resources/db/changelog/123-test-package-rbac.sql index 676ec6c0..950acef8 100644 --- a/src/main/resources/db/changelog/123-test-package-rbac.sql +++ b/src/main/resources/db/changelog/123-test-package-rbac.sql @@ -1,6 +1,5 @@ --liquibase formatted sql --- This code generated was by RbacViewPostgresGenerator at 2024-03-07T18:03:22.000977525. - +-- This code generated was by RbacViewPostgresGenerator at 2024-03-08T08:48:56.148164198. -- ============================================================================ --changeset test-package-rbac-OBJECT:1 endDelimiter:--// @@ -9,7 +8,6 @@ call generateRelatedRbacObject('test_package'); --// - -- ============================================================================ --changeset test-package-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--// -- ---------------------------------------------------------------------------- @@ -22,7 +20,7 @@ call generateRbacRoleDescriptors('testPackage', 'test_package'); -- ---------------------------------------------------------------------------- /* - A Creates the roles, grants and permission for the AFTER INSERT TRIGGER. + Creates the roles, grants and permission for the AFTER INSERT TRIGGER. */ create or replace procedure buildRbacSystemForTestPackage( @@ -42,7 +40,6 @@ begin perform createRoleWithGrants( testPackageOwner(NEW), permissions => array['DELETE', 'UPDATE'], - userUuids => array[currentUserUuid()], incomingSuperRoles => array[testCustomerAdmin(newCustomer)] ); @@ -205,7 +202,6 @@ create trigger test_package_insert_permission_check_tg execute procedure test_package_insert_permission_missing_tf(); --// - -- ============================================================================ --changeset test-package-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- @@ -215,7 +211,6 @@ call generateRbacIdentityView('test_package', $idName$ --// - -- ============================================================================ --changeset test-package-rbac-RESTRICTED-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -- 2.39.5 From d40cf019cce30bc780ec9257511465c76c9e096b Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 8 Mar 2024 13:21:00 +0100 Subject: [PATCH 41/53] implement assumed in Generator --- .../hsadminng/rbac/rbacdef/RbacView.java | 5 +- .../rbacdef/RbacViewMermaidFlowchart.java | 5 +- .../RolesGrantsAndPermissionsGenerator.java | 27 +++++---- .../test/cust/TestCustomerEntity.java | 4 +- .../hsadminng/test/pac/TestPackageEntity.java | 3 +- .../resources/db/changelog/050-rbac-base.sql | 24 +++++++- .../db/changelog/057-rbac-role-builder.sql | 34 ++++------- .../db/changelog/058-rbac-generators.sql | 22 +++---- .../db/changelog/080-rbac-global.sql | 4 +- .../db/changelog/113-test-customer-rbac.md | 41 +++++++++++++ .../db/changelog/113-test-customer-rbac.sql | 4 +- .../db/changelog/123-test-package-rbac.md | 57 +++++++++++++++++++ .../db/changelog/123-test-package-rbac.sql | 2 +- .../hsadminng/context/ContextBasedTest.java | 19 ++++++- .../test/cust/TestCustomerEntityTest.java | 4 +- ...TestCustomerRepositoryIntegrationTest.java | 19 +------ .../test/pac/TestPackageEntityTest.java | 13 ++--- .../TestPackageRepositoryIntegrationTest.java | 13 ++--- 18 files changed, 203 insertions(+), 97 deletions(-) create mode 100644 src/main/resources/db/changelog/113-test-customer-rbac.md create mode 100644 src/main/resources/db/changelog/123-test-package-rbac.md 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 064a7350..5dd52c71 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -197,7 +197,7 @@ public class RbacView { }); importedRbacView.getGrantDefs().forEach(grantDef -> { if (grantDef.grantType() == RbacGrantDefinition.GrantType.ROLE_TO_ROLE) { - findOrCreateGrantDef( + final var importedGrantDef = findOrCreateGrantDef( findRbacRole( mapper.map(grantDef.getSubRoleDef().entityAlias.aliasName), grantDef.getSubRoleDef().getRole()), @@ -205,6 +205,9 @@ public class RbacView { mapper.map(grantDef.getSuperRoleDef().entityAlias.aliasName), grantDef.getSuperRoleDef().getRole()) ); + if (!grantDef.isAssumed()) { + importedGrantDef.unassumed(); + } } }); return this; 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 27615f85..9a806cc4 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java @@ -109,9 +109,8 @@ public class RbacViewMermaidFlowchart { } private String grantDef(final RbacView.RbacGrantDefinition grant) { - final var arrow = grant.isToCreate() - ? grant.isAssumed() ? " ==> " : " == /// ==> " - : grant.isAssumed() ? " -.-> " : " -.- /// -.-> "; + final var arrow = (grant.isToCreate() ? " ==>" : " -.->") + + (grant.isAssumed() ? " " : "|XX| "); 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/RolesGrantsAndPermissionsGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java index 20377ac4..edb1f609 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java @@ -230,7 +230,8 @@ class RolesGrantsAndPermissionsGenerator { 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}${assumed});" + .replace("${assumed}", grantDef.isAssumed() ? "" : ", unassumed()") .replace("${subRoleRef}", roleRef(NEW, grantDef.getSubRoleDef())) .replace("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef())); case PERM_TO_ROLE -> @@ -345,12 +346,11 @@ class RolesGrantsAndPermissionsGenerator { 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)) + final var arrayElements = incomingGrants.stream() + .map(g -> toPlPgSqlReference(NEW, g.getSuperRoleDef(), g.isAssumed())) .toList(); plPgSql.indented(() -> - plPgSql.writeLn("incomingSuperRoles => array[" + joinArrayElements(arraElements, 1) + "],\n")); + plPgSql.writeLn("incomingSuperRoles => array[" + joinArrayElements(arrayElements, 1) + "],\n")); rbacGrants.removeAll(incomingGrants); } } @@ -359,8 +359,7 @@ class RolesGrantsAndPermissionsGenerator { final var outgoingGrants = findOutgoingSuperRolesForRole(rbacDef.getRootEntityAlias(), role); if (!outgoingGrants.isEmpty()) { final var arrayElements = outgoingGrants.stream() - .map(RbacView.RbacGrantDefinition::getSubRoleDef) - .map(r -> toPlPgSqlReference(NEW, r)) + .map(g -> toPlPgSqlReference(NEW, g.getSubRoleDef(), g.isAssumed())) .toList(); plPgSql.indented(() -> plPgSql.writeLn("outgoingSubRoles => array[" + joinArrayElements(arrayElements, 1) + "],\n")); @@ -485,14 +484,18 @@ class RolesGrantsAndPermissionsGenerator { }; } - private String toPlPgSqlReference(final PostgresTriggerReference triggerRef, final RbacView.RbacRoleDefinition roleDef) { - return toVar(roleDef) + - (roleDef.getEntityAlias().isGlobal() ? "()" + private String toPlPgSqlReference( + final PostgresTriggerReference triggerRef, + final RbacView.RbacRoleDefinition roleDef, + final boolean assumed) { + final var assumedArg = assumed ? "" : ", unassumed()"; + return toRoleRef(roleDef) + + (roleDef.getEntityAlias().isGlobal() ? ( assumed ? "()" : "(unassumed())") : rbacDef.isRootEntityAlias(roleDef.getEntityAlias()) ? ("(" + triggerRef.name() + ")") - : "(" + toTriggerReference(triggerRef, roleDef.getEntityAlias()) + ")"); + : "(" + toTriggerReference(triggerRef, roleDef.getEntityAlias()) + assumedArg + ")"); } - private static String toVar(final RbacView.RbacRoleDefinition roleDef) { + private static String toRoleRef(final RbacView.RbacRoleDefinition roleDef) { return uncapitalize(roleDef.getEntityAlias().simpleName()) + capitalize(roleDef.getRole().roleName()); } 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 21a5f650..bde8db09 100644 --- a/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java @@ -43,8 +43,8 @@ public class TestCustomerEntity implements HasUuid { .withUpdatableColumns("reference", "prefix", "adminUserName") .createRole(OWNER, (with) -> { - with.owningUser(CREATOR); - with.incomingSuperRole(GLOBAL, ADMIN); + with.owningUser(CREATOR).unassumed(); + with.incomingSuperRole(GLOBAL, ADMIN).unassumed(); with.permission(DELETE); }) .createSubRole(ADMIN, (with) -> { 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 acbbc6ec..757fcf05 100644 --- a/src/main/java/net/hostsharing/hsadminng/test/pac/TestPackageEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/test/pac/TestPackageEntity.java @@ -15,7 +15,6 @@ 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; @@ -58,7 +57,7 @@ public class TestPackageEntity implements HasUuid { .toRole("customer", ADMIN).grantPermission("package", INSERT) .createRole(OWNER, (with) -> { - with.incomingSuperRole("customer", ADMIN).unassumed(); + with.incomingSuperRole("customer", ADMIN); with.permission(DELETE); with.permission(UPDATE); }) diff --git a/src/main/resources/db/changelog/050-rbac-base.sql b/src/main/resources/db/changelog/050-rbac-base.sql index 44e5cba9..f3b784c8 100644 --- a/src/main/resources/db/changelog/050-rbac-base.sql +++ b/src/main/resources/db/changelog/050-rbac-base.sql @@ -203,15 +203,33 @@ create type RbacRoleDescriptor as ( objectTable varchar(63), -- for human readability and easier debugging objectUuid uuid, - roleType RbacRoleType + roleType RbacRoleType, + assumed boolean ); -create or replace function roleDescriptor(objectTable varchar(63), objectUuid uuid, roleType RbacRoleType) +create or replace function assumed() + returns boolean + stable -- leakproof + language sql as $$ + select true; +$$; + +create or replace function unassumed() + returns boolean + stable -- leakproof + language sql as $$ +select false; +$$; + + +create or replace function roleDescriptor( + objectTable varchar(63), objectUuid uuid, roleType RbacRoleType, + assumed boolean = true) -- just for DSL readability, belongs actually to the grant returns RbacRoleDescriptor returns null on null input stable -- leakproof language sql as $$ -select objectTable, objectUuid, roleType::RbacRoleType; + select objectTable, objectUuid, roleType::RbacRoleType, assumed; $$; create or replace function createRole(roleDescriptor RbacRoleDescriptor) diff --git a/src/main/resources/db/changelog/057-rbac-role-builder.sql b/src/main/resources/db/changelog/057-rbac-role-builder.sql index 93b36909..221919a3 100644 --- a/src/main/resources/db/changelog/057-rbac-role-builder.sql +++ b/src/main/resources/db/changelog/057-rbac-role-builder.sql @@ -13,24 +13,6 @@ begin return createPermissions(forObjectUuid, permitOps); end; $$; -create or replace function toRoleUuids(roleDescriptors RbacRoleDescriptor[]) - returns uuid[] - language plpgsql - strict as $$ -declare - superRoleDescriptor RbacRoleDescriptor; - superRoleUuids uuid[] := array []::uuid[]; -begin - foreach superRoleDescriptor in array roleDescriptors - loop - if superRoleDescriptor is not null then - superRoleUuids := superRoleUuids || getRoleId(superRoleDescriptor, 'fail'); - end if; - end loop; - - return superRoleUuids; -end; $$; - -- ================================================================= -- CREATE ROLE @@ -50,25 +32,29 @@ create or replace function createRoleWithGrants( language plpgsql as $$ declare roleUuid uuid; - superRoleUuid uuid; + subRoleDesc RbacRoleDescriptor; + superRoleDesc RbacRoleDescriptor; subRoleUuid uuid; + superRoleUuid uuid; userUuid uuid; grantedByRoleUuid uuid; begin roleUuid := createRole(roleDescriptor); - if cardinality(permissions) >0 then + if cardinality(permissions) > 0 then call grantPermissionsToRole(roleUuid, toPermissionUuids(roleDescriptor.objectuuid, permissions)); end if; - foreach superRoleUuid in array toRoleUuids(incomingSuperRoles) + foreach superRoleDesc in array incomingSuperRoles loop - call grantRoleToRole(roleUuid, superRoleUuid); + superRoleUuid = getRoleId(superRoleDesc, 'fail'); + call grantRoleToRole(roleUuid, superRoleUuid, superRoleDesc.assumed); end loop; - foreach subRoleUuid in array toRoleUuids(outgoingSubRoles) + foreach subRoleDesc in array outgoingSubRoles loop - call grantRoleToRole(subRoleUuid, roleUuid); + subRoleUuid = getRoleId(subRoleDesc, 'fail'); + call grantRoleToRole(subRoleUuid, roleUuid, subRoleDesc.assumed); end loop; if cardinality(userUuids) > 0 then diff --git a/src/main/resources/db/changelog/058-rbac-generators.sql b/src/main/resources/db/changelog/058-rbac-generators.sql index d77d1f20..4f4fb086 100644 --- a/src/main/resources/db/changelog/058-rbac-generators.sql +++ b/src/main/resources/db/changelog/058-rbac-generators.sql @@ -35,50 +35,50 @@ end; $$; --changeset rbac-generators-ROLE-DESCRIPTORS:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -create or replace procedure generateRbacRoleDescriptors(prefix text, targetTable text) +create procedure generateRbacRoleDescriptors(prefix text, targetTable text) language plpgsql as $$ declare sql text; begin sql = format($sql$ - create or replace function %1$sOwner(entity %2$s) + create or replace function %1$sOwner(entity %2$s, assumed boolean = true) returns RbacRoleDescriptor language plpgsql strict as $f$ begin - return roleDescriptor('%2$s', entity.uuid, 'owner'); + return roleDescriptor('%2$s', entity.uuid, 'owner', assumed); end; $f$; - create or replace function %1$sAdmin(entity %2$s) + create or replace function %1$sAdmin(entity %2$s, assumed boolean = true) returns RbacRoleDescriptor language plpgsql strict as $f$ begin - return roleDescriptor('%2$s', entity.uuid, 'admin'); + return roleDescriptor('%2$s', entity.uuid, 'admin', assumed); end; $f$; - create or replace function %1$sAgent(entity %2$s) + create or replace function %1$sAgent(entity %2$s, assumed boolean = true) returns RbacRoleDescriptor language plpgsql strict as $f$ begin - return roleDescriptor('%2$s', entity.uuid, 'agent'); + return roleDescriptor('%2$s', entity.uuid, 'agent', assumed); end; $f$; - create or replace function %1$sTenant(entity %2$s) + create or replace function %1$sTenant(entity %2$s, assumed boolean = true) returns RbacRoleDescriptor language plpgsql strict as $f$ begin - return roleDescriptor('%2$s', entity.uuid, 'tenant'); + return roleDescriptor('%2$s', entity.uuid, 'tenant', assumed); end; $f$; - create or replace function %1$sGuest(entity %2$s) + create or replace function %1$sGuest(entity %2$s, assumed boolean = true) returns RbacRoleDescriptor language plpgsql strict as $f$ begin - return roleDescriptor('%2$s', entity.uuid, 'guest'); + return roleDescriptor('%2$s', entity.uuid, 'guest', assumed); end; $f$; $sql$, prefix, targetTable); diff --git a/src/main/resources/db/changelog/080-rbac-global.sql b/src/main/resources/db/changelog/080-rbac-global.sql index a262791c..8313d05d 100644 --- a/src/main/resources/db/changelog/080-rbac-global.sql +++ b/src/main/resources/db/changelog/080-rbac-global.sql @@ -109,12 +109,12 @@ commit; /* A global administrator role. */ -create or replace function globalAdmin() +create or replace function globalAdmin(assumed boolean = true) returns RbacRoleDescriptor returns null on null input stable -- leakproof language sql as $$ -select 'global', (select uuid from RbacObject where objectTable = 'global'), 'admin'::RbacRoleType; +select 'global', (select uuid from RbacObject where objectTable = 'global'), 'admin'::RbacRoleType, assumed; $$; begin transaction; diff --git a/src/main/resources/db/changelog/113-test-customer-rbac.md b/src/main/resources/db/changelog/113-test-customer-rbac.md new file mode 100644 index 00000000..29e360a6 --- /dev/null +++ b/src/main/resources/db/changelog/113-test-customer-rbac.md @@ -0,0 +1,41 @@ +### rbac customer 2024-03-08T13:03:39.397294085 + +```mermaid +%%{init:{'flowchart':{'htmlLabels':false}}}%% +flowchart TB + +subgraph customer["`**customer**`"] + direction TB + style customer fill:#dd4901,stroke:#274d6e,stroke-width:8px + + subgraph customer:roles[ ] + 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,stroke:white + + perm:customer:DELETE{{customer:DELETE}} + perm:customer:UPDATE{{customer:UPDATE}} + perm:customer:SELECT{{customer:SELECT}} + end +end + +%% granting roles to users +user:creator ==>|XX| role:customer:owner + +%% granting roles to roles +role:global:admin ==>|XX| role:customer:owner +role:customer:owner ==> role:customer:admin +role:customer:admin ==> role:customer:tenant + +%% granting permissions to roles +role:customer:owner ==> perm:customer:DELETE +role:customer:admin ==> perm:customer:UPDATE +role:customer:tenant ==> perm:customer:SELECT + +``` diff --git a/src/main/resources/db/changelog/113-test-customer-rbac.sql b/src/main/resources/db/changelog/113-test-customer-rbac.sql index da24ae34..2bdaf47a 100644 --- a/src/main/resources/db/changelog/113-test-customer-rbac.sql +++ b/src/main/resources/db/changelog/113-test-customer-rbac.sql @@ -1,5 +1,5 @@ --liquibase formatted sql --- This code generated was by RbacViewPostgresGenerator at 2024-03-08T08:48:56.112505380. +-- This code generated was by RbacViewPostgresGenerator at 2024-03-08T13:03:39.428165899. -- ============================================================================ --changeset test-customer-rbac-OBJECT:1 endDelimiter:--// @@ -37,7 +37,7 @@ begin testCustomerOwner(NEW), permissions => array['DELETE'], userUuids => array[currentUserUuid()], - incomingSuperRoles => array[globalAdmin()] + incomingSuperRoles => array[globalAdmin(unassumed())] ); perform createRoleWithGrants( diff --git a/src/main/resources/db/changelog/123-test-package-rbac.md b/src/main/resources/db/changelog/123-test-package-rbac.md new file mode 100644 index 00000000..42950bd0 --- /dev/null +++ b/src/main/resources/db/changelog/123-test-package-rbac.md @@ -0,0 +1,57 @@ +### rbac package 2024-03-08T13:03:39.472333368 + +```mermaid +%%{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:DELETE{{package:DELETE}} + perm:package:UPDATE{{package:UPDATE}} + perm:package:SELECT{{package:SELECT}} + 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 roles +role:global:admin -.->|XX| 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:DELETE +role:package:owner ==> perm:package:UPDATE +role:package:tenant ==> perm:package:SELECT + +``` diff --git a/src/main/resources/db/changelog/123-test-package-rbac.sql b/src/main/resources/db/changelog/123-test-package-rbac.sql index 950acef8..0f071c9e 100644 --- a/src/main/resources/db/changelog/123-test-package-rbac.sql +++ b/src/main/resources/db/changelog/123-test-package-rbac.sql @@ -1,5 +1,5 @@ --liquibase formatted sql --- This code generated was by RbacViewPostgresGenerator at 2024-03-08T08:48:56.148164198. +-- This code generated was by RbacViewPostgresGenerator at 2024-03-08T13:03:39.473061981. -- ============================================================================ --changeset test-package-rbac-OBJECT:1 endDelimiter:--// diff --git a/src/test/java/net/hostsharing/hsadminng/context/ContextBasedTest.java b/src/test/java/net/hostsharing/hsadminng/context/ContextBasedTest.java index a1f4cfbc..7f08f044 100644 --- a/src/test/java/net/hostsharing/hsadminng/context/ContextBasedTest.java +++ b/src/test/java/net/hostsharing/hsadminng/context/ContextBasedTest.java @@ -6,14 +6,31 @@ import org.junit.jupiter.api.TestInfo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Import; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + @Import(RbacGrantsDiagramService.class) public abstract class ContextBasedTest { @Autowired protected Context context; + @PersistenceContext + protected EntityManager em; // just to be used in subclasses + + /** + * To generate a flowchart diagram from the database use something like this in a defined context: + +
+     RbacGrantsDiagramService.writeToFile(
+         "title",
+         diagramService.allGrantsToCurrentUser(of(RbacGrantsDiagramService.Include.USERS, RbacGrantsDiagramService.Include.TEST_ENTITIES, RbacGrantsDiagramService.Include.NOT_ASSUMED, RbacGrantsDiagramService.Include.DETAILS, RbacGrantsDiagramService.Include.PERMISSIONS)),
+         "filename.md
+     );
+    
+ */ @Autowired - protected RbacGrantsDiagramService diagramService; + protected RbacGrantsDiagramService diagramService; // just to be used in subclasses TestInfo test; 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 4ff123d5..7094530e 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityTest.java @@ -36,10 +36,10 @@ class TestCustomerEntityTest { end %% granting roles to users - user:creator ==> role:customer:owner + user:creator ==>|XX| role:customer:owner %% granting roles to roles - role:global:admin ==> role:customer:owner + role:global:admin ==>|XX| role:customer:owner role:customer:owner ==> role:customer:admin role:customer:admin ==> role:customer:tenant diff --git a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java index 01aa0760..27458b14 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java @@ -2,8 +2,6 @@ package net.hostsharing.hsadminng.test.cust; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.ContextBasedTest; -import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService; -import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService.Include; import net.hostsharing.test.JpaAttempt; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -12,14 +10,11 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceException; import jakarta.servlet.http.HttpServletRequest; import java.util.List; import java.util.UUID; -import static java.util.EnumSet.of; import static net.hostsharing.test.JpaAttempt.attempt; import static org.assertj.core.api.Assertions.assertThat; @@ -30,9 +25,6 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest { @Autowired TestCustomerRepository testCustomerRepository; - @PersistenceContext - EntityManager em; - @MockBean HttpServletRequest request; @@ -118,15 +110,15 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest { } @Test - public void globalAdmin_withAssumedglobalAdminRole_canViewAllCustomers() { + public void globalAdmin_withAssumedCustomerOwnerRole_canViewExactlyThatCustomer() { given: - context("superuser-alex@hostsharing.net", "global#global.admin"); + context("superuser-alex@hostsharing.net", "test_customer#yyy.owner"); // when final var result = testCustomerRepository.findCustomerByOptionalPrefixLike(null); then: - allTheseCustomersAreReturned(result, "xxx", "yyy", "zzz"); + allTheseCustomersAreReturned(result, "yyy"); } @Test @@ -144,11 +136,6 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest { @Test public void customerAdmin_withAssumedOwnedPackageAdminRole_canViewOnlyItsOwnCustomer() { context("customer-admin@xxx.example.com"); - RbacGrantsDiagramService.writeToFile( - "customerAdmin_withAssumedOwnedPackageAdminRole_canViewOnlyItsOwnCustomer", - diagramService.allGrantsToCurrentUser(of(Include.USERS, Include.TEST_ENTITIES, Include.NOT_ASSUMED, Include.DETAILS, Include.PERMISSIONS)), - "doc/customerAdmin_withAssumedOwnedPackageAdminRole_canViewOnlyItsOwnCustomer.md" - ); context("customer-admin@xxx.example.com", "test_package#xxx00.admin"); diff --git a/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityTest.java b/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityTest.java index 534da710..392d0274 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityTest.java @@ -35,7 +35,7 @@ class TestPackageEntityTest { perm:package:SELECT{{package:SELECT}} end end - + subgraph customer["`**customer**`"] direction TB style customer fill:#99bcdb,stroke:#274d6e,stroke-width:8px @@ -48,19 +48,16 @@ class TestPackageEntityTest { 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:global:admin -.->|XX| role:customer:owner role:customer:owner -.-> role:customer:admin role:customer:admin -.-> role:customer:tenant - role:customer:admin == /// ==> role:package:owner + 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:DELETE diff --git a/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageRepositoryIntegrationTest.java index 89c6f993..a201d79e 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageRepositoryIntegrationTest.java @@ -1,6 +1,7 @@ package net.hostsharing.hsadminng.test.pac; import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.hsadminng.context.ContextBasedTest; import net.hostsharing.test.JpaAttempt; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -19,10 +20,7 @@ import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest @Import( { Context.class, JpaAttempt.class }) -class TestPackageRepositoryIntegrationTest { - - @Autowired - Context context; +class TestPackageRepositoryIntegrationTest extends ContextBasedTest { @Autowired TestPackageRepository testPackageRepository; @@ -40,9 +38,10 @@ class TestPackageRepositoryIntegrationTest { class FindAllByOptionalNameLike { @Test - public void globalAdmin_withoutAssumedRole_canNotViewAnyPackages_becauseThoseGrantsAreNotassumedd() { + public void globalAdmin_withoutAssumedRole_canNotViewAnyPackages_becauseThoseGrantsAreNotAssumed() { // given - context.define("superuser-alex@hostsharing.net"); + // alex is not just global-admin but lso the creating user, thus we use fran + context.define("superuser-fran@hostsharing.net"); // when final var result = testPackageRepository.findAllByOptionalNameLike(null); @@ -52,7 +51,7 @@ class TestPackageRepositoryIntegrationTest { } @Test - public void globalAdmin_withAssumedglobalAdminRole__canNotViewAnyPackages_becauseThoseGrantsAreNotassumedd() { + public void globalAdmin_withAssumedglobalAdminRole__canNotViewAnyPackages_becauseThoseGrantsAreNotAssumed() { given: context.define("superuser-alex@hostsharing.net", "global#global.admin"); -- 2.39.5 From bbcef53b876d0421dbb2b7c39107d04c8aa90308 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 8 Mar 2024 14:03:56 +0100 Subject: [PATCH 42/53] fix tests in Hostsharing-Office Entity-Tests --- ...ceBankAccountRepositoryIntegrationTest.java | 4 ++-- ...OfficeContactRepositoryIntegrationTest.java | 6 +++--- ...tsTransactionRepositoryIntegrationTest.java | 2 +- ...esTransactionRepositoryIntegrationTest.java | 2 +- ...iceMembershipRepositoryIntegrationTest.java | 6 +++--- ...OfficePartnerRepositoryIntegrationTest.java | 18 +++++++++--------- ...sOfficePersonRepositoryIntegrationTest.java | 6 +++--- ...eRelationshipRepositoryIntegrationTest.java | 6 +++--- ...ceSepaMandateRepositoryIntegrationTest.java | 6 +++--- 9 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java index f2847290..eb14e634 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java @@ -109,7 +109,7 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTestWithC )); assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(Array.fromFormatted( initialGrantNames, - "{ grant perm delete on hs_office_bankaccount#sometempaccC to role hs_office_bankaccount#sometempaccC.owner by system and assume }", + "{ grant perm DELETE on hs_office_bankaccount#sometempaccC to role hs_office_bankaccount#sometempaccC.owner by system and assume }", "{ grant role hs_office_bankaccount#sometempaccC.owner to role global#global.admin by system and assume }", "{ grant role hs_office_bankaccount#sometempaccC.owner to user selfregistered-user-drew@hostsharing.org by global#global.admin and assume }", @@ -117,7 +117,7 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTestWithC "{ grant role hs_office_bankaccount#sometempaccC.tenant to role hs_office_bankaccount#sometempaccC.admin by system and assume }", - "{ grant perm view on hs_office_bankaccount#sometempaccC to role hs_office_bankaccount#sometempaccC.guest by system and assume }", + "{ grant perm SELECT on hs_office_bankaccount#sometempaccC to role hs_office_bankaccount#sometempaccC.guest by system and assume }", "{ grant role hs_office_bankaccount#sometempaccC.guest to role hs_office_bankaccount#sometempaccC.tenant by system and assume }", null )); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRepositoryIntegrationTest.java index a78b761e..91ee8bde 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRepositoryIntegrationTest.java @@ -111,11 +111,11 @@ class HsOfficeContactRepositoryIntegrationTest extends ContextBasedTestWithClean assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(Array.from( initialGrantNames, "{ grant role hs_office_contact#anothernewcontact.owner to role global#global.admin by system and assume }", - "{ grant perm edit on hs_office_contact#anothernewcontact to role hs_office_contact#anothernewcontact.admin by system and assume }", + "{ grant perm UPDATE on hs_office_contact#anothernewcontact to role hs_office_contact#anothernewcontact.admin by system and assume }", "{ grant role hs_office_contact#anothernewcontact.tenant to role hs_office_contact#anothernewcontact.admin by system and assume }", - "{ grant perm * on hs_office_contact#anothernewcontact to role hs_office_contact#anothernewcontact.owner by system and assume }", + "{ grant perm DELETE on hs_office_contact#anothernewcontact to role hs_office_contact#anothernewcontact.owner by system and assume }", "{ grant role hs_office_contact#anothernewcontact.admin to role hs_office_contact#anothernewcontact.owner by system and assume }", - "{ grant perm view on hs_office_contact#anothernewcontact to role hs_office_contact#anothernewcontact.guest by system and assume }", + "{ grant perm SELECT on hs_office_contact#anothernewcontact to role hs_office_contact#anothernewcontact.guest by system and assume }", "{ grant role hs_office_contact#anothernewcontact.guest to role hs_office_contact#anothernewcontact.tenant by system and assume }", "{ grant role hs_office_contact#anothernewcontact.owner to user selfregistered-user-drew@hostsharing.org by global#global.admin and assume }" )); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionRepositoryIntegrationTest.java index f18447df..1f6964b8 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionRepositoryIntegrationTest.java @@ -114,7 +114,7 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase .map(s -> s.replace("hs_office_", "")) .containsExactlyInAnyOrder(Array.fromFormatted( initialGrantNames, - "{ grant perm view on coopassetstransaction#temprefB to role membership#1000101:....tenant by system and assume }", + "{ grant perm SELECT on coopassetstransaction#temprefB to role membership#1000101:....tenant by system and assume }", null)); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java index 20602661..609e7940 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java @@ -113,7 +113,7 @@ class HsOfficeCoopSharesTransactionRepositoryIntegrationTest extends ContextBase .map(s -> s.replace("hs_office_", "")) .containsExactlyInAnyOrder(Array.fromFormatted( initialGrantNames, - "{ grant perm view on coopsharestransaction#temprefB to role membership#1000101:....tenant by system and assume }", + "{ grant perm SELECT on coopsharestransaction#temprefB to role membership#1000101:....tenant by system and assume }", null)); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java index 6a0cd485..4483304a 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java @@ -126,11 +126,11 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl initialGrantNames, // owner - "{ grant perm * on membership#1000117:First to role membership#1000117:First.owner by system and assume }", + "{ grant perm DELETE on membership#1000117:First to role membership#1000117:First.owner by system and assume }", "{ grant role membership#1000117:First.owner to role global#global.admin by system and assume }", // admin - "{ grant perm edit on membership#1000117:First to role membership#1000117:First.admin by system and assume }", + "{ grant perm UPDATE on membership#1000117:First to role membership#1000117:First.admin by system and assume }", "{ grant role membership#1000117:First.admin to role membership#1000117:First.owner by system and assume }", // agent @@ -149,7 +149,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl "{ grant role membership#1000117:First.tenant to role partner#10001:First.agent by system and assume }", // guest - "{ grant perm view on membership#1000117:First to role membership#1000117:First.guest by system and assume }", + "{ grant perm SELECT on membership#1000117:First to role membership#1000117:First.guest by system and assume }", "{ grant role membership#1000117:First.guest to role membership#1000117:First.tenant by system and assume }", "{ grant role membership#1000117:First.guest to role partner#10001:First.tenant by system and assume }", "{ grant role membership#1000117:First.guest to role debitor#1000111:First.tenant by system and assume }", diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java index 2512a07d..f9ed63e4 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java @@ -171,29 +171,29 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean "{ grant role relationship#HostsharingeG-with-PARTNER-EBess.tenant to role person#EBess.admin by system and assume }", "{ grant role relationship#HostsharingeG-with-PARTNER-EBess.owner to role person#HostsharingeG.admin by system and assume }", "{ grant role relationship#HostsharingeG-with-PARTNER-EBess.tenant to role person#HostsharingeG.admin by system and assume }", - "{ grant perm edit on relationship#HostsharingeG-with-PARTNER-EBess to role relationship#HostsharingeG-with-PARTNER-EBess.admin by system and assume }", + "{ grant perm UPDATE on relationship#HostsharingeG-with-PARTNER-EBess to role relationship#HostsharingeG-with-PARTNER-EBess.admin by system and assume }", "{ grant role relationship#HostsharingeG-with-PARTNER-EBess.tenant to role relationship#HostsharingeG-with-PARTNER-EBess.admin by system and assume }", - "{ grant perm * on relationship#HostsharingeG-with-PARTNER-EBess to role relationship#HostsharingeG-with-PARTNER-EBess.owner by system and assume }", + "{ grant perm DELETE on relationship#HostsharingeG-with-PARTNER-EBess to role relationship#HostsharingeG-with-PARTNER-EBess.owner by system and assume }", "{ grant role relationship#HostsharingeG-with-PARTNER-EBess.admin to role relationship#HostsharingeG-with-PARTNER-EBess.owner by system and assume }", - "{ grant perm view on relationship#HostsharingeG-with-PARTNER-EBess to role relationship#HostsharingeG-with-PARTNER-EBess.tenant by system and assume }", + "{ grant perm SELECT on relationship#HostsharingeG-with-PARTNER-EBess to role relationship#HostsharingeG-with-PARTNER-EBess.tenant by system and assume }", "{ grant role contact#4th.tenant to role relationship#HostsharingeG-with-PARTNER-EBess.tenant by system and assume }", "{ grant role person#EBess.tenant to role relationship#HostsharingeG-with-PARTNER-EBess.tenant by system and assume }", "{ grant role person#HostsharingeG.tenant to role relationship#HostsharingeG-with-PARTNER-EBess.tenant by system and assume }", // owner - "{ grant perm * on partner#20032:EBess-4th to role partner#20032:EBess-4th.owner by system and assume }", - "{ grant perm * on partner_details#20032:EBess-4th-details to role partner#20032:EBess-4th.owner by system and assume }", + "{ grant perm DELETE on partner#20032:EBess-4th to role partner#20032:EBess-4th.owner by system and assume }", + "{ grant perm DELETE on partner_details#20032:EBess-4th-details to role partner#20032:EBess-4th.owner by system and assume }", "{ grant role partner#20032:EBess-4th.owner to role global#global.admin by system and assume }", // admin - "{ grant perm edit on partner#20032:EBess-4th to role partner#20032:EBess-4th.admin by system and assume }", - "{ grant perm edit on partner_details#20032:EBess-4th-details to role partner#20032:EBess-4th.admin by system and assume }", + "{ grant perm UPDATE on partner#20032:EBess-4th to role partner#20032:EBess-4th.admin by system and assume }", + "{ grant perm UPDATE on partner_details#20032:EBess-4th-details to role partner#20032:EBess-4th.admin by system and assume }", "{ grant role partner#20032:EBess-4th.admin to role partner#20032:EBess-4th.owner by system and assume }", "{ grant role person#EBess.tenant to role partner#20032:EBess-4th.admin by system and assume }", "{ grant role contact#4th.tenant to role partner#20032:EBess-4th.admin by system and assume }", // agent - "{ grant perm view on partner_details#20032:EBess-4th-details to role partner#20032:EBess-4th.agent by system and assume }", + "{ grant perm SELECT on partner_details#20032:EBess-4th-details to role partner#20032:EBess-4th.agent by system and assume }", "{ grant role partner#20032:EBess-4th.agent to role partner#20032:EBess-4th.admin by system and assume }", "{ grant role partner#20032:EBess-4th.agent to role person#EBess.admin by system and assume }", "{ grant role partner#20032:EBess-4th.agent to role contact#4th.admin by system and assume }", @@ -204,7 +204,7 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean "{ grant role contact#4th.guest to role partner#20032:EBess-4th.tenant by system and assume }", // guest - "{ grant perm view on partner#20032:EBess-4th to role partner#20032:EBess-4th.guest by system and assume }", + "{ grant perm SELECT on partner#20032:EBess-4th to role partner#20032:EBess-4th.guest by system and assume }", "{ grant role partner#20032:EBess-4th.guest to role partner#20032:EBess-4th.tenant by system and assume }", null))); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonRepositoryIntegrationTest.java index dd3e08c9..d3da9ada 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonRepositoryIntegrationTest.java @@ -113,11 +113,11 @@ class HsOfficePersonRepositoryIntegrationTest extends ContextBasedTestWithCleanu Array.from( initialGrantNames, "{ grant role hs_office_person#anothernewperson.owner to role global#global.admin by system and assume }", - "{ grant perm edit on hs_office_person#anothernewperson to role hs_office_person#anothernewperson.admin by system and assume }", + "{ grant perm UPDATE on hs_office_person#anothernewperson to role hs_office_person#anothernewperson.admin by system and assume }", "{ grant role hs_office_person#anothernewperson.tenant to role hs_office_person#anothernewperson.admin by system and assume }", - "{ grant perm * on hs_office_person#anothernewperson to role hs_office_person#anothernewperson.owner by system and assume }", + "{ grant perm DELETE on hs_office_person#anothernewperson to role hs_office_person#anothernewperson.owner by system and assume }", "{ grant role hs_office_person#anothernewperson.admin to role hs_office_person#anothernewperson.owner by system and assume }", - "{ grant perm view on hs_office_person#anothernewperson to role hs_office_person#anothernewperson.guest by system and assume }", + "{ grant perm SELECT on hs_office_person#anothernewperson to role hs_office_person#anothernewperson.guest by system and assume }", "{ grant role hs_office_person#anothernewperson.guest to role hs_office_person#anothernewperson.tenant by system and assume }", "{ grant role hs_office_person#anothernewperson.owner to user selfregistered-user-drew@hostsharing.org by global#global.admin and assume }" )); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipRepositoryIntegrationTest.java index 8d89479c..46d60a40 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipRepositoryIntegrationTest.java @@ -115,14 +115,14 @@ class HsOfficeRelationshipRepositoryIntegrationTest extends ContextBasedTestWith assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(Array.fromFormatted( initialGrantNames, - "{ grant perm * on hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita to role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.owner by system and assume }", + "{ grant perm DELETE on hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita to role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.owner by system and assume }", "{ grant role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.owner to role global#global.admin by system and assume }", "{ grant role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.owner to role hs_office_person#BesslerAnita.admin by system and assume }", - "{ grant perm edit on hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita to role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.admin by system and assume }", + "{ grant perm UPDATE on hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita to role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.admin by system and assume }", "{ grant role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.admin to role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.owner by system and assume }", - "{ grant perm view on hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita to role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.tenant by system and assume }", + "{ grant perm SELECT on hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita to role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.tenant by system and assume }", "{ grant role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.tenant to role hs_office_contact#fourthcontact.admin by system and assume }", "{ grant role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.tenant to role hs_office_person#BesslerAnita.admin by system and assume }", diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java index 04b5b5cf..79910d28 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java @@ -131,11 +131,11 @@ class HsOfficeSepaMandateRepositoryIntegrationTest extends ContextBasedTestWithC initialGrantNames, // owner - "{ grant perm * on sepamandate#temprefB to role sepamandate#temprefB.owner by system and assume }", + "{ grant perm DELETE on sepamandate#temprefB to role sepamandate#temprefB.owner by system and assume }", "{ grant role sepamandate#temprefB.owner to role global#global.admin by system and assume }", // admin - "{ grant perm edit on sepamandate#temprefB to role sepamandate#temprefB.admin by system and assume }", + "{ grant perm UPDATE on sepamandate#temprefB to role sepamandate#temprefB.admin by system and assume }", "{ grant role sepamandate#temprefB.admin to role sepamandate#temprefB.owner by system and assume }", "{ grant role bankaccount#Paul....tenant to role sepamandate#temprefB.admin by system and assume }", @@ -151,7 +151,7 @@ class HsOfficeSepaMandateRepositoryIntegrationTest extends ContextBasedTestWithC "{ grant role bankaccount#Paul....guest to role sepamandate#temprefB.tenant by system and assume }", // guest - "{ grant perm view on sepamandate#temprefB to role sepamandate#temprefB.guest by system and assume }", + "{ grant perm SELECT on sepamandate#temprefB to role sepamandate#temprefB.guest by system and assume }", "{ grant role sepamandate#temprefB.guest to role sepamandate#temprefB.tenant by system and assume }", null)); } -- 2.39.5 From 7fab1186ed0914419c7fb3b8bb3dd91061a1d043 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 8 Mar 2024 14:51:04 +0100 Subject: [PATCH 43/53] WIP trying to fix Debitor RBAC system --- .../hs/office/debitor/HsOfficeDebitorController.java | 1 + src/main/resources/db/changelog/050-rbac-base.sql | 6 ++++-- src/main/resources/db/changelog/057-rbac-role-builder.sql | 5 +++-- .../debitor/HsOfficeDebitorControllerAcceptanceTest.java | 3 +-- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorController.java index bc4175ca..91e2785c 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorController.java @@ -62,6 +62,7 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi { final var entityToSave = mapper.map(body, HsOfficeDebitorEntity.class); final var saved = debitorRepo.save(entityToSave); + em.flush(); // FIXME: remove final var uri = MvcUriComponentsBuilder.fromController(getClass()) diff --git a/src/main/resources/db/changelog/050-rbac-base.sql b/src/main/resources/db/changelog/050-rbac-base.sql index f3b784c8..2d851197 100644 --- a/src/main/resources/db/changelog/050-rbac-base.sql +++ b/src/main/resources/db/changelog/050-rbac-base.sql @@ -300,15 +300,17 @@ create or replace function getRoleId(roleDescriptor RbacRoleDescriptor, whenNotE declare roleUuid uuid; begin - roleUuid = findRoleId(roleDescriptor); + roleUuid := findRoleId(roleDescriptor); + assert roleUuid is not null, 'roleUuid must not be null'; -- FIXME: remove if (roleUuid is null) then if (whenNotExists = 'fail') then raise exception 'RbacRole "%#%.%" not found', roleDescriptor.objectTable, roleDescriptor.objectUuid, roleDescriptor.roleType; end if; if (whenNotExists = 'create') then - roleUuid = createRole(roleDescriptor); + roleUuid := createRole(roleDescriptor); end if; end if; + assert roleUuid is not null, 'roleUuid must not be null'; -- FIXME: remove return roleUuid; end; $$; diff --git a/src/main/resources/db/changelog/057-rbac-role-builder.sql b/src/main/resources/db/changelog/057-rbac-role-builder.sql index 221919a3..49975123 100644 --- a/src/main/resources/db/changelog/057-rbac-role-builder.sql +++ b/src/main/resources/db/changelog/057-rbac-role-builder.sql @@ -47,13 +47,14 @@ begin foreach superRoleDesc in array incomingSuperRoles loop - superRoleUuid = getRoleId(superRoleDesc, 'fail'); + superRoleUuid := getRoleId(superRoleDesc, 'fail'); call grantRoleToRole(roleUuid, superRoleUuid, superRoleDesc.assumed); end loop; foreach subRoleDesc in array outgoingSubRoles loop - subRoleUuid = getRoleId(subRoleDesc, 'fail'); + subRoleUuid := getRoleId(subRoleDesc, 'fail'); + assert subRoleUuid is not null, 'subRoleUuid must not be null'; -- FIXME: remove call grantRoleToRole(subRoleUuid, roleUuid, subRoleDesc.assumed); end loop; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java index 839039a2..0616e338 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java @@ -145,8 +145,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu } @Nested - @Accepts({ "Debitor:C(Create)" }) - class CreateDebitor { + class AddDebitor { @Test void globalAdmin_withoutAssumedRole_canAddDebitorWithBankAccount() { -- 2.39.5 From c2ad5a7e28bea2d37f8cc9a4f45eae410c060e98 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 8 Mar 2024 16:04:58 +0100 Subject: [PATCH 44/53] fix Debitor RBAC system --- .../debitor/HsOfficeDebitorController.java | 1 - .../resources/db/changelog/050-rbac-base.sql | 37 +----- .../db/changelog/057-rbac-role-builder.sql | 11 +- .../changelog/118-test-customer-test-data.sql | 4 +- .../changelog/128-test-package-test-data.sql | 2 +- .../changelog/233-hs-office-partner-rbac.sql | 6 +- .../313-hs-office-coopshares-rbac.sql | 2 +- .../323-hs-office-coopassets-rbac.sql | 2 +- .../hsadminng/arch/ArchitectureTest.java | 7 +- ...fficeDebitorRepositoryIntegrationTest.java | 8 +- ...acGrantsDiagramServiceIntegrationTest.java | 106 ++++++------------ ...t.java => TestCustomerEntityUnitTest.java} | 2 +- ...st.java => TestPackageEntityUnitTest.java} | 2 +- 13 files changed, 64 insertions(+), 126 deletions(-) rename src/test/java/net/hostsharing/hsadminng/test/cust/{TestCustomerEntityTest.java => TestCustomerEntityUnitTest.java} (98%) rename src/test/java/net/hostsharing/hsadminng/test/pac/{TestPackageEntityTest.java => TestPackageEntityUnitTest.java} (98%) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorController.java index 91e2785c..bc4175ca 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorController.java @@ -62,7 +62,6 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi { final var entityToSave = mapper.map(body, HsOfficeDebitorEntity.class); final var saved = debitorRepo.save(entityToSave); - em.flush(); // FIXME: remove final var uri = MvcUriComponentsBuilder.fromController(getClass()) diff --git a/src/main/resources/db/changelog/050-rbac-base.sql b/src/main/resources/db/changelog/050-rbac-base.sql index 2d851197..4e63700f 100644 --- a/src/main/resources/db/changelog/050-rbac-base.sql +++ b/src/main/resources/db/changelog/050-rbac-base.sql @@ -86,29 +86,6 @@ create or replace function findRbacUserId(userName varchar) language sql as $$ select uuid from RbacUser where name = userName $$; - -create type RbacWhenNotExists as enum ('fail', 'create'); - -create or replace function getRbacUserId(userName varchar, whenNotExists RbacWhenNotExists) - returns uuid - returns null on null input - language plpgsql as $$ -declare - userUuid uuid; -begin - userUuid = findRbacUserId(userName); - if (userUuid is null) then - if (whenNotExists = 'fail') then - raise exception 'RbacUser with name="%" not found', userName; - end if; - if (whenNotExists = 'create') then - userUuid = createRbacUser(userName); - end if; - end if; - return userUuid; -end; -$$; - --// -- ============================================================================ @@ -293,24 +270,18 @@ create or replace function findRoleId(roleDescriptor RbacRoleDescriptor) select uuid from RbacRole where objectUuid = roleDescriptor.objectUuid and roleType = roleDescriptor.roleType; $$; -create or replace function getRoleId(roleDescriptor RbacRoleDescriptor, whenNotExists RbacWhenNotExists) +create or replace function getRoleId(roleDescriptor RbacRoleDescriptor) returns uuid - returns null on null input language plpgsql as $$ declare roleUuid uuid; begin + assert roleDescriptor is not null, 'roleDescriptor must not be null'; + roleUuid := findRoleId(roleDescriptor); - assert roleUuid is not null, 'roleUuid must not be null'; -- FIXME: remove if (roleUuid is null) then - if (whenNotExists = 'fail') then - raise exception 'RbacRole "%#%.%" not found', roleDescriptor.objectTable, roleDescriptor.objectUuid, roleDescriptor.roleType; - end if; - if (whenNotExists = 'create') then - roleUuid := createRole(roleDescriptor); - end if; + raise exception 'RbacRole "%#%.%" not found', roleDescriptor.objectTable, roleDescriptor.objectUuid, roleDescriptor.roleType; end if; - assert roleUuid is not null, 'roleUuid must not be null'; -- FIXME: remove return roleUuid; end; $$; diff --git a/src/main/resources/db/changelog/057-rbac-role-builder.sql b/src/main/resources/db/changelog/057-rbac-role-builder.sql index 49975123..1a7da953 100644 --- a/src/main/resources/db/changelog/057-rbac-role-builder.sql +++ b/src/main/resources/db/changelog/057-rbac-role-builder.sql @@ -45,16 +45,15 @@ begin call grantPermissionsToRole(roleUuid, toPermissionUuids(roleDescriptor.objectuuid, permissions)); end if; - foreach superRoleDesc in array incomingSuperRoles + foreach superRoleDesc in array array_remove(incomingSuperRoles, null) loop - superRoleUuid := getRoleId(superRoleDesc, 'fail'); + superRoleUuid := getRoleId(superRoleDesc); call grantRoleToRole(roleUuid, superRoleUuid, superRoleDesc.assumed); end loop; - foreach subRoleDesc in array outgoingSubRoles + foreach subRoleDesc in array array_remove(outgoingSubRoles, null) loop - subRoleUuid := getRoleId(subRoleDesc, 'fail'); - assert subRoleUuid is not null, 'subRoleUuid must not be null'; -- FIXME: remove + subRoleUuid := getRoleId(subRoleDesc); call grantRoleToRole(subRoleUuid, roleUuid, subRoleDesc.assumed); end loop; @@ -62,7 +61,7 @@ begin if grantedByRole is null then grantedByRoleUuid := roleUuid; else - grantedByRoleUuid := getRoleId(grantedByRole, 'fail'); + grantedByRoleUuid := getRoleId(grantedByRole); end if; foreach userUuid in array userUuids loop diff --git a/src/main/resources/db/changelog/118-test-customer-test-data.sql b/src/main/resources/db/changelog/118-test-customer-test-data.sql index 1e239001..85c34ac6 100644 --- a/src/main/resources/db/changelog/118-test-customer-test-data.sql +++ b/src/main/resources/db/changelog/118-test-customer-test-data.sql @@ -46,8 +46,8 @@ begin select * into newCust from test_customer where reference=custReference; call grantRoleToUser( - getRoleId(testCustomerOwner(newCust), 'fail'), - getRoleId(testCustomerAdmin(newCust), 'fail'), + getRoleId(testCustomerOwner(newCust)), + getRoleId(testCustomerAdmin(newCust)), custAdminUuid, true); end; $$; diff --git a/src/main/resources/db/changelog/128-test-package-test-data.sql b/src/main/resources/db/changelog/128-test-package-test-data.sql index 8c6568f3..9abba772 100644 --- a/src/main/resources/db/changelog/128-test-package-test-data.sql +++ b/src/main/resources/db/changelog/128-test-package-test-data.sql @@ -35,7 +35,7 @@ begin returning * into pac; call grantRoleToUser( - getRoleId(testCustomerAdmin(cust), 'fail'), + getRoleId(testCustomerAdmin(cust)), findRoleId(testPackageAdmin(pac)), createRbacUser('pac-admin-' || pacName || '@' || cust.prefix || '.example.com'), true); diff --git a/src/main/resources/db/changelog/233-hs-office-partner-rbac.sql b/src/main/resources/db/changelog/233-hs-office-partner-rbac.sql index a6ad3733..e7634d46 100644 --- a/src/main/resources/db/changelog/233-hs-office-partner-rbac.sql +++ b/src/main/resources/db/changelog/233-hs-office-partner-rbac.sql @@ -98,12 +98,12 @@ begin --Attention: Cannot be in partner-details because of insert order (partner is not in database yet) call grantPermissionsToRole( - getRoleId(hsOfficePartnerOwner(NEW), 'fail'), + getRoleId(hsOfficePartnerOwner(NEW)), createPermissions(NEW.detailsUuid, array ['DELETE']) ); call grantPermissionsToRole( - getRoleId(hsOfficePartnerAdmin(NEW), 'fail'), + getRoleId(hsOfficePartnerAdmin(NEW)), createPermissions(NEW.detailsUuid, array ['UPDATE']) ); @@ -111,7 +111,7 @@ begin -- Yes, here hsOfficePartnerAGENT is used, not hsOfficePartnerTENANT. -- Do NOT grant view permission on partner-details to hsOfficePartnerTENANT! -- Otherwise package-admins etc. would be able to read the data. - getRoleId(hsOfficePartnerAgent(NEW), 'fail'), + getRoleId(hsOfficePartnerAgent(NEW)), createPermissions(NEW.detailsUuid, array ['SELECT']) ); diff --git a/src/main/resources/db/changelog/313-hs-office-coopshares-rbac.sql b/src/main/resources/db/changelog/313-hs-office-coopshares-rbac.sql index a79d354e..5082a3ca 100644 --- a/src/main/resources/db/changelog/313-hs-office-coopshares-rbac.sql +++ b/src/main/resources/db/changelog/313-hs-office-coopshares-rbac.sql @@ -42,7 +42,7 @@ begin -- coopsharestransactions cannot be edited nor deleted, just created+viewed call grantPermissionsToRole( - getRoleId(hsOfficeMembershipTenant(newHsOfficeMembership), 'fail'), + getRoleId(hsOfficeMembershipTenant(newHsOfficeMembership)), createPermissions(NEW.uuid, array ['SELECT']) ); diff --git a/src/main/resources/db/changelog/323-hs-office-coopassets-rbac.sql b/src/main/resources/db/changelog/323-hs-office-coopassets-rbac.sql index 38fec4ff..6fbdc5ce 100644 --- a/src/main/resources/db/changelog/323-hs-office-coopassets-rbac.sql +++ b/src/main/resources/db/changelog/323-hs-office-coopassets-rbac.sql @@ -42,7 +42,7 @@ begin -- coopassetstransactions cannot be edited nor deleted, just created+viewed call grantPermissionsToRole( - getRoleId(hsOfficeMembershipTenant(newHsOfficeMembership), 'fail'), + getRoleId(hsOfficeMembershipTenant(newHsOfficeMembership)), createPermissions(NEW.uuid, array ['SELECT']) ); diff --git a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java index fe50ccf1..82856124 100644 --- a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java +++ b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java @@ -49,6 +49,8 @@ public class ArchitectureTest { "..rbac.rbacuser", "..rbac.rbacgrant", "..rbac.rbacrole", + "..rbac.rbacobject", + "..rbac.rbacdef", "..stringify" // ATTENTION: Don't simply add packages here, also add arch rules for the new package! ); @@ -116,7 +118,10 @@ public class ArchitectureTest { public static final ArchRule hsAdminPackagesRule = classes() .that().resideInAPackage("..hs.office.(*)..") .should().onlyBeAccessed().byClassesThat() - .resideInAnyPackage("..hs.office.(*).."); + .resideInAnyPackage( + "..hs.office.(*)..", + "..rbac.rbacgrant" // TODO: just because of RbacGrantsDiagramServiceIntegrationTest + ); @ArchTest @SuppressWarnings("unused") diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java index c703c31a..7c2420ee 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java @@ -119,7 +119,7 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean // then System.out.println("ok"); -// result.assertExceptionWithRootCauseMessage(org.hibernate.exception.ConstraintViolationException.class); + result.assertExceptionWithRootCauseMessage(org.hibernate.exception.ConstraintViolationException.class); } @Test @@ -167,12 +167,12 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean .containsExactlyInAnyOrder(Array.fromFormatted( initialGrantNames, // owner - "{ grant perm * on debitor#1000422:FeG to role debitor#1000422:FeG.owner by system and assume }", + "{ grant perm DELETE on debitor#1000422:FeG to role debitor#1000422:FeG.owner by system and assume }", "{ grant role debitor#1000422:FeG.owner to role global#global.admin by system and assume }", "{ grant role debitor#1000422:FeG.owner to user superuser-alex by global#global.admin and assume }", // admin - "{ grant perm edit on debitor#1000422:FeG to role debitor#1000422:FeG.admin by system and assume }", + "{ grant perm UPDATE on debitor#1000422:FeG to role debitor#1000422:FeG.admin by system and assume }", "{ grant role debitor#1000422:FeG.admin to role debitor#1000422:FeG.owner by system and assume }", // agent @@ -187,7 +187,7 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean "{ grant role partner#10004:FeG.tenant to role debitor#1000422:FeG.tenant by system and assume }", // guest - "{ grant perm view on debitor#1000422:FeG to role debitor#1000422:FeG.guest by system and assume }", + "{ grant perm SELECT on debitor#1000422:FeG to role debitor#1000422:FeG.guest by system and assume }", "{ grant role debitor#1000422:FeG.guest to role debitor#1000422:FeG.tenant by system and assume }", null)); diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramServiceIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramServiceIntegrationTest.java index 442f4979..0ed953ca 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramServiceIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramServiceIntegrationTest.java @@ -4,7 +4,10 @@ import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.office.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService.Include; import net.hostsharing.test.JpaAttempt; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.mock.mockito.MockBean; @@ -28,6 +31,27 @@ class RbacGrantsDiagramServiceIntegrationTest extends ContextBasedTestWithCleanu @MockBean HttpServletRequest request; + @Autowired + Context context; + + @Autowired + RbacGrantsDiagramService diagramService; + + TestInfo test; + + @BeforeEach + void init(TestInfo testInfo) { + this.test = testInfo; + } + + protected void context(final String currentUser, final String assumedRoles) { + context.define(test.getDisplayName(), null, currentUser, assumedRoles); + } + + protected void context(final String currentUser) { + context(currentUser, null); + } + @Test void allGrantsToCurrentUser() { context("superuser-alex@hostsharing.net", "test_domain#xxx00-aaaa.owner"); @@ -36,27 +60,9 @@ class RbacGrantsDiagramServiceIntegrationTest extends ContextBasedTestWithCleanu assertThat(graph).isEqualTo(""" flowchart TB - role:test_package#xxx00.tenant[ - test_package - xxx00.t - tenant] --> role:test_customer#xxx.tenant[ - test_customer - xxx.t - tenant] - role:test_domain#xxx00-aaaa.owner[ - test_domain - xxx00-aaaa.o - owner] --> role:test_domain#xxx00-aaaa.admin[ - test_domain - xxx00-aaaa.a - admin] - role:test_domain#xxx00-aaaa.admin[ - test_domain - xxx00-aaaa.a - admin] --> role:test_package#xxx00.tenant[ - test_package - xxx00.t - tenant] + role:test_domain#xxx00-aaaa.admin --> role:test_package#xxx00.tenant + role:test_domain#xxx00-aaaa.owner --> role:test_domain#xxx00-aaaa.admin + role:test_package#xxx00.tenant --> role:test_customer#xxx.tenant """.trim()); } @@ -68,60 +74,18 @@ class RbacGrantsDiagramServiceIntegrationTest extends ContextBasedTestWithCleanu assertThat(graph).isEqualTo(""" flowchart TB - role:test_domain#xxx00-aaaa.owner[ - test_domain - xxx00-aaaa.o - owner] --> perm:*:on:test_domain#xxx00-aaaa{{ - test_domain - xxx00-aaaa - *}} - role:test_customer#xxx.tenant[ - test_customer - xxx.t - tenant] --> perm:view:on:test_customer#xxx{{ - test_customer - xxx - view}} - role:test_domain#xxx00-aaaa.admin[ - test_domain - xxx00-aaaa.a - admin] --> perm:edit:on:test_domain#xxx00-aaaa{{ - test_domain - xxx00-aaaa - edit}} - role:test_package#xxx00.tenant[ - test_package - xxx00.t - tenant] --> role:test_customer#xxx.tenant[ - test_customer - xxx.t - tenant] - role:test_domain#xxx00-aaaa.owner[ - test_domain - xxx00-aaaa.o - owner] --> role:test_domain#xxx00-aaaa.admin[ - test_domain - xxx00-aaaa.a - admin] - role:test_package#xxx00.tenant[ - test_package - xxx00.t - tenant] --> perm:view:on:test_package#xxx00{{ - test_package - xxx00 - view}} - role:test_domain#xxx00-aaaa.admin[ - test_domain - xxx00-aaaa.a - admin] --> role:test_package#xxx00.tenant[ - test_package - xxx00.t - tenant] + role:test_customer#xxx.tenant --> perm:SELECT:on:test_customer#xxx + role:test_domain#xxx00-aaaa.admin --> perm:UPDATE:on:test_domain#xxx00-aaaa + role:test_domain#xxx00-aaaa.admin --> role:test_package#xxx00.tenant + role:test_domain#xxx00-aaaa.owner --> perm:DELETE:on:test_domain#xxx00-aaaa + role:test_domain#xxx00-aaaa.owner --> role:test_domain#xxx00-aaaa.admin + role:test_package#xxx00.tenant --> perm:SELECT:on:test_package#xxx00 + role:test_package#xxx00.tenant --> role:test_customer#xxx.tenant """.trim()); } @Test -// @Disabled + @Disabled // enable to generate from a real database void print() throws IOException { //context("superuser-alex@hostsharing.net", "hs_office_person#FirbySusan.admin"); context("superuser-alex@hostsharing.net"); diff --git a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityTest.java b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityUnitTest.java similarity index 98% rename from src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityTest.java rename to src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityUnitTest.java index 7094530e..4dfcc183 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityUnitTest.java @@ -5,7 +5,7 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; -class TestCustomerEntityTest { +class TestCustomerEntityUnitTest { @Test void definesRbac() { diff --git a/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityTest.java b/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityUnitTest.java similarity index 98% rename from src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityTest.java rename to src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityUnitTest.java index 392d0274..2546a8e8 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityUnitTest.java @@ -5,7 +5,7 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; -class TestPackageEntityTest { +class TestPackageEntityUnitTest { @Test void definesRbac() { -- 2.39.5 From d71d0215ec916b222b16e75c63e9922257ca2966 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 8 Mar 2024 18:00:40 +0100 Subject: [PATCH 45/53] fix RbacOp check --- .../resources/db/changelog/050-rbac-base.sql | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/main/resources/db/changelog/050-rbac-base.sql b/src/main/resources/db/changelog/050-rbac-base.sql index 4e63700f..e27bd907 100644 --- a/src/main/resources/db/changelog/050-rbac-base.sql +++ b/src/main/resources/db/changelog/050-rbac-base.sql @@ -357,25 +357,23 @@ create trigger deleteRbacRolesOfRbacObject_Trigger */ create domain RbacOp as varchar(67) -- TODO: shorten to 8, once the deprecated values are gone --- FIXME: --- check ( --- VALUE = 'DELETE' --- or VALUE = 'UPDATE' --- or VALUE = 'SELECT' --- or VALUE = 'INSERT' --- or VALUE = 'ASSUME' --- -- TODO: all values below are deprecated, use insert with table --- or VALUE ~ '^add-[a-z]+$' --- or VALUE ~ '^new-[a-z-]+$' --- ) -; + check ( + VALUE = 'DELETE' + or VALUE = 'UPDATE' + or VALUE = 'SELECT' + or VALUE = 'INSERT' + or VALUE = 'ASSUME' + -- TODO: all values below are deprecated, use insert with table + or VALUE ~ '^add-[a-z]+$' + or VALUE ~ '^new-[a-z-]+$' + ); create table RbacPermission ( uuid uuid primary key references RbacReference (uuid) on delete cascade, objectUuid uuid not null references RbacObject, op RbacOp not null, - opTableName RbacOp, + opTableName varchar(60), unique (objectUuid, op) ); -- 2.39.5 From eb6b56e4761e849112d370c0e4fac0dbba02de18 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 8 Mar 2024 19:46:35 +0100 Subject: [PATCH 46/53] fix Rbac most Rbac Integration-Tests (view->SELECT etc.) --- .../rbac/rbacuser/RbacUserPermission.java | 2 +- .../db/changelog/051-rbac-user-grant.sql | 2 +- .../resources/db/changelog/055-rbac-views.sql | 8 +- .../RbacGrantControllerAcceptanceTest.java | 6 +- .../RbacGrantRepositoryIntegrationTest.java | 6 +- .../RbacUserControllerAcceptanceTest.java | 35 ++-- .../RbacUserRepositoryIntegrationTest.java | 166 +++++++++--------- 7 files changed, 106 insertions(+), 119 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserPermission.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserPermission.java index ba251885..f29503c3 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserPermission.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserPermission.java @@ -8,8 +8,8 @@ public interface RbacUserPermission { String getRoleName(); UUID getPermissionUuid(); String getOp(); + String getOpTableName(); String getObjectTable(); String getObjectIdName(); UUID getObjectUuid(); - } diff --git a/src/main/resources/db/changelog/051-rbac-user-grant.sql b/src/main/resources/db/changelog/051-rbac-user-grant.sql index b71869f9..beeeb7d2 100644 --- a/src/main/resources/db/changelog/051-rbac-user-grant.sql +++ b/src/main/resources/db/changelog/051-rbac-user-grant.sql @@ -58,7 +58,7 @@ begin select roleIdName from rbacRole_ev where uuid=grantedByRoleUuid into grantedByRoleIdName; select roleIdName from rbacRole_ev where uuid=grantedRoleUuid into grantedRoleIdName; raise exception '[403] Access to granted role % (%) forbidden for % (%)', - grantedRoleIdName, grantedRoleUuid, grantedByRoleUuid, grantedByRoleIdName; + grantedRoleIdName, grantedRoleUuid, grantedByRoleIdName, grantedByRoleUuid; end if; insert diff --git a/src/main/resources/db/changelog/055-rbac-views.sql b/src/main/resources/db/changelog/055-rbac-views.sql index b1757c56..cd1ff9fb 100644 --- a/src/main/resources/db/changelog/055-rbac-views.sql +++ b/src/main/resources/db/changelog/055-rbac-views.sql @@ -341,7 +341,7 @@ grant all privileges on RbacOwnGrantedPermissions_rv to ${HSADMINNG_POSTGRES_RES */ create or replace function grantedPermissions(targetUserUuid uuid) - returns table(roleUuid uuid, roleName text, permissionUuid uuid, op RbacOp, objectTable varchar, objectIdName varchar, objectUuid uuid) + returns table(roleUuid uuid, roleName text, permissionUuid uuid, op RbacOp, opTableName varchar(60), objectTable varchar(60), objectIdName varchar, objectUuid uuid) returns null on null input language plpgsql as $$ declare @@ -357,11 +357,13 @@ begin return query select xp.roleUuid, (xp.roleObjectTable || '#' || xp.roleObjectIdName || '.' || xp.roleType) as roleName, - xp.permissionUuid, xp.op, xp.permissionObjectTable, xp.permissionObjectIdName, xp.permissionObjectUuid + xp.permissionUuid, xp.op, xp.opTableName, + xp.permissionObjectTable, xp.permissionObjectIdName, xp.permissionObjectUuid from (select r.uuid as roleUuid, r.roletype, ro.objectTable as roleObjectTable, findIdNameByObjectUuid(ro.objectTable, ro.uuid) as roleObjectIdName, - p.uuid as permissionUuid, p.op, po.objecttable as permissionObjectTable, + p.uuid as permissionUuid, p.op, p.opTableName, + po.objecttable as permissionObjectTable, findIdNameByObjectUuid(po.objectTable, po.uuid) as permissionObjectIdName, po.uuid as permissionObjectUuid from queryPermissionsGrantedToSubjectId( targetUserUuid) as p diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantControllerAcceptanceTest.java index 6f0abc93..fdf7e693 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantControllerAcceptanceTest.java @@ -73,14 +73,14 @@ class RbacGrantControllerAcceptanceTest extends ContextBasedTest { .contentType("application/json") .body("", hasItem( allOf( - hasEntry("grantedByRoleIdName", "global#global.admin"), + hasEntry("grantedByRoleIdName", "test_customer#xxx.owner"), hasEntry("grantedRoleIdName", "test_customer#xxx.admin"), hasEntry("granteeUserName", "customer-admin@xxx.example.com") ) )) .body("", hasItem( allOf( - hasEntry("grantedByRoleIdName", "global#global.admin"), + hasEntry("grantedByRoleIdName", "test_customer#yyy.owner"), hasEntry("grantedRoleIdName", "test_customer#yyy.admin"), hasEntry("granteeUserName", "customer-admin@yyy.example.com") ) @@ -296,7 +296,7 @@ class RbacGrantControllerAcceptanceTest extends ContextBasedTest { result.assertThat() .statusCode(403) .body("message", containsString("Access to granted role")) - .body("message", containsString("forbidden for {test_package#xxx00.admin}")); + .body("message", containsString("forbidden for test_package#xxx00.admin")); assertThat(findAllGrantsOf(givenCurrentUserAsPackageAdmin)) .extracting(RbacGrantEntity::getGranteeUserName) .doesNotContain(givenNewUser.getName()); diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantRepositoryIntegrationTest.java index 3b09e861..8ce615b7 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantRepositoryIntegrationTest.java @@ -84,7 +84,7 @@ class RbacGrantRepositoryIntegrationTest extends ContextBasedTest { // then exactlyTheseRbacGrantsAreReturned( result, - "{ grant role test_customer#xxx.admin to user customer-admin@xxx.example.com by role global#global.admin and assume }", + "{ grant role test_customer#xxx.admin to user customer-admin@xxx.example.com by role test_customer#xxx.owner and assume }", "{ grant role test_package#xxx00.admin to user pac-admin-xxx00@xxx.example.com by role test_customer#xxx.admin and assume }", "{ grant role test_package#xxx01.admin to user pac-admin-xxx01@xxx.example.com by role test_customer#xxx.admin and assume }", "{ grant role test_package#xxx02.admin to user pac-admin-xxx02@xxx.example.com by role test_customer#xxx.admin and assume }"); @@ -162,8 +162,8 @@ class RbacGrantRepositoryIntegrationTest extends ContextBasedTest { // then attempt.assertExceptionWithRootCauseMessage( JpaSystemException.class, - "ERROR: [403] Access to granted role " + given.packageOwnerRoleUuid - + " forbidden for {test_package#xxx00.admin}"); + "ERROR: [403] Access to granted role test_package#xxx00.owner", + "forbidden for test_package#xxx00.admin"); jpaAttempt.transacted(() -> { // finally, we use the new user to make sure, no roles were granted context(given.arbitraryUser.getName(), null); diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerAcceptanceTest.java index aca26fe4..b2620537 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerAcceptanceTest.java @@ -288,19 +288,14 @@ class RbacUserControllerAcceptanceTest { .body("", hasItem( allOf( hasEntry("roleName", "test_customer#yyy.tenant"), - hasEntry("op", "select")) - )) - .body("", hasItem( - allOf( - hasEntry("roleName", "test_package#yyy00.admin"), - hasEntry("op", "add-domain")) + hasEntry("op", "SELECT")) )) .body("", hasItem( allOf( hasEntry("roleName", "test_domain#yyy00-aaaa.owner"), - hasEntry("op", "delete")) + hasEntry("op", "DELETE")) )) - .body("size()", is(7)); + .body("size()", is(6)); // @formatter:on } @@ -313,7 +308,7 @@ class RbacUserControllerAcceptanceTest { RestAssured .given() .header("current-user", "superuser-alex@hostsharing.net") - .header("assumed-roles", "test_package#yyy00.admin") + .header("assumed-roles", "test_customer#yyy.admin") .port(port) .when() .get("http://localhost/api/rbac/users/" + givenUser.getUuid() + "/permissions") @@ -323,19 +318,14 @@ class RbacUserControllerAcceptanceTest { .body("", hasItem( allOf( hasEntry("roleName", "test_customer#yyy.tenant"), - hasEntry("op", "select")) - )) - .body("", hasItem( - allOf( - hasEntry("roleName", "test_package#yyy00.admin"), - hasEntry("op", "add-domain")) + hasEntry("op", "SELECT")) )) .body("", hasItem( allOf( hasEntry("roleName", "test_domain#yyy00-aaaa.owner"), - hasEntry("op", "delete")) + hasEntry("op", "DELETE")) )) - .body("size()", is(7)); + .body("size()", is(6)); // @formatter:on } @@ -357,19 +347,14 @@ class RbacUserControllerAcceptanceTest { .body("", hasItem( allOf( hasEntry("roleName", "test_customer#yyy.tenant"), - hasEntry("op", "select")) - )) - .body("", hasItem( - allOf( - hasEntry("roleName", "test_package#yyy00.admin"), - hasEntry("op", "add-domain")) + hasEntry("op", "SELECT")) )) .body("", hasItem( allOf( hasEntry("roleName", "test_domain#yyy00-aaaa.owner"), - hasEntry("op", "delete")) + hasEntry("op", "DELETE")) )) - .body("size()", is(7)); + .body("size()", is(6)); // @formatter:on } diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java index ea0a3109..e5b74ccb 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java @@ -183,47 +183,47 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { // @formatter:off "global#global.admin -> global#global: add-customer", - "test_customer#xxx.admin -> test_customer#xxx: add-package", - "test_customer#xxx.admin -> test_customer#xxx: view", - "test_customer#xxx.owner -> test_customer#xxx: *", - "test_customer#xxx.tenant -> test_customer#xxx: view", - "test_package#xxx00.admin -> test_package#xxx00: add-domain", - "test_package#xxx00.admin -> test_package#xxx00: add-domain", - "test_package#xxx00.tenant -> test_package#xxx00: view", - "test_package#xxx01.admin -> test_package#xxx01: add-domain", - "test_package#xxx01.admin -> test_package#xxx01: add-domain", - "test_package#xxx01.tenant -> test_package#xxx01: view", - "test_package#xxx02.admin -> test_package#xxx02: add-domain", - "test_package#xxx02.admin -> test_package#xxx02: add-domain", - "test_package#xxx02.tenant -> test_package#xxx02: view", + "test_customer#xxx.admin -> test_customer#xxx: SELECT", + "test_customer#xxx.owner -> test_customer#xxx: DELETE", + "test_customer#xxx.tenant -> test_customer#xxx: SELECT", + "test_customer#xxx.admin -> test_customer#xxx: INSERT:test_package", + "test_package#xxx00.admin -> test_package#xxx00: INSERT:test_domain", + "test_package#xxx00.admin -> test_package#xxx00: INSERT:test_domain", + "test_package#xxx00.tenant -> test_package#xxx00: SELECT", + "test_package#xxx01.admin -> test_package#xxx01: INSERT:test_domain", + "test_package#xxx01.admin -> test_package#xxx01: INSERT:test_domain", + "test_package#xxx01.tenant -> test_package#xxx01: SELECT", + "test_package#xxx02.admin -> test_package#xxx02: INSERT:test_domain", + "test_package#xxx02.admin -> test_package#xxx02: INSERT:test_domain", + "test_package#xxx02.tenant -> test_package#xxx02: SELECT", - "test_customer#yyy.admin -> test_customer#yyy: add-package", - "test_customer#yyy.admin -> test_customer#yyy: view", - "test_customer#yyy.owner -> test_customer#yyy: *", - "test_customer#yyy.tenant -> test_customer#yyy: view", - "test_package#yyy00.admin -> test_package#yyy00: add-domain", - "test_package#yyy00.admin -> test_package#yyy00: add-domain", - "test_package#yyy00.tenant -> test_package#yyy00: view", - "test_package#yyy01.admin -> test_package#yyy01: add-domain", - "test_package#yyy01.admin -> test_package#yyy01: add-domain", - "test_package#yyy01.tenant -> test_package#yyy01: view", - "test_package#yyy02.admin -> test_package#yyy02: add-domain", - "test_package#yyy02.admin -> test_package#yyy02: add-domain", - "test_package#yyy02.tenant -> test_package#yyy02: view", + "test_customer#yyy.admin -> test_customer#yyy: SELECT", + "test_customer#yyy.owner -> test_customer#yyy: DELETE", + "test_customer#yyy.tenant -> test_customer#yyy: SELECT", + "test_customer#yyy.admin -> test_customer#yyy: INSERT:test_package", + "test_package#yyy00.admin -> test_package#yyy00: INSERT:test_domain", + "test_package#yyy00.admin -> test_package#yyy00: INSERT:test_domain", + "test_package#yyy00.tenant -> test_package#yyy00: SELECT", + "test_package#yyy01.admin -> test_package#yyy01: INSERT:test_domain", + "test_package#yyy01.admin -> test_package#yyy01: INSERT:test_domain", + "test_package#yyy01.tenant -> test_package#yyy01: SELECT", + "test_package#yyy02.admin -> test_package#yyy02: INSERT:test_domain", + "test_package#yyy02.admin -> test_package#yyy02: INSERT:test_domain", + "test_package#yyy02.tenant -> test_package#yyy02: SELECT", - "test_customer#zzz.admin -> test_customer#zzz: add-package", - "test_customer#zzz.admin -> test_customer#zzz: view", - "test_customer#zzz.owner -> test_customer#zzz: *", - "test_customer#zzz.tenant -> test_customer#zzz: view", - "test_package#zzz00.admin -> test_package#zzz00: add-domain", - "test_package#zzz00.admin -> test_package#zzz00: add-domain", - "test_package#zzz00.tenant -> test_package#zzz00: view", - "test_package#zzz01.admin -> test_package#zzz01: add-domain", - "test_package#zzz01.admin -> test_package#zzz01: add-domain", - "test_package#zzz01.tenant -> test_package#zzz01: view", - "test_package#zzz02.admin -> test_package#zzz02: add-domain", - "test_package#zzz02.admin -> test_package#zzz02: add-domain", - "test_package#zzz02.tenant -> test_package#zzz02: view" + "test_customer#zzz.admin -> test_customer#zzz: SELECT", + "test_customer#zzz.owner -> test_customer#zzz: DELETE", + "test_customer#zzz.tenant -> test_customer#zzz: SELECT", + "test_customer#zzz.admin -> test_customer#zzz: INSERT:test_package", + "test_package#zzz00.admin -> test_package#zzz00: INSERT:test_domain", + "test_package#zzz00.admin -> test_package#zzz00: INSERT:test_domain", + "test_package#zzz00.tenant -> test_package#zzz00: SELECT", + "test_package#zzz01.admin -> test_package#zzz01: INSERT:test_domain", + "test_package#zzz01.admin -> test_package#zzz01: INSERT:test_domain", + "test_package#zzz01.tenant -> test_package#zzz01: SELECT", + "test_package#zzz02.admin -> test_package#zzz02: INSERT:test_domain", + "test_package#zzz02.admin -> test_package#zzz02: INSERT:test_domain", + "test_package#zzz02.tenant -> test_package#zzz02: SELECT" // @formatter:on ); @@ -251,32 +251,32 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { allTheseRbacPermissionsAreReturned( result, // @formatter:off - "test_customer#xxx.admin -> test_customer#xxx: add-package", - "test_customer#xxx.admin -> test_customer#xxx: view", - "test_customer#xxx.tenant -> test_customer#xxx: view", + "test_customer#xxx.admin -> test_customer#xxx: INSERT:test_package", + "test_customer#xxx.admin -> test_customer#xxx: SELECT", + "test_customer#xxx.tenant -> test_customer#xxx: SELECT", - "test_package#xxx00.admin -> test_package#xxx00: add-domain", - "test_package#xxx00.admin -> test_package#xxx00: add-domain", - "test_package#xxx00.tenant -> test_package#xxx00: view", - "test_domain#xxx00-aaaa.owner -> test_domain#xxx00-aaaa: *", + "test_package#xxx00.admin -> test_package#xxx00: INSERT:test_domain", + "test_package#xxx00.admin -> test_package#xxx00: INSERT:test_domain", + "test_package#xxx00.tenant -> test_package#xxx00: SELECT", + "test_domain#xxx00-aaaa.owner -> test_domain#xxx00-aaaa: DELETE", - "test_package#xxx01.admin -> test_package#xxx01: add-domain", - "test_package#xxx01.admin -> test_package#xxx01: add-domain", - "test_package#xxx01.tenant -> test_package#xxx01: view", - "test_domain#xxx01-aaaa.owner -> test_domain#xxx01-aaaa: *", + "test_package#xxx01.admin -> test_package#xxx01: INSERT:test_domain", + "test_package#xxx01.admin -> test_package#xxx01: INSERT:test_domain", + "test_package#xxx01.tenant -> test_package#xxx01: SELECT", + "test_domain#xxx01-aaaa.owner -> test_domain#xxx01-aaaa: DELETE", - "test_package#xxx02.admin -> test_package#xxx02: add-domain", - "test_package#xxx02.admin -> test_package#xxx02: add-domain", - "test_package#xxx02.tenant -> test_package#xxx02: view", - "test_domain#xxx02-aaaa.owner -> test_domain#xxx02-aaaa: *" + "test_package#xxx02.admin -> test_package#xxx02: INSERT:test_domain", + "test_package#xxx02.admin -> test_package#xxx02: INSERT:test_domain", + "test_package#xxx02.tenant -> test_package#xxx02: SELECT", + "test_domain#xxx02-aaaa.owner -> test_domain#xxx02-aaaa: DELETE" // @formatter:on ); noneOfTheseRbacPermissionsAreReturned( result, // @formatter:off - "test_customer#yyy.admin -> test_customer#yyy: add-package", - "test_customer#yyy.admin -> test_customer#yyy: view", - "test_customer#yyy.tenant -> test_customer#yyy: view" + "test_customer#yyy.admin -> test_customer#yyy: INSERT:test_package", + "test_customer#yyy.admin -> test_customer#yyy: SELECT", + "test_customer#yyy.tenant -> test_customer#yyy: SELECT" // @formatter:on ); } @@ -311,26 +311,26 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { allTheseRbacPermissionsAreReturned( result, // @formatter:off - "test_customer#xxx.tenant -> test_customer#xxx: view", + "test_customer#xxx.tenant -> test_customer#xxx: SELECT", // "test_customer#xxx.admin -> test_customer#xxx: view" - Not permissions through the customer admin! - "test_package#xxx00.admin -> test_package#xxx00: add-domain", - "test_package#xxx00.admin -> test_package#xxx00: add-domain", - "test_package#xxx00.tenant -> test_package#xxx00: view", - "test_domain#xxx00-aaaa.owner -> test_domain#xxx00-aaaa: *", - "test_domain#xxx00-aaab.owner -> test_domain#xxx00-aaab: *" + "test_package#xxx00.admin -> test_package#xxx00: INSERT:test_domain", + "test_package#xxx00.admin -> test_package#xxx00: INSERT:test_domain", + "test_package#xxx00.tenant -> test_package#xxx00: SELECT", + "test_domain#xxx00-aaaa.owner -> test_domain#xxx00-aaaa: DELETE", + "test_domain#xxx00-aaab.owner -> test_domain#xxx00-aaab: DELETE" // @formatter:on ); noneOfTheseRbacPermissionsAreReturned( result, // @formatter:off - "test_customer#yyy.admin -> test_customer#yyy: add-package", - "test_customer#yyy.admin -> test_customer#yyy: view", - "test_customer#yyy.tenant -> test_customer#yyy: view", - "test_package#yyy00.admin -> test_package#yyy00: add-domain", - "test_package#yyy00.admin -> test_package#yyy00: add-domain", - "test_package#yyy00.tenant -> test_package#yyy00: view", - "test_domain#yyy00-aaaa.owner -> test_domain#yyy00-aaaa: *", - "test_domain#yyy00-aaab.owner -> test_domain#yyy00-aaab: *" + "test_customer#yyy.admin -> test_customer#yyy: INSERT:test_package", + "test_customer#yyy.admin -> test_customer#yyy: SELECT", + "test_customer#yyy.tenant -> test_customer#yyy: SELECT", + "test_package#yyy00.admin -> test_package#yyy00: INSERT:test_domain", + "test_package#yyy00.admin -> test_package#yyy00: INSERT:test_domain", + "test_package#yyy00.tenant -> test_package#yyy00: SELECT", + "test_domain#yyy00-aaaa.owner -> test_domain#yyy00-aaaa: DELETE", + "test_domain#yyy00-aaab.owner -> test_domain#yyy00-aaab: DELETE" // @formatter:on ); } @@ -359,11 +359,10 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { allTheseRbacPermissionsAreReturned( result, // @formatter:off - "test_customer#xxx.tenant -> test_customer#xxx: view", + "test_customer#xxx.tenant -> test_customer#xxx: SELECT", // "test_customer#xxx.admin -> test_customer#xxx: view" - Not permissions through the customer admin! - "test_package#xxx00.admin -> test_package#xxx00: add-domain", - "test_package#xxx00.admin -> test_package#xxx00: add-domain", - "test_package#xxx00.tenant -> test_package#xxx00: view" + "test_package#xxx00.admin -> test_package#xxx00: INSERT:test_domain", + "test_package#xxx00.tenant -> test_package#xxx00: SELECT" // @formatter:on ); noneOfTheseRbacPermissionsAreReturned( @@ -373,13 +372,13 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { "test_customer#xxx.admin -> test_customer#xxx: add-package", // no permissions on other customer's objects "test_customer#yyy.admin -> test_customer#yyy: add-package", - "test_customer#yyy.admin -> test_customer#yyy: view", - "test_customer#yyy.tenant -> test_customer#yyy: view", - "test_package#yyy00.admin -> test_package#yyy00: add-domain", - "test_package#yyy00.admin -> test_package#yyy00: add-domain", - "test_package#yyy00.tenant -> test_package#yyy00: view", - "test_domain#yyy00-aaaa.owner -> test_domain#yyy00-aaaa: *", - "test_domain#yyy00-xxxb.owner -> test_domain#yyy00-xxxb: *" + "test_customer#yyy.admin -> test_customer#yyy: SELECT", + "test_customer#yyy.tenant -> test_customer#yyy: SELECT", + "test_package#yyy00.admin -> test_package#yyy00: INSERT:test_domain", + "test_package#yyy00.admin -> test_package#yyy00: INSERT:test_domain", + "test_package#yyy00.tenant -> test_package#yyy00: SELECT", + "test_domain#yyy00-aaaa.owner -> test_domain#yyy00-aaaa: DELETE", + "test_domain#yyy00-xxxb.owner -> test_domain#yyy00-xxxb: DELETE" // @formatter:on ); } @@ -432,7 +431,8 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { final List actualResult, final String... expectedRoleNames) { assertThat(actualResult) - .extracting(p -> p.getRoleName() + " -> " + p.getObjectTable() + "#" + p.getObjectIdName() + ": " + p.getOp()) + .extracting(p -> p.getRoleName() + " -> " + p.getObjectTable() + "#" + p.getObjectIdName() + ": " + p.getOp() + + (p.getOpTableName() != null ? (":"+p.getOpTableName()) : "" )) .contains(expectedRoleNames); } -- 2.39.5 From e81da57ffde0a262e5f75f7a378f0662b4c3064a Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sat, 9 Mar 2024 09:12:29 +0100 Subject: [PATCH 47/53] add RBAC def for Domain and fix related assertions --- .../hsadminng/rbac/rbacdef/RbacView.java | 5 + .../test/cust/TestCustomerEntity.java | 2 + .../hsadminng/test/dom/TestDomainEntity.java | 73 +++++ .../resources/db/changelog/055-rbac-views.sql | 17 +- .../db/changelog/113-test-customer-rbac.md | 2 +- .../db/changelog/113-test-customer-rbac.sql | 2 +- .../db/changelog/123-test-package-rbac.md | 2 +- .../db/changelog/123-test-package-rbac.sql | 2 +- .../db/changelog/133-test-domain-rbac.sql | 254 +++++++++++++----- .../223-hs-office-relationship-rbac.md | 148 ---------- .../hsadminng/arch/ArchitectureTest.java | 1 + ...acGrantsDiagramServiceIntegrationTest.java | 5 +- .../RbacUserControllerAcceptanceTest.java | 9 +- .../RbacUserRepositoryIntegrationTest.java | 87 +++--- 14 files changed, 333 insertions(+), 276 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/test/dom/TestDomainEntity.java 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 5dd52c71..ba1d741d 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -16,6 +16,7 @@ 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.dom.TestDomainEntity; import net.hostsharing.hsadminng.test.pac.TestPackageEntity; import jakarta.persistence.Table; @@ -596,6 +597,9 @@ public class RbacView { } String getRawTableName() { + if ( aliasName.equals("global")) { + return "global"; // TODO: maybe we should introduce a GlobalEntity class? + } return withoutRvSuffix(entityClass.getAnnotation(Table.class).name()); } @@ -784,6 +788,7 @@ public class RbacView { Stream.of( TestCustomerEntity.class, TestPackageEntity.class, + TestDomainEntity.class, HsOfficePersonEntity.class, HsOfficePartnerEntity.class, HsOfficePartnerDetailsEntity.class, 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 bde8db09..bc6c7ae8 100644 --- a/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java @@ -41,6 +41,8 @@ public class TestCustomerEntity implements HasUuid { .withIdentityView(SQL.projection("prefix")) .withRestrictedViewOrderBy(SQL.expression("reference")) .withUpdatableColumns("reference", "prefix", "adminUserName") + // TODO: do we want explicit specification of parent-indenpendent insert permissions? + // .toRole("global", ADMIN).grantPermission("customer", INSERT) .createRole(OWNER, (with) -> { with.owningUser(CREATOR).unassumed(); diff --git a/src/main/java/net/hostsharing/hsadminng/test/dom/TestDomainEntity.java b/src/main/java/net/hostsharing/hsadminng/test/dom/TestDomainEntity.java new file mode 100644 index 00000000..6a031df7 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/test/dom/TestDomainEntity.java @@ -0,0 +1,73 @@ +package net.hostsharing.hsadminng.test.dom; + +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.pac.TestPackageEntity; + +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.Role.*; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.fetchedBySql; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; + +@Entity +@Table(name = "test_domain_rv") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class TestDomainEntity implements HasUuid { + + @Id + @GeneratedValue + private UUID uuid; + + @Version + private int version; + + @ManyToOne(optional = false) + @JoinColumn(name = "packageuuid") + private TestPackageEntity pac; + + private String name; + + private String description; + + public static RbacView rbac() { + return rbacViewFor("domain", TestDomainEntity.class) + .withIdentityView(SQL.projection("name")) + .withUpdatableColumns("version", "packageUuid", "description") + + .importEntityAlias("package", TestPackageEntity.class, + dependsOnColumn("packageUuid"), + fetchedBySql(""" + SELECT * FROM test_package p + WHERE p.uuid= ${ref}.packageUuid + """)) + .toRole("package", ADMIN).grantPermission("domain", INSERT) + + .createRole(OWNER, (with) -> { + with.incomingSuperRole("package", ADMIN); + with.outgoingSubRole("package", TENANT); + with.permission(DELETE); + with.permission(UPDATE); + }) + .createSubRole(ADMIN, (with) -> { + with.outgoingSubRole("package", TENANT); + with.permission(SELECT); + }); + } + + public static void main(String[] args) throws IOException { + rbac().generateWithBaseFileName("133-test-domain-rbac"); + } +} diff --git a/src/main/resources/db/changelog/055-rbac-views.sql b/src/main/resources/db/changelog/055-rbac-views.sql index cd1ff9fb..b494d120 100644 --- a/src/main/resources/db/changelog/055-rbac-views.sql +++ b/src/main/resources/db/changelog/055-rbac-views.sql @@ -337,10 +337,8 @@ grant all privileges on RbacOwnGrantedPermissions_rv to ${HSADMINNG_POSTGRES_RES /* Returns all permissions granted to the given user, which are also visible to the current user or assumed roles. - - - */ -create or replace function grantedPermissions(targetUserUuid uuid) +*/ +create or replace function grantedPermissionsRaw(targetUserUuid uuid) returns table(roleUuid uuid, roleName text, permissionUuid uuid, op RbacOp, opTableName varchar(60), objectTable varchar(60), objectIdName varchar, objectUuid uuid) returns null on null input language plpgsql as $$ @@ -375,4 +373,15 @@ begin ) xp; -- @formatter:on end; $$; + +create or replace function grantedPermissions(targetUserUuid uuid) + returns table(roleUuid uuid, roleName text, permissionUuid uuid, op RbacOp, opTableName varchar(60), objectTable varchar(60), objectIdName varchar, objectUuid uuid) + returns null on null input + language sql as $$ + select * from grantedPermissionsRaw(targetUserUuid) + union all + select roleUuid, roleName, permissionUuid, 'SELECT'::RbacOp, opTableName, objectTable, objectIdName, objectUuid + from grantedPermissionsRaw(targetUserUuid) + where op <> 'SELECT'::RbacOp; +$$; --// diff --git a/src/main/resources/db/changelog/113-test-customer-rbac.md b/src/main/resources/db/changelog/113-test-customer-rbac.md index 29e360a6..eb224d9f 100644 --- a/src/main/resources/db/changelog/113-test-customer-rbac.md +++ b/src/main/resources/db/changelog/113-test-customer-rbac.md @@ -1,4 +1,4 @@ -### rbac customer 2024-03-08T13:03:39.397294085 +### rbac customer 2024-03-09T08:56:16.396142507 ```mermaid %%{init:{'flowchart':{'htmlLabels':false}}}%% diff --git a/src/main/resources/db/changelog/113-test-customer-rbac.sql b/src/main/resources/db/changelog/113-test-customer-rbac.sql index 2bdaf47a..630ae406 100644 --- a/src/main/resources/db/changelog/113-test-customer-rbac.sql +++ b/src/main/resources/db/changelog/113-test-customer-rbac.sql @@ -1,5 +1,5 @@ --liquibase formatted sql --- This code generated was by RbacViewPostgresGenerator at 2024-03-08T13:03:39.428165899. +-- This code generated was by RbacViewPostgresGenerator at 2024-03-09T08:56:16.421821997. -- ============================================================================ --changeset test-customer-rbac-OBJECT:1 endDelimiter:--// diff --git a/src/main/resources/db/changelog/123-test-package-rbac.md b/src/main/resources/db/changelog/123-test-package-rbac.md index 42950bd0..2d3c9779 100644 --- a/src/main/resources/db/changelog/123-test-package-rbac.md +++ b/src/main/resources/db/changelog/123-test-package-rbac.md @@ -1,4 +1,4 @@ -### rbac package 2024-03-08T13:03:39.472333368 +### rbac package 2024-03-09T08:56:16.449886471 ```mermaid %%{init:{'flowchart':{'htmlLabels':false}}}%% diff --git a/src/main/resources/db/changelog/123-test-package-rbac.sql b/src/main/resources/db/changelog/123-test-package-rbac.sql index 0f071c9e..eb23305e 100644 --- a/src/main/resources/db/changelog/123-test-package-rbac.sql +++ b/src/main/resources/db/changelog/123-test-package-rbac.sql @@ -1,5 +1,5 @@ --liquibase formatted sql --- This code generated was by RbacViewPostgresGenerator at 2024-03-08T13:03:39.473061981. +-- This code generated was by RbacViewPostgresGenerator at 2024-03-09T08:56:16.450322125. -- ============================================================================ --changeset test-package-rbac-OBJECT:1 endDelimiter:--// diff --git a/src/main/resources/db/changelog/133-test-domain-rbac.sql b/src/main/resources/db/changelog/133-test-domain-rbac.sql index e890d267..fe97690e 100644 --- a/src/main/resources/db/changelog/133-test-domain-rbac.sql +++ b/src/main/resources/db/changelog/133-test-domain-rbac.sql @@ -1,4 +1,5 @@ --liquibase formatted sql +-- This code generated was by RbacViewPostgresGenerator at 2024-03-09T08:56:16.469632602. -- ============================================================================ --changeset test-domain-rbac-OBJECT:1 endDelimiter:--// @@ -11,91 +12,200 @@ call generateRelatedRbacObject('test_domain'); --changeset test-domain-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--// -- ---------------------------------------------------------------------------- call generateRbacRoleDescriptors('testDomain', 'test_domain'); - -create or replace function createTestDomainTenantRoleIfNotExists(domain test_domain) - returns uuid - returns null on null input - language plpgsql as $$ -declare - domainTenantRoleDesc RbacRoleDescriptor; - domainTenantRoleUuid uuid; -begin - domainTenantRoleDesc = testdomainTenant(domain); - domainTenantRoleUuid = findRoleId(domainTenantRoleDesc); - if domainTenantRoleUuid is not null then - return domainTenantRoleUuid; - end if; - - return createRoleWithGrants( - domainTenantRoleDesc, - permissions => array['SELECT'], - incomingSuperRoles => array[testdomainAdmin(domain)] - ); -end; $$; --// -- ============================================================================ ---changeset test-domain-rbac-ROLES-CREATION:1 endDelimiter:--// +--changeset test-domain-rbac-insert-trigger:1 endDelimiter:--// -- ---------------------------------------------------------------------------- + /* - Creates the roles and their assignments for a new domain for the AFTER INSERT TRIGGER. + Creates the roles, grants and permission for the AFTER INSERT TRIGGER. */ -create or replace function createRbacRulesForTestDomain() +create or replace procedure buildRbacSystemForTestDomain( + NEW test_domain +) + language plpgsql as $$ + +declare + newPackage test_package; + +begin + call enterTriggerForObjectUuid(NEW.uuid); + SELECT * FROM test_package p + WHERE p.uuid= NEW.packageUuid + into newPackage; + + perform createRoleWithGrants( + testDomainOwner(NEW), + permissions => array['DELETE', 'UPDATE'], + incomingSuperRoles => array[testPackageAdmin(newPackage)], + outgoingSubRoles => array[testPackageTenant(newPackage)] + ); + + perform createRoleWithGrants( + testDomainAdmin(NEW), + permissions => array['SELECT'], + incomingSuperRoles => array[testDomainOwner(NEW)], + outgoingSubRoles => array[testPackageTenant(newPackage)] + ); + + call leaveTriggerForObjectUuid(NEW.uuid); +end; $$; + +/* + AFTER INSERT TRIGGER to create the role+grant structure for a new test_domain row. + */ + +create or replace function insertTriggerForTestDomain_tf() returns trigger language plpgsql strict as $$ -declare - parentPackage test_package; begin - if TG_OP <> 'INSERT' then - raise exception 'invalid usage of TRIGGER AFTER INSERT'; - end if; - - call enterTriggerForObjectUuid(NEW.uuid); - - select * from test_package where uuid = NEW.packageUuid into parentPackage; - - -- an owner role is created and assigned to the package's admin group - perform createRoleWithGrants( - testDomainOwner(NEW), - permissions => array['DELETE'], - incomingSuperRoles => array[testPackageAdmin(parentPackage)] - ); - - -- and a domain admin role is created and assigned to the domain owner as well - perform createRoleWithGrants( - testDomainAdmin(NEW), - permissions => array['UPDATE'], - incomingSuperRoles => array[testDomainOwner(NEW)], - outgoingSubRoles => array[testPackageTenant(parentPackage)] - ); - - -- a tenent role is only created on demand - - call leaveTriggerForObjectUuid(NEW.uuid); + call buildRbacSystemForTestDomain(NEW); return NEW; end; $$; - -/* - An AFTER INSERT TRIGGER which creates the role structure for a new domain. - */ -drop trigger if exists createRbacRulesForTestDomain_Trigger on test_domain; -create trigger createRbacRulesForTestDomain_Trigger - after insert - on test_domain +create trigger insertTriggerForTestDomain_tg + after insert on test_domain for each row -execute procedure createRbacRulesForTestDomain(); +execute procedure insertTriggerForTestDomain_tf(); + --// +-- ============================================================================ +--changeset test-domain-rbac-update-trigger:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- +/* + Called from the AFTER UPDATE TRIGGER to re-wire the grants. + */ + +create or replace procedure updateRbacRulesForTestDomain( + OLD test_domain, + NEW test_domain +) + language plpgsql as $$ + +declare + oldPackage test_package; + newPackage test_package; + +begin + call enterTriggerForObjectUuid(NEW.uuid); + + SELECT * FROM test_package p + WHERE p.uuid= OLD.packageUuid + into oldPackage; + SELECT * FROM test_package p + WHERE p.uuid= NEW.packageUuid + into newPackage; + + if NEW.packageUuid <> OLD.packageUuid then + + call revokePermissionFromRole(findPermissionId(OLD.uuid, 'INSERT'), testPackageAdmin(oldPackage)); + + call revokeRoleFromRole(testDomainOwner(OLD), testPackageAdmin(oldPackage)); + call grantRoleToRole(testDomainOwner(NEW), testPackageAdmin(newPackage)); + + call revokeRoleFromRole(testPackageTenant(oldPackage), testDomainOwner(OLD)); + call grantRoleToRole(testPackageTenant(newPackage), testDomainOwner(NEW)); + + call revokeRoleFromRole(testPackageTenant(oldPackage), testDomainAdmin(OLD)); + call grantRoleToRole(testPackageTenant(newPackage), testDomainAdmin(NEW)); + + end if; + + call leaveTriggerForObjectUuid(NEW.uuid); +end; $$; + +/* + AFTER INSERT TRIGGER to re-wire the grant structure for a new test_domain row. + */ + +create or replace function updateTriggerForTestDomain_tf() + returns trigger + language plpgsql + strict as $$ +begin + call updateRbacRulesForTestDomain(OLD, NEW); + return NEW; +end; $$; + +create trigger updateTriggerForTestDomain_tg + after update on test_domain + for each row +execute procedure updateTriggerForTestDomain_tf(); + +--// + +-- ============================================================================ +--changeset test-domain-rbac-INSERT:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +/* + Creates INSERT INTO test_domain permissions for the related test_package rows. + */ +do language plpgsql $$ + declare + row test_package; + permissionUuid uuid; + roleUuid uuid; + begin + call defineContext('create INSERT INTO test_domain permissions for the related test_package rows'); + + FOR row IN SELECT * FROM test_package + LOOP + roleUuid := findRoleId(testPackageAdmin(row)); + permissionUuid := createPermission(row.uuid, 'INSERT', 'test_domain'); + call grantPermissionToRole(roleUuid, permissionUuid); + END LOOP; + END; +$$; + +/** + Adds test_domain INSERT permission to specified role of new test_package rows. +*/ +create or replace function test_domain_test_package_insert_tf() + returns trigger + language plpgsql + strict as $$ +begin + call grantPermissionToRole( + testPackageAdmin(NEW), + createPermission(NEW.uuid, 'INSERT', 'test_domain')); + return NEW; +end; $$; + +create trigger test_domain_test_package_insert_tg + after insert on test_package + for each row +execute procedure test_domain_test_package_insert_tf(); + +/** + Checks if the user or assumed roles are allowed to insert a row to test_domain. +*/ +create or replace function test_domain_insert_permission_missing_tf() + returns trigger + language plpgsql as $$ +begin + raise exception '[403] insert into test_domain not allowed for current subjects % (%)', + currentSubjects(), currentSubjectsUuids(); +end; $$; + +create trigger test_domain_insert_permission_check_tg + before insert on test_domain + for each row + when ( not hasInsertPermission(NEW.packageUuid, 'INSERT', 'test_domain') ) + execute procedure test_domain_insert_permission_missing_tf(); + +--// -- ============================================================================ --changeset test-domain-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- call generateRbacIdentityView('test_domain', $idName$ - target.name + name $idName$); --// @@ -103,15 +213,13 @@ call generateRbacIdentityView('test_domain', $idName$ -- ============================================================================ --changeset test-domain-rbac-RESTRICTED-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- - -/* - Creates a view to the customer main table which maps the identifying name - (in this case, the prefix) to the objectUuid. - */ -drop view if exists test_domain_rv; -create or replace view test_domain_rv as -select target.* - from test_domain as target - where target.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('SELECT', 'domain', currentSubjectsUuids())); -grant all privileges on test_domain_rv to ${HSADMINNG_POSTGRES_RESTRICTED_USERNAME}; +call generateRbacRestrictedView('test_domain', + 'name', + $updates$ + version = new.version, + packageUuid = new.packageUuid, + description = new.description + $updates$); --// + + diff --git a/src/main/resources/db/changelog/223-hs-office-relationship-rbac.md b/src/main/resources/db/changelog/223-hs-office-relationship-rbac.md index c41de32c..8ffa55ff 100644 --- a/src/main/resources/db/changelog/223-hs-office-relationship-rbac.md +++ b/src/main/resources/db/changelog/223-hs-office-relationship-rbac.md @@ -42,151 +42,3 @@ subgraph hsOfficeRelationship end ``` - if TG_OP = 'INSERT' then - - -- the owner role with full access for admins of the relAnchor global admins - ownerRole = createRole( - hsOfficeRelationshipOwner(NEW), - grantingPermissions(forObjectUuid => NEW.uuid, permitOps => array ['*']), - beneathRoles(array[ - globalAdmin(), - hsOfficePersonAdmin(newRelAnchor)]) - ); - - -- the admin role with full access for the owner - adminRole = createRole( - hsOfficeRelationshipAdmin(NEW), - grantingPermissions(forObjectUuid => NEW.uuid, permitOps => array ['edit']), - beneathRole(ownerRole) - ); - - -- the tenant role for those related users who can view the data - perform createRole( - hsOfficeRelationshipTenant, - grantingPermissions(forObjectUuid => NEW.uuid, permitOps => array ['view']), - beneathRoles(array[ - hsOfficePersonAdmin(newRelAnchor), - hsOfficePersonAdmin(newRelHolder), - hsOfficeContactAdmin(newContact)]), - withSubRoles(array[ - hsOfficePersonTenant(newRelAnchor), - hsOfficePersonTenant(newRelHolder), - hsOfficeContactTenant(newContact)]) - ); - - -- anchor and holder admin roles need each others tenant role - -- to be able to see the joined relationship - call grantRoleToRole(hsOfficePersonTenant(newRelAnchor), hsOfficePersonAdmin(newRelHolder)); - call grantRoleToRole(hsOfficePersonTenant(newRelHolder), hsOfficePersonAdmin(newRelAnchor)); - call grantRoleToRoleIfNotNull(hsOfficePersonTenant(newRelHolder), hsOfficeContactAdmin(newContact)); - - elsif TG_OP = 'UPDATE' then - - if OLD.contactUuid <> NEW.contactUuid then - -- nothing but the contact can be updated, - -- in other cases, a new relationship needs to be created and the old updated - - select * from hs_office_contact as c where c.uuid = OLD.contactUuid into oldContact; - - call revokeRoleFromRole( hsOfficeRelationshipTenant, hsOfficeContactAdmin(oldContact) ); - call grantRoleToRole( hsOfficeRelationshipTenant, hsOfficeContactAdmin(newContact) ); - - call revokeRoleFromRole( hsOfficeContactTenant(oldContact), hsOfficeRelationshipTenant ); - call grantRoleToRole( hsOfficeContactTenant(newContact), hsOfficeRelationshipTenant ); - end if; - else - raise exception 'invalid usage of TRIGGER'; - end if; - - return NEW; -end; $$; - -/* - An AFTER INSERT TRIGGER which creates the role structure for a new customer. - */ -create trigger createRbacRolesForHsOfficeRelationship_Trigger - after insert - on hs_office_relationship - for each row -execute procedure hsOfficeRelationshipRbacRolesTrigger(); - -/* - An AFTER UPDATE TRIGGER which updates the role structure of a customer. - */ -create trigger updateRbacRolesForHsOfficeRelationship_Trigger - after update - on hs_office_relationship - for each row -execute procedure hsOfficeRelationshipRbacRolesTrigger(); ---// - - --- ============================================================================ ---changeset hs-office-relationship-rbac-IDENTITY-VIEW:1 endDelimiter:--// --- ---------------------------------------------------------------------------- -call generateRbacIdentityView('hs_office_relationship', $idName$ - (select idName from hs_office_person_iv p where p.uuid = target.relAnchorUuid) - || '-with-' || target.relType || '-' || - (select idName from hs_office_person_iv p where p.uuid = target.relHolderUuid) - $idName$); ---// - - --- ============================================================================ ---changeset hs-office-relationship-rbac-RESTRICTED-VIEW:1 endDelimiter:--// --- ---------------------------------------------------------------------------- -call generateRbacRestrictedView('hs_office_relationship', - '(select idName from hs_office_person_iv p where p.uuid = target.relHolderUuid)', - $updates$ - contactUuid = new.contactUuid - $updates$); ---// - --- TODO: exception if one tries to amend any other column - - --- ============================================================================ ---changeset hs-office-relationship-rbac-NEW-RELATHIONSHIP:1 endDelimiter:--// --- ---------------------------------------------------------------------------- -/* - Creates a global permission for new-relationship and assigns it to the hostsharing admins role. - */ -do language plpgsql $$ - declare - addCustomerPermissions uuid[]; - globalObjectUuid uuid; - globalAdminRoleUuid uuid ; - begin - call defineContext('granting global new-relationship permission to global admin role', null, null, null); - - globalAdminRoleUuid := findRoleId(globalAdmin()); - globalObjectUuid := (select uuid from global); - addCustomerPermissions := createPermissions(globalObjectUuid, array ['new-relationship']); - call grantPermissionsToRole(globalAdminRoleUuid, addCustomerPermissions); - end; -$$; - -/** - Used by the trigger to prevent the add-customer to current user respectively assumed roles. - */ -create or replace function addHsOfficeRelationshipNotAllowedForCurrentSubjects() - returns trigger - language PLPGSQL -as $$ -begin - raise exception '[403] new-relationship not permitted for %', - array_to_string(currentSubjects(), ';', 'null'); -end; $$; - -/** - Checks if the user or assumed roles are allowed to create a new customer. - */ -create trigger hs_office_relationship_insert_trigger - before insert - on hs_office_relationship - for each row - -- TODO.spec: who is allowed to create new relationships - when ( not hasAssumedRole() ) -execute procedure addHsOfficeRelationshipNotAllowedForCurrentSubjects(); ---// - diff --git a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java index 82856124..013b2309 100644 --- a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java +++ b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java @@ -28,6 +28,7 @@ public class ArchitectureTest { "..test", "..test.cust", "..test.pac", + "..test.dom", "..context", "..generated..", "..persistence..", diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramServiceIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramServiceIntegrationTest.java index 0ed953ca..0e0421c8 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramServiceIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramServiceIntegrationTest.java @@ -62,6 +62,7 @@ class RbacGrantsDiagramServiceIntegrationTest extends ContextBasedTestWithCleanu role:test_domain#xxx00-aaaa.admin --> role:test_package#xxx00.tenant role:test_domain#xxx00-aaaa.owner --> role:test_domain#xxx00-aaaa.admin + role:test_domain#xxx00-aaaa.owner --> role:test_package#xxx00.tenant role:test_package#xxx00.tenant --> role:test_customer#xxx.tenant """.trim()); } @@ -75,10 +76,12 @@ class RbacGrantsDiagramServiceIntegrationTest extends ContextBasedTestWithCleanu flowchart TB role:test_customer#xxx.tenant --> perm:SELECT:on:test_customer#xxx - role:test_domain#xxx00-aaaa.admin --> perm:UPDATE:on:test_domain#xxx00-aaaa + role:test_domain#xxx00-aaaa.admin --> perm:SELECT:on:test_domain#xxx00-aaaa role:test_domain#xxx00-aaaa.admin --> role:test_package#xxx00.tenant role:test_domain#xxx00-aaaa.owner --> perm:DELETE:on:test_domain#xxx00-aaaa + role:test_domain#xxx00-aaaa.owner --> perm:UPDATE:on:test_domain#xxx00-aaaa role:test_domain#xxx00-aaaa.owner --> role:test_domain#xxx00-aaaa.admin + role:test_domain#xxx00-aaaa.owner --> role:test_package#xxx00.tenant role:test_package#xxx00.tenant --> perm:SELECT:on:test_package#xxx00 role:test_package#xxx00.tenant --> role:test_customer#xxx.tenant """.trim()); diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerAcceptanceTest.java index b2620537..9d7e16ca 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerAcceptanceTest.java @@ -295,7 +295,8 @@ class RbacUserControllerAcceptanceTest { hasEntry("roleName", "test_domain#yyy00-aaaa.owner"), hasEntry("op", "DELETE")) )) - .body("size()", is(6)); + // actual content tested in integration test, so this is enough for here: + .body("size()", greaterThanOrEqualTo(6)); // @formatter:on } @@ -325,7 +326,8 @@ class RbacUserControllerAcceptanceTest { hasEntry("roleName", "test_domain#yyy00-aaaa.owner"), hasEntry("op", "DELETE")) )) - .body("size()", is(6)); + // actual content tested in integration test, so this is enough for here: + .body("size()", greaterThanOrEqualTo(6)); // @formatter:on } @@ -354,7 +356,8 @@ class RbacUserControllerAcceptanceTest { hasEntry("roleName", "test_domain#yyy00-aaaa.owner"), hasEntry("op", "DELETE")) )) - .body("size()", is(6)); + // actual content tested in integration test, so this is enough for here: + .body("size()", greaterThanOrEqualTo(6)); // @formatter:on } diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java index e5b74ccb..c63047ed 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java @@ -20,6 +20,7 @@ import jakarta.servlet.http.HttpServletRequest; import java.util.List; import java.util.UUID; +import static java.util.Comparator.comparing; import static net.hostsharing.test.JpaAttempt.attempt; import static org.assertj.core.api.Assertions.assertThat; @@ -181,50 +182,48 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { private static final String[] ALL_USER_PERMISSIONS = Array.of( // @formatter:off - "global#global.admin -> global#global: add-customer", + "test_customer#xxx.admin -> test_customer#xxx: SELECT", + "test_customer#xxx.owner -> test_customer#xxx: DELETE", + "test_customer#xxx.tenant -> test_customer#xxx: SELECT", + "test_customer#xxx.admin -> test_customer#xxx: INSERT:test_package", + "test_package#xxx00.admin -> test_package#xxx00: INSERT:test_domain", + "test_package#xxx00.admin -> test_package#xxx00: INSERT:test_domain", + "test_package#xxx00.tenant -> test_package#xxx00: SELECT", + "test_package#xxx01.admin -> test_package#xxx01: INSERT:test_domain", + "test_package#xxx01.admin -> test_package#xxx01: INSERT:test_domain", + "test_package#xxx01.tenant -> test_package#xxx01: SELECT", + "test_package#xxx02.admin -> test_package#xxx02: INSERT:test_domain", + "test_package#xxx02.admin -> test_package#xxx02: INSERT:test_domain", + "test_package#xxx02.tenant -> test_package#xxx02: SELECT", - "test_customer#xxx.admin -> test_customer#xxx: SELECT", - "test_customer#xxx.owner -> test_customer#xxx: DELETE", - "test_customer#xxx.tenant -> test_customer#xxx: SELECT", - "test_customer#xxx.admin -> test_customer#xxx: INSERT:test_package", - "test_package#xxx00.admin -> test_package#xxx00: INSERT:test_domain", - "test_package#xxx00.admin -> test_package#xxx00: INSERT:test_domain", - "test_package#xxx00.tenant -> test_package#xxx00: SELECT", - "test_package#xxx01.admin -> test_package#xxx01: INSERT:test_domain", - "test_package#xxx01.admin -> test_package#xxx01: INSERT:test_domain", - "test_package#xxx01.tenant -> test_package#xxx01: SELECT", - "test_package#xxx02.admin -> test_package#xxx02: INSERT:test_domain", - "test_package#xxx02.admin -> test_package#xxx02: INSERT:test_domain", - "test_package#xxx02.tenant -> test_package#xxx02: SELECT", + "test_customer#yyy.admin -> test_customer#yyy: SELECT", + "test_customer#yyy.owner -> test_customer#yyy: DELETE", + "test_customer#yyy.tenant -> test_customer#yyy: SELECT", + "test_customer#yyy.admin -> test_customer#yyy: INSERT:test_package", + "test_package#yyy00.admin -> test_package#yyy00: INSERT:test_domain", + "test_package#yyy00.admin -> test_package#yyy00: INSERT:test_domain", + "test_package#yyy00.tenant -> test_package#yyy00: SELECT", + "test_package#yyy01.admin -> test_package#yyy01: INSERT:test_domain", + "test_package#yyy01.admin -> test_package#yyy01: INSERT:test_domain", + "test_package#yyy01.tenant -> test_package#yyy01: SELECT", + "test_package#yyy02.admin -> test_package#yyy02: INSERT:test_domain", + "test_package#yyy02.admin -> test_package#yyy02: INSERT:test_domain", + "test_package#yyy02.tenant -> test_package#yyy02: SELECT", - "test_customer#yyy.admin -> test_customer#yyy: SELECT", - "test_customer#yyy.owner -> test_customer#yyy: DELETE", - "test_customer#yyy.tenant -> test_customer#yyy: SELECT", - "test_customer#yyy.admin -> test_customer#yyy: INSERT:test_package", - "test_package#yyy00.admin -> test_package#yyy00: INSERT:test_domain", - "test_package#yyy00.admin -> test_package#yyy00: INSERT:test_domain", - "test_package#yyy00.tenant -> test_package#yyy00: SELECT", - "test_package#yyy01.admin -> test_package#yyy01: INSERT:test_domain", - "test_package#yyy01.admin -> test_package#yyy01: INSERT:test_domain", - "test_package#yyy01.tenant -> test_package#yyy01: SELECT", - "test_package#yyy02.admin -> test_package#yyy02: INSERT:test_domain", - "test_package#yyy02.admin -> test_package#yyy02: INSERT:test_domain", - "test_package#yyy02.tenant -> test_package#yyy02: SELECT", - - "test_customer#zzz.admin -> test_customer#zzz: SELECT", - "test_customer#zzz.owner -> test_customer#zzz: DELETE", - "test_customer#zzz.tenant -> test_customer#zzz: SELECT", - "test_customer#zzz.admin -> test_customer#zzz: INSERT:test_package", - "test_package#zzz00.admin -> test_package#zzz00: INSERT:test_domain", - "test_package#zzz00.admin -> test_package#zzz00: INSERT:test_domain", - "test_package#zzz00.tenant -> test_package#zzz00: SELECT", - "test_package#zzz01.admin -> test_package#zzz01: INSERT:test_domain", - "test_package#zzz01.admin -> test_package#zzz01: INSERT:test_domain", - "test_package#zzz01.tenant -> test_package#zzz01: SELECT", - "test_package#zzz02.admin -> test_package#zzz02: INSERT:test_domain", - "test_package#zzz02.admin -> test_package#zzz02: INSERT:test_domain", - "test_package#zzz02.tenant -> test_package#zzz02: SELECT" - // @formatter:on + "test_customer#zzz.admin -> test_customer#zzz: SELECT", + "test_customer#zzz.owner -> test_customer#zzz: DELETE", + "test_customer#zzz.tenant -> test_customer#zzz: SELECT", + "test_customer#zzz.admin -> test_customer#zzz: INSERT:test_package", + "test_package#zzz00.admin -> test_package#zzz00: INSERT:test_domain", + "test_package#zzz00.admin -> test_package#zzz00: INSERT:test_domain", + "test_package#zzz00.tenant -> test_package#zzz00: SELECT", + "test_package#zzz01.admin -> test_package#zzz01: INSERT:test_domain", + "test_package#zzz01.admin -> test_package#zzz01: INSERT:test_domain", + "test_package#zzz01.tenant -> test_package#zzz01: SELECT", + "test_package#zzz02.admin -> test_package#zzz02: INSERT:test_domain", + "test_package#zzz02.admin -> test_package#zzz02: INSERT:test_domain", + "test_package#zzz02.tenant -> test_package#zzz02: SELECT" + // @formatter:on ); @Test @@ -233,7 +232,9 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { context("superuser-alex@hostsharing.net"); // when - final var result = rbacUserRepository.findPermissionsOfUserByUuid(userUUID("superuser-alex@hostsharing.net")); + final var result = rbacUserRepository.findPermissionsOfUserByUuid(userUUID("superuser-fran@hostsharing.net")) + .stream().filter(p -> p.getObjectTable().contains("test_")) + .sorted(comparing(RbacUserPermission::toString)).toList(); // then allTheseRbacPermissionsAreReturned(result, ALL_USER_PERMISSIONS); -- 2.39.5 From c7931a67a9447ac7afd61550c2e2ad930a31d5d8 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sun, 10 Mar 2024 06:57:34 +0100 Subject: [PATCH 48/53] reduce the changeset --- sql/rbac-tests.sql | 4 ++-- .../resources/db/changelog/050-rbac-base.sql | 24 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/sql/rbac-tests.sql b/sql/rbac-tests.sql index ad017189..e30ac926 100644 --- a/sql/rbac-tests.sql +++ b/sql/rbac-tests.sql @@ -19,11 +19,11 @@ select * FROM queryAllPermissionsOfSubjectId(findRbacUser('rosa@example.com')); select * -FROM queryAllRbacUsersWithPermissionsFor(findPermissionId('customer', +FROM queryAllRbacUsersWithPermissionsFor(findEffectivePermissionId('customer', (SELECT uuid FROM RbacObject WHERE objectTable = 'customer' LIMIT 1), 'add-package')); select * -FROM queryAllRbacUsersWithPermissionsFor(findPermissionId('package', +FROM queryAllRbacUsersWithPermissionsFor(findEffectivePermissionId('package', (SELECT uuid FROM RbacObject WHERE objectTable = 'package' LIMIT 1), 'DELETE')); diff --git a/src/main/resources/db/changelog/050-rbac-base.sql b/src/main/resources/db/changelog/050-rbac-base.sql index e27bd907..2992d6a9 100644 --- a/src/main/resources/db/changelog/050-rbac-base.sql +++ b/src/main/resources/db/changelog/050-rbac-base.sql @@ -443,18 +443,6 @@ begin end; $$; -create or replace function findPermissionId(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null) - returns uuid - returns null on null input - stable -- leakproof - language sql as $$ -select uuid - from RbacPermission p - where p.objectUuid = forObjectUuid - and p.op = forOp - and p.opTableName = forOpTableName -$$; - create or replace function findEffectivePermissionId(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null) returns uuid returns null on null input @@ -466,6 +454,18 @@ select uuid and (forOp = 'SELECT' or p.op = forOp) -- all other RbacOp include 'SELECT' and p.opTableName = forOpTableName $$; + +create or replace function findPermissionId(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null) + returns uuid + returns null on null input + stable -- leakproof + language sql as $$ +select uuid + from RbacPermission p + where p.objectUuid = forObjectUuid + and p.op = forOp + and p.opTableName = forOpTableName +$$; --// -- ============================================================================ -- 2.39.5 From 8d697e1ea712df05bdb258f22dff8b8573c3d05e Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sun, 10 Mar 2024 07:13:12 +0100 Subject: [PATCH 49/53] introduce singleton() --- .../rbac/rbacdef/InsertTriggerGenerator.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java index 7afd1941..5303c27e 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java @@ -1,6 +1,7 @@ package net.hostsharing.hsadminng.rbac.rbacdef; import java.util.Optional; +import java.util.function.BinaryOperator; import java.util.stream.Stream; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT; @@ -142,19 +143,20 @@ public class InsertTriggerGenerator { private Optional getOptionalInsertGrant() { return getInsertGrants() - .reduce((x, y) -> { - throw new IllegalStateException("only a single INSERT permission grant allowed"); - }); + .reduce(singleton()); } private Optional getOptionalInsertSuperRole() { return getInsertGrants() .map(RbacView.RbacGrantDefinition::getSuperRoleDef) - .reduce((x, y) -> { - throw new IllegalStateException("only a single INSERT permission grant allowed"); - }); + .reduce(singleton()); } + private static BinaryOperator singleton() { + return (x, y) -> { + throw new IllegalStateException("only a single INSERT permission grant allowed"); + }; + } private static String toVar(final RbacView.RbacRoleDefinition roleDef) { return uncapitalize(roleDef.getEntityAlias().simpleName()) + capitalize(roleDef.getRole().roleName()); -- 2.39.5 From 8b78265e512008b1ce0caf6360cc9ae51c4258dc Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sun, 10 Mar 2024 11:45:56 +0100 Subject: [PATCH 50/53] support SQL_QUERY for identity view --- .../rbacdef/RbacIdentityViewGenerator.java | 28 +++++++++++++------ .../db/changelog/058-rbac-generators.sql | 22 +++++++++++---- .../db/changelog/113-test-customer-rbac.md | 2 +- .../db/changelog/113-test-customer-rbac.sql | 8 +++--- .../db/changelog/123-test-package-rbac.md | 2 +- .../db/changelog/123-test-package-rbac.sql | 8 +++--- .../db/changelog/133-test-domain-rbac.sql | 8 +++--- .../changelog/203-hs-office-contact-rbac.sql | 2 +- .../changelog/213-hs-office-person-rbac.sql | 2 +- .../223-hs-office-relationship-rbac.sql | 2 +- .../changelog/233-hs-office-partner-rbac.sql | 2 +- .../234-hs-office-partner-details-rbac.sql | 2 +- .../243-hs-office-bankaccount-rbac.sql | 2 +- .../253-hs-office-sepamandate-rbac.sql | 2 +- .../changelog/273-hs-office-debitor-rbac.sql | 2 +- .../303-hs-office-membership-rbac.sql | 2 +- .../313-hs-office-coopshares-rbac.sql | 3 +- .../323-hs-office-coopassets-rbac.sql | 3 +- 18 files changed, 62 insertions(+), 40 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacIdentityViewGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacIdentityViewGenerator.java index 9eba4a68..d664a83b 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacIdentityViewGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacIdentityViewGenerator.java @@ -20,14 +20,26 @@ public class RbacIdentityViewGenerator { -- ============================================================================ --changeset ${liquibaseTagPrefix}-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- - call generateRbacIdentityView('${rawTableName}', $idName$ - ${identityViewSqlPart} - $idName$); - --// - """, - with("liquibaseTagPrefix", liquibaseTagPrefix), - with("identityViewSqlPart", rbacDef.getIdentityViewSqlQuery().sql), // TODO: other part types - with("rawTableName", rawTableName)); + with("liquibaseTagPrefix", liquibaseTagPrefix)); + + plPgSql.writeLn( + switch (rbacDef.getIdentityViewSqlQuery().part) { + case SQL_PROJECTION -> """ + call generateRbacIdentityViewFromProjection('${rawTableName}', $idName$ + ${identityViewSqlPart} + $idName$); + """; + case SQL_QUERY -> """ + call generateRbacIdentityViewFromProjection('${rawTableName}', $idName$ + ${identityViewSqlPart} + $idName$); + """; + default -> throw new IllegalStateException("illegal SQL part given"); + }, + with("identityViewSqlPart", rbacDef.getIdentityViewSqlQuery().sql), + with("rawTableName", rawTableName)); + + plPgSql.writeLn("--//"); } } diff --git a/src/main/resources/db/changelog/058-rbac-generators.sql b/src/main/resources/db/changelog/058-rbac-generators.sql index 4f4fb086..89d585ea 100644 --- a/src/main/resources/db/changelog/058-rbac-generators.sql +++ b/src/main/resources/db/changelog/058-rbac-generators.sql @@ -91,7 +91,7 @@ end; $$; --changeset rbac-generators-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -create or replace procedure generateRbacIdentityView(targetTable text, idNameExpression text) +create or replace procedure generateRbacIdentityViewFromQuery(targetTable text, sqlQuery text) language plpgsql as $$ declare sql text; @@ -100,11 +100,9 @@ begin -- create a view to the target main table which maps an idName to the objectUuid sql = format($sql$ - create or replace view %1$s_iv as - select target.uuid, cleanIdentifier(%2$s) as idName - from %1$s as target; + create or replace view %1$s_iv as %2$s; grant all privileges on %1$s_iv to ${HSADMINNG_POSTGRES_RESTRICTED_USERNAME}; - $sql$, targetTable, idNameExpression); + $sql$, targetTable, sqlQuery); execute sql; -- creates a function which maps an idName to the objectUuid @@ -129,6 +127,20 @@ begin $sql$, targetTable); execute sql; end; $$; + +create or replace procedure generateRbacIdentityViewFromProjection(targetTable text, sqlProjection text) + language plpgsql as $$ +declare + sqlQuery text; +begin + targettable := lower(targettable); + + sqlQuery = format($sql$ + select target.uuid, cleanIdentifier(%2$s) as idName + from %1$s as target; + $sql$, targetTable, sqlProjection); + call generateRbacIdentityViewFromQuery(targetTable, sqlQuery); +end; $$; --// diff --git a/src/main/resources/db/changelog/113-test-customer-rbac.md b/src/main/resources/db/changelog/113-test-customer-rbac.md index eb224d9f..a585b153 100644 --- a/src/main/resources/db/changelog/113-test-customer-rbac.md +++ b/src/main/resources/db/changelog/113-test-customer-rbac.md @@ -1,4 +1,4 @@ -### rbac customer 2024-03-09T08:56:16.396142507 +### rbac customer 2024-03-10T11:42:41.089596517 ```mermaid %%{init:{'flowchart':{'htmlLabels':false}}}%% diff --git a/src/main/resources/db/changelog/113-test-customer-rbac.sql b/src/main/resources/db/changelog/113-test-customer-rbac.sql index 630ae406..25128963 100644 --- a/src/main/resources/db/changelog/113-test-customer-rbac.sql +++ b/src/main/resources/db/changelog/113-test-customer-rbac.sql @@ -1,5 +1,5 @@ --liquibase formatted sql --- This code generated was by RbacViewPostgresGenerator at 2024-03-09T08:56:16.421821997. +-- This code generated was by RbacViewPostgresGenerator at 2024-03-10T11:42:41.121556631. -- ============================================================================ --changeset test-customer-rbac-OBJECT:1 endDelimiter:--// @@ -102,12 +102,12 @@ create trigger test_customer_insert_permission_check_tg -- ============================================================================ --changeset test-customer-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityView('test_customer', $idName$ + +call generateRbacIdentityViewFromProjection('test_customer', $idName$ prefix $idName$); + --// - - -- ============================================================================ --changeset test-customer-rbac-RESTRICTED-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- diff --git a/src/main/resources/db/changelog/123-test-package-rbac.md b/src/main/resources/db/changelog/123-test-package-rbac.md index 2d3c9779..a8c0d66b 100644 --- a/src/main/resources/db/changelog/123-test-package-rbac.md +++ b/src/main/resources/db/changelog/123-test-package-rbac.md @@ -1,4 +1,4 @@ -### rbac package 2024-03-09T08:56:16.449886471 +### rbac package 2024-03-10T11:42:41.162678472 ```mermaid %%{init:{'flowchart':{'htmlLabels':false}}}%% diff --git a/src/main/resources/db/changelog/123-test-package-rbac.sql b/src/main/resources/db/changelog/123-test-package-rbac.sql index eb23305e..ad0359ff 100644 --- a/src/main/resources/db/changelog/123-test-package-rbac.sql +++ b/src/main/resources/db/changelog/123-test-package-rbac.sql @@ -1,5 +1,5 @@ --liquibase formatted sql --- This code generated was by RbacViewPostgresGenerator at 2024-03-09T08:56:16.450322125. +-- This code generated was by RbacViewPostgresGenerator at 2024-03-10T11:42:41.163393064. -- ============================================================================ --changeset test-package-rbac-OBJECT:1 endDelimiter:--// @@ -205,12 +205,12 @@ create trigger test_package_insert_permission_check_tg -- ============================================================================ --changeset test-package-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityView('test_package', $idName$ + +call generateRbacIdentityViewFromProjection('test_package', $idName$ name $idName$); + --// - - -- ============================================================================ --changeset test-package-rbac-RESTRICTED-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- diff --git a/src/main/resources/db/changelog/133-test-domain-rbac.sql b/src/main/resources/db/changelog/133-test-domain-rbac.sql index fe97690e..a29a1b5a 100644 --- a/src/main/resources/db/changelog/133-test-domain-rbac.sql +++ b/src/main/resources/db/changelog/133-test-domain-rbac.sql @@ -1,5 +1,5 @@ --liquibase formatted sql --- This code generated was by RbacViewPostgresGenerator at 2024-03-09T08:56:16.469632602. +-- This code generated was by RbacViewPostgresGenerator at 2024-03-10T11:42:41.186902574. -- ============================================================================ --changeset test-domain-rbac-OBJECT:1 endDelimiter:--// @@ -204,12 +204,12 @@ create trigger test_domain_insert_permission_check_tg -- ============================================================================ --changeset test-domain-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityView('test_domain', $idName$ + +call generateRbacIdentityViewFromProjection('test_domain', $idName$ name $idName$); + --// - - -- ============================================================================ --changeset test-domain-rbac-RESTRICTED-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- diff --git a/src/main/resources/db/changelog/203-hs-office-contact-rbac.sql b/src/main/resources/db/changelog/203-hs-office-contact-rbac.sql index dc51efa3..3a9b0c34 100644 --- a/src/main/resources/db/changelog/203-hs-office-contact-rbac.sql +++ b/src/main/resources/db/changelog/203-hs-office-contact-rbac.sql @@ -75,7 +75,7 @@ execute procedure createRbacRolesForHsOfficeContact(); --changeset hs-office-contact-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityView('hs_office_contact', $idName$ +call generateRbacIdentityViewFromProjection('hs_office_contact', $idName$ target.label $idName$); --// diff --git a/src/main/resources/db/changelog/213-hs-office-person-rbac.sql b/src/main/resources/db/changelog/213-hs-office-person-rbac.sql index c903e086..fbb1f8e1 100644 --- a/src/main/resources/db/changelog/213-hs-office-person-rbac.sql +++ b/src/main/resources/db/changelog/213-hs-office-person-rbac.sql @@ -73,7 +73,7 @@ execute procedure createRbacRolesForHsOfficePerson(); -- ============================================================================ --changeset hs-office-person-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityView('hs_office_person', $idName$ +call generateRbacIdentityViewFromProjection('hs_office_person', $idName$ concat(target.tradeName, target.familyName, target.givenName) $idName$); --// diff --git a/src/main/resources/db/changelog/223-hs-office-relationship-rbac.sql b/src/main/resources/db/changelog/223-hs-office-relationship-rbac.sql index 34d23793..126664a4 100644 --- a/src/main/resources/db/changelog/223-hs-office-relationship-rbac.sql +++ b/src/main/resources/db/changelog/223-hs-office-relationship-rbac.sql @@ -124,7 +124,7 @@ execute procedure hsOfficeRelationshipRbacRolesTrigger(); -- ============================================================================ --changeset hs-office-relationship-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityView('hs_office_relationship', $idName$ +call generateRbacIdentityViewFromProjection('hs_office_relationship', $idName$ (select idName from hs_office_person_iv p where p.uuid = target.relAnchorUuid) || '-with-' || target.relType || '-' || (select idName from hs_office_person_iv p where p.uuid = target.relHolderUuid) diff --git a/src/main/resources/db/changelog/233-hs-office-partner-rbac.sql b/src/main/resources/db/changelog/233-hs-office-partner-rbac.sql index e7634d46..d16048fd 100644 --- a/src/main/resources/db/changelog/233-hs-office-partner-rbac.sql +++ b/src/main/resources/db/changelog/233-hs-office-partner-rbac.sql @@ -187,7 +187,7 @@ execute procedure hsOfficePartnerRbacRolesTrigger(); -- ============================================================================ --changeset hs-office-partner-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityView('hs_office_partner', $idName$ +call generateRbacIdentityViewFromProjection('hs_office_partner', $idName$ partnerNumber || ':' || (select idName from hs_office_person_iv p where p.uuid = target.personuuid) || '-' || diff --git a/src/main/resources/db/changelog/234-hs-office-partner-details-rbac.sql b/src/main/resources/db/changelog/234-hs-office-partner-details-rbac.sql index 7cd72003..c4e053b9 100644 --- a/src/main/resources/db/changelog/234-hs-office-partner-details-rbac.sql +++ b/src/main/resources/db/changelog/234-hs-office-partner-details-rbac.sql @@ -10,7 +10,7 @@ call generateRelatedRbacObject('hs_office_partner_details'); -- ============================================================================ --changeset hs-office-partner-details-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityView('hs_office_partner_details', $idName$ +call generateRbacIdentityViewFromProjection('hs_office_partner_details', $idName$ (select idName || '-details' from hs_office_partner_iv partner_iv join hs_office_partner partner on (partner_iv.uuid = partner.uuid) where partner.detailsUuid = target.uuid) diff --git a/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.sql b/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.sql index 5b1ae81f..93b605ce 100644 --- a/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.sql +++ b/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.sql @@ -74,7 +74,7 @@ execute procedure createRbacRolesForHsOfficeBankAccount(); --changeset hs-office-bankaccount-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityView('hs_office_bankaccount', $idName$ +call generateRbacIdentityViewFromProjection('hs_office_bankaccount', $idName$ target.holder $idName$); --// diff --git a/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.sql b/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.sql index 44815f32..da7887cd 100644 --- a/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.sql +++ b/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.sql @@ -94,7 +94,7 @@ execute procedure hsOfficeSepaMandateRbacRolesTrigger(); -- ============================================================================ --changeset hs-office-sepamandate-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityView('hs_office_sepamandate', idNameExpression => 'target.reference'); +call generateRbacIdentityViewFromProjection('hs_office_sepamandate', 'target.reference'); --// diff --git a/src/main/resources/db/changelog/273-hs-office-debitor-rbac.sql b/src/main/resources/db/changelog/273-hs-office-debitor-rbac.sql index 48109078..5f684f49 100644 --- a/src/main/resources/db/changelog/273-hs-office-debitor-rbac.sql +++ b/src/main/resources/db/changelog/273-hs-office-debitor-rbac.sql @@ -173,7 +173,7 @@ execute procedure hsOfficeDebitorRbacRolesTrigger(); -- ============================================================================ --changeset hs-office-debitor-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityView('hs_office_debitor', $idName$ +call generateRbacIdentityViewFromProjection('hs_office_debitor', $idName$ '#' || (select partnerNumber from hs_office_partner p where p.uuid = target.partnerUuid) || to_char(debitorNumberSuffix, 'fm00') || diff --git a/src/main/resources/db/changelog/303-hs-office-membership-rbac.sql b/src/main/resources/db/changelog/303-hs-office-membership-rbac.sql index 10125d69..2a4a4a50 100644 --- a/src/main/resources/db/changelog/303-hs-office-membership-rbac.sql +++ b/src/main/resources/db/changelog/303-hs-office-membership-rbac.sql @@ -93,7 +93,7 @@ execute procedure hsOfficeMembershipRbacRolesTrigger(); -- ============================================================================ --changeset hs-office-membership-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityView('hs_office_membership', idNameExpression => $idName$ +call generateRbacIdentityViewFromProjection('hs_office_membership', $idName$ '#' || (select partnerNumber from hs_office_partner p where p.uuid = target.partnerUuid) || memberNumberSuffix || diff --git a/src/main/resources/db/changelog/313-hs-office-coopshares-rbac.sql b/src/main/resources/db/changelog/313-hs-office-coopshares-rbac.sql index 5082a3ca..5ee8bfbe 100644 --- a/src/main/resources/db/changelog/313-hs-office-coopshares-rbac.sql +++ b/src/main/resources/db/changelog/313-hs-office-coopshares-rbac.sql @@ -68,8 +68,7 @@ execute procedure hsOfficeCoopSharesTransactionRbacRolesTrigger(); -- ============================================================================ --changeset hs-office-coopSharesTransaction-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityView('hs_office_coopSharesTransaction', - idNameExpression => 'target.reference'); +call generateRbacIdentityViewFromProjection('hs_office_coopSharesTransaction', 'target.reference'); --// diff --git a/src/main/resources/db/changelog/323-hs-office-coopassets-rbac.sql b/src/main/resources/db/changelog/323-hs-office-coopassets-rbac.sql index 6fbdc5ce..69920385 100644 --- a/src/main/resources/db/changelog/323-hs-office-coopassets-rbac.sql +++ b/src/main/resources/db/changelog/323-hs-office-coopassets-rbac.sql @@ -68,8 +68,7 @@ execute procedure hsOfficeCoopAssetsTransactionRbacRolesTrigger(); -- ============================================================================ --changeset hs-office-coopAssetsTransaction-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityView('hs_office_coopAssetsTransaction', - idNameExpression => 'target.reference'); +call generateRbacIdentityViewFromProjection('hs_office_coopAssetsTransaction', 'target.reference'); --// -- 2.39.5 From 1c2cdf207cf880f395c89dc073177abaad263da0 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 11 Mar 2024 09:13:07 +0100 Subject: [PATCH 51/53] add check for @Version field --- .../hsadminng/rbac/rbacdef/RbacView.java | 14 ++++++++++++-- .../hsadminng/test/cust/TestCustomerEntity.java | 2 +- .../db/changelog/113-test-customer-rbac.md | 2 +- .../db/changelog/113-test-customer-rbac.sql | 2 +- .../db/changelog/123-test-package-rbac.md | 2 +- .../db/changelog/123-test-package-rbac.sql | 2 +- .../db/changelog/133-test-domain-rbac.sql | 2 +- 7 files changed, 18 insertions(+), 8 deletions(-) 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 ba1d741d..caa2405d 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -20,6 +20,7 @@ import net.hostsharing.hsadminng.test.dom.TestDomainEntity; import net.hostsharing.hsadminng.test.pac.TestPackageEntity; import jakarta.persistence.Table; +import jakarta.persistence.Version; import jakarta.validation.constraints.NotNull; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -29,6 +30,7 @@ import java.util.function.Consumer; import java.util.stream.Stream; import static java.lang.reflect.Modifier.isStatic; +import static java.util.Arrays.stream; 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; @@ -76,7 +78,7 @@ public class RbacView { public RbacView withUpdatableColumns(final String... columnNames) { Collections.addAll(updatableColumns, columnNames); - // TODO: automatically add @Version column, otherwise optimistic locking won't work + verifyVersionColumnExists(); return this; } @@ -214,6 +216,14 @@ public class RbacView { return this; } + private void verifyVersionColumnExists() { + if (stream(rootEntityAlias.entityClass.getDeclaredFields()) + .noneMatch(f -> f.getAnnotation(Version.class) != null)) { + // TODO: convert this into throw Exception once RbacEntity is a base class with @Version field + System.err.println("@Version field required in updatable entity " + rootEntityAlias.entityClass); + } + } + public RbacGrantBuilder toRole(final String entityAlias, final Role role) { return new RbacGrantBuilder(entityAlias, role); } @@ -801,7 +811,7 @@ public class RbacView { HsOfficeCoopSharesTransactionEntity.class, HsOfficeMembershipEntity.class ).forEach(c -> { - final Method mainMethod = Arrays.stream(c.getMethods()).filter( + final Method mainMethod = stream(c.getMethods()).filter( m -> isStatic(m.getModifiers()) && m.getName().equals("main") ) .findFirst() 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 bc6c7ae8..99b0fb3c 100644 --- a/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java @@ -41,7 +41,7 @@ public class TestCustomerEntity implements HasUuid { .withIdentityView(SQL.projection("prefix")) .withRestrictedViewOrderBy(SQL.expression("reference")) .withUpdatableColumns("reference", "prefix", "adminUserName") - // TODO: do we want explicit specification of parent-indenpendent insert permissions? + // TODO: do we want explicit specification of parent-independent insert permissions? // .toRole("global", ADMIN).grantPermission("customer", INSERT) .createRole(OWNER, (with) -> { diff --git a/src/main/resources/db/changelog/113-test-customer-rbac.md b/src/main/resources/db/changelog/113-test-customer-rbac.md index a585b153..f126a216 100644 --- a/src/main/resources/db/changelog/113-test-customer-rbac.md +++ b/src/main/resources/db/changelog/113-test-customer-rbac.md @@ -1,4 +1,4 @@ -### rbac customer 2024-03-10T11:42:41.089596517 +### rbac customer 2024-03-11T09:06:04.484587070 ```mermaid %%{init:{'flowchart':{'htmlLabels':false}}}%% diff --git a/src/main/resources/db/changelog/113-test-customer-rbac.sql b/src/main/resources/db/changelog/113-test-customer-rbac.sql index 25128963..da758029 100644 --- a/src/main/resources/db/changelog/113-test-customer-rbac.sql +++ b/src/main/resources/db/changelog/113-test-customer-rbac.sql @@ -1,5 +1,5 @@ --liquibase formatted sql --- This code generated was by RbacViewPostgresGenerator at 2024-03-10T11:42:41.121556631. +-- This code generated was by RbacViewPostgresGenerator at 2024-03-11T09:06:04.497071201. -- ============================================================================ --changeset test-customer-rbac-OBJECT:1 endDelimiter:--// diff --git a/src/main/resources/db/changelog/123-test-package-rbac.md b/src/main/resources/db/changelog/123-test-package-rbac.md index a8c0d66b..4b56651a 100644 --- a/src/main/resources/db/changelog/123-test-package-rbac.md +++ b/src/main/resources/db/changelog/123-test-package-rbac.md @@ -1,4 +1,4 @@ -### rbac package 2024-03-10T11:42:41.162678472 +### rbac package 2024-03-11T09:06:04.536081351 ```mermaid %%{init:{'flowchart':{'htmlLabels':false}}}%% diff --git a/src/main/resources/db/changelog/123-test-package-rbac.sql b/src/main/resources/db/changelog/123-test-package-rbac.sql index ad0359ff..371a28ff 100644 --- a/src/main/resources/db/changelog/123-test-package-rbac.sql +++ b/src/main/resources/db/changelog/123-test-package-rbac.sql @@ -1,5 +1,5 @@ --liquibase formatted sql --- This code generated was by RbacViewPostgresGenerator at 2024-03-10T11:42:41.163393064. +-- This code generated was by RbacViewPostgresGenerator at 2024-03-11T09:06:04.536525766. -- ============================================================================ --changeset test-package-rbac-OBJECT:1 endDelimiter:--// diff --git a/src/main/resources/db/changelog/133-test-domain-rbac.sql b/src/main/resources/db/changelog/133-test-domain-rbac.sql index a29a1b5a..c230bcde 100644 --- a/src/main/resources/db/changelog/133-test-domain-rbac.sql +++ b/src/main/resources/db/changelog/133-test-domain-rbac.sql @@ -1,5 +1,5 @@ --liquibase formatted sql --- This code generated was by RbacViewPostgresGenerator at 2024-03-10T11:42:41.186902574. +-- This code generated was by RbacViewPostgresGenerator at 2024-03-11T09:06:04.558752062. -- ============================================================================ --changeset test-domain-rbac-OBJECT:1 endDelimiter:--// -- 2.39.5 From c67af5948b6bd4a8fc6d855dc57a20b92a086155 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 11 Mar 2024 09:20:25 +0100 Subject: [PATCH 52/53] use XX for not-assumed --- .../hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java | 6 +++--- .../hsadminng/test/cust/TestCustomerController.java | 1 - .../partner/HsOfficePartnerRepositoryIntegrationTest.java | 1 - 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java index 57f86ded..0296cd61 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java @@ -127,9 +127,9 @@ public class RbacGrantsDiagramService { : ""; final var grants = graph.stream() - .map(g -> quoted(g.getAscendantIdName()) + - (g.isAssumed() ? " --> " : " -.-> ") + - quoted(g.getDescendantIdName())) + .map(g -> quoted(g.getAscendantIdName()) + + " -->" + (g.isAssumed() ? " " : "|XX| ") + + quoted(g.getDescendantIdName())) .sorted() .collect(joining("\n")); diff --git a/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerController.java b/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerController.java index 78752d9d..67607c83 100644 --- a/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerController.java +++ b/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerController.java @@ -53,7 +53,6 @@ public class TestCustomerController implements TestCustomersApi { context.define(currentUser, assumedRoles); final var saved = testCustomerRepository.save(mapper.map(customer, TestCustomerEntity.class)); - em.flush(); final var uri = MvcUriComponentsBuilder.fromController(getClass()) .path("/api/test/customers/{id}") diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java index f9ed63e4..94d06a77 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java @@ -473,7 +473,6 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean .contact(givenContact) .build(); relationshipRepo.save(partnerRole); - em.flush(); // TODO: why is that necessary? final var newPartner = HsOfficePartnerEntity.builder() .partnerNumber(partnerNumber) -- 2.39.5 From 7aa158e4066825d19f55505aea8cefbb164fbf44 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 11 Mar 2024 12:01:23 +0100 Subject: [PATCH 53/53] amendments according to code review --- .../hsadminng/rbac/rbacdef/RbacView.java | 2 +- ...=> RbacViewMermaidFlowchartGenerator.java} | 8 +- .../resources/db/changelog/010-context.sql | 13 +-- .../resources/db/changelog/020-audit-log.sql | 4 +- .../db/changelog/051-rbac-user-grant.sql | 2 - .../db/changelog/113-test-customer-rbac.md | 4 +- .../db/changelog/113-test-customer-rbac.sql | 2 +- .../db/changelog/123-test-package-rbac.md | 4 +- .../db/changelog/123-test-package-rbac.sql | 2 +- .../db/changelog/133-test-domain-rbac.md | 88 +++++++++++++++++++ .../db/changelog/133-test-domain-rbac.sql | 2 +- ...fficeDebitorRepositoryIntegrationTest.java | 1 - .../RbacGrantControllerAcceptanceTest.java | 2 + .../test/cust/TestCustomerEntityUnitTest.java | 4 +- .../test/pac/TestPackageEntityUnitTest.java | 4 +- 15 files changed, 119 insertions(+), 23 deletions(-) rename src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/{RbacViewMermaidFlowchart.java => RbacViewMermaidFlowchartGenerator.java} (95%) create mode 100644 src/main/resources/db/changelog/133-test-domain-rbac.md 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 caa2405d..28d29365 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -264,7 +264,7 @@ public class RbacView { } public void generateWithBaseFileName(final String baseFileName) { - new RbacViewMermaidFlowchart(this).generateToMarkdownFile(Path.of(OUTPUT_BASEDIR, baseFileName + ".md")); + new RbacViewMermaidFlowchartGenerator(this).generateToMarkdownFile(Path.of(OUTPUT_BASEDIR, baseFileName + ".md")); new RbacViewPostgresGenerator(this).generateToChangeLog(Path.of(OUTPUT_BASEDIR, baseFileName + ".sql")); } diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchartGenerator.java similarity index 95% rename from src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java rename to src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchartGenerator.java index 9a806cc4..ccef566d 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchartGenerator.java @@ -9,7 +9,7 @@ import java.time.LocalDateTime; import static java.util.stream.Collectors.joining; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.*; -public class RbacViewMermaidFlowchart { +public class RbacViewMermaidFlowchartGenerator { public static final String HOSTSHARING_DARK_ORANGE = "#dd4901"; public static final String HOSTSHARING_LIGHT_ORANGE = "#feb28c"; @@ -18,7 +18,7 @@ public class RbacViewMermaidFlowchart { private final RbacView rbacDef; private final StringWriter flowchart = new StringWriter(); - public RbacViewMermaidFlowchart(final RbacView rbacDef) { + public RbacViewMermaidFlowchartGenerator(final RbacView rbacDef) { this.rbacDef = rbacDef; flowchart.writeLn(""" %%{init:{'flowchart':{'htmlLabels':false}}}%% @@ -147,7 +147,9 @@ public class RbacViewMermaidFlowchart { Files.writeString( path, """ - ### rbac %{entityAlias} %{timestamp} + ### rbac %{entityAlias} + + This code generated was by RbacViewMermaidFlowchartGenerator at %{timestamp}. ```mermaid %{flowchart} diff --git a/src/main/resources/db/changelog/010-context.sql b/src/main/resources/db/changelog/010-context.sql index d1129184..8de41891 100644 --- a/src/main/resources/db/changelog/010-context.sql +++ b/src/main/resources/db/changelog/010-context.sql @@ -24,23 +24,26 @@ end; $$; */ create or replace procedure defineContext( currentTask varchar(96), - currentRequest varchar(512) = null, - currentUser varchar = null, - assumedRoles varchar = null + currentRequest text = null, + currentUser varchar(63) = null, + assumedRoles varchar(256) = null ) language plpgsql as $$ begin - assert length(currentTask) <= 96, 'currentTask must not be longer than 96 characters'; - assert length(currentTask) > 8, 'currentTask must be at least 8 characters long'; + currentTask := coalesce(currentTask, ''); + assert length(currentTask) <= 96, FORMAT('currentTask must not be longer than 96 characters: "%s"', currentTask); + assert length(currentTask) > 8, FORMAT('currentTask must be at least 8 characters long: "%s""', currentTask); execute format('set local hsadminng.currentTask to %L', currentTask); currentRequest := coalesce(currentRequest, ''); execute format('set local hsadminng.currentRequest to %L', currentRequest); currentUser := coalesce(currentUser, ''); + assert length(currentUser) <= 63, FORMAT('currentUser must not be longer than 63 characters: "%s"', currentUser); execute format('set local hsadminng.currentUser to %L', currentUser); assumedRoles := coalesce(assumedRoles, ''); + assert length(assumedRoles) <= 256, FORMAT('assumedRoles must not be longer than 256 characters: "%s"', assumedRoles); execute format('set local hsadminng.assumedRoles to %L', assumedRoles); call contextDefined(currentTask, currentRequest, currentUser, assumedRoles); diff --git a/src/main/resources/db/changelog/020-audit-log.sql b/src/main/resources/db/changelog/020-audit-log.sql index 173e5741..ec14ad0d 100644 --- a/src/main/resources/db/changelog/020-audit-log.sql +++ b/src/main/resources/db/changelog/020-audit-log.sql @@ -27,9 +27,9 @@ create table tx_context txId bigint not null, txTimestamp timestamp not null, currentUser varchar(63) not null, -- not the uuid, because users can be deleted - assumedRoles varchar not null, -- not the uuids, because roles can be deleted + assumedRoles varchar(256) not null, -- not the uuids, because roles can be deleted currentTask varchar(96) not null, - currentRequest varchar(512) not null + currentRequest text not null ); create index on tx_context using brin (txTimestamp); diff --git a/src/main/resources/db/changelog/051-rbac-user-grant.sql b/src/main/resources/db/changelog/051-rbac-user-grant.sql index beeeb7d2..a82865c8 100644 --- a/src/main/resources/db/changelog/051-rbac-user-grant.sql +++ b/src/main/resources/db/changelog/051-rbac-user-grant.sql @@ -119,8 +119,6 @@ end; $$; create or replace procedure revokePermissionFromRole(permissionUuid uuid, superRoleUuid uuid) language plpgsql as $$ begin - -- TODO: call checkRevokeRoleFromUserPreconditions(grantedByRoleUuid, grantedRoleUuid, userUuid); - raise INFO 'delete from RbacGrants where ascendantUuid = % and descendantUuid = %', superRoleUuid, permissionUuid; delete from RbacGrants as g where g.ascendantUuid = superRoleUuid and g.descendantUuid = permissionUuid; diff --git a/src/main/resources/db/changelog/113-test-customer-rbac.md b/src/main/resources/db/changelog/113-test-customer-rbac.md index f126a216..7770e470 100644 --- a/src/main/resources/db/changelog/113-test-customer-rbac.md +++ b/src/main/resources/db/changelog/113-test-customer-rbac.md @@ -1,4 +1,6 @@ -### rbac customer 2024-03-11T09:06:04.484587070 +### rbac customer + +This code generated was by RbacViewMermaidFlowchartGenerator at 2024-03-11T11:29:11.571772062. ```mermaid %%{init:{'flowchart':{'htmlLabels':false}}}%% diff --git a/src/main/resources/db/changelog/113-test-customer-rbac.sql b/src/main/resources/db/changelog/113-test-customer-rbac.sql index da758029..6ae19710 100644 --- a/src/main/resources/db/changelog/113-test-customer-rbac.sql +++ b/src/main/resources/db/changelog/113-test-customer-rbac.sql @@ -1,5 +1,5 @@ --liquibase formatted sql --- This code generated was by RbacViewPostgresGenerator at 2024-03-11T09:06:04.497071201. +-- This code generated was by RbacViewPostgresGenerator at 2024-03-11T11:29:11.584886824. -- ============================================================================ --changeset test-customer-rbac-OBJECT:1 endDelimiter:--// diff --git a/src/main/resources/db/changelog/123-test-package-rbac.md b/src/main/resources/db/changelog/123-test-package-rbac.md index 4b56651a..78da4439 100644 --- a/src/main/resources/db/changelog/123-test-package-rbac.md +++ b/src/main/resources/db/changelog/123-test-package-rbac.md @@ -1,4 +1,6 @@ -### rbac package 2024-03-11T09:06:04.536081351 +### rbac package + +This code generated was by RbacViewMermaidFlowchartGenerator at 2024-03-11T11:29:11.624847792. ```mermaid %%{init:{'flowchart':{'htmlLabels':false}}}%% diff --git a/src/main/resources/db/changelog/123-test-package-rbac.sql b/src/main/resources/db/changelog/123-test-package-rbac.sql index 371a28ff..20562642 100644 --- a/src/main/resources/db/changelog/123-test-package-rbac.sql +++ b/src/main/resources/db/changelog/123-test-package-rbac.sql @@ -1,5 +1,5 @@ --liquibase formatted sql --- This code generated was by RbacViewPostgresGenerator at 2024-03-11T09:06:04.536525766. +-- This code generated was by RbacViewPostgresGenerator at 2024-03-11T11:29:11.625353859. -- ============================================================================ --changeset test-package-rbac-OBJECT:1 endDelimiter:--// diff --git a/src/main/resources/db/changelog/133-test-domain-rbac.md b/src/main/resources/db/changelog/133-test-domain-rbac.md new file mode 100644 index 00000000..bd5cf706 --- /dev/null +++ b/src/main/resources/db/changelog/133-test-domain-rbac.md @@ -0,0 +1,88 @@ +### rbac domain + +This code generated was by RbacViewMermaidFlowchartGenerator at 2024-03-11T11:29:11.644658132. + +```mermaid +%%{init:{'flowchart':{'htmlLabels':false}}}%% +flowchart TB + +subgraph package.customer["`**package.customer**`"] + direction TB + style package.customer fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph package.customer:roles[ ] + style package.customer:roles fill:#99bcdb,stroke:white + + role:package.customer:owner[[package.customer:owner]] + role:package.customer:admin[[package.customer:admin]] + role:package.customer:tenant[[package.customer:tenant]] + end +end + +subgraph package["`**package**`"] + direction TB + style package fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph package.customer["`**package.customer**`"] + direction TB + style package.customer fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph package.customer:roles[ ] + style package.customer:roles fill:#99bcdb,stroke:white + + role:package.customer:owner[[package.customer:owner]] + role:package.customer:admin[[package.customer:admin]] + role:package.customer:tenant[[package.customer:tenant]] + end + end + + subgraph package:roles[ ] + style package:roles fill:#99bcdb,stroke:white + + role:package:owner[[package:owner]] + role:package:admin[[package:admin]] + role:package:tenant[[package:tenant]] + end +end + +subgraph domain["`**domain**`"] + direction TB + style domain fill:#dd4901,stroke:#274d6e,stroke-width:8px + + subgraph domain:roles[ ] + style domain:roles fill:#dd4901,stroke:white + + role:domain:owner[[domain:owner]] + role:domain:admin[[domain:admin]] + end + + subgraph domain:permissions[ ] + style domain:permissions fill:#dd4901,stroke:white + + perm:domain:INSERT{{domain:INSERT}} + perm:domain:DELETE{{domain:DELETE}} + perm:domain:UPDATE{{domain:UPDATE}} + perm:domain:SELECT{{domain:SELECT}} + end +end + +%% granting roles to roles +role:global:admin -.->|XX| role:package.customer:owner +role:package.customer:owner -.-> role:package.customer:admin +role:package.customer:admin -.-> role:package.customer:tenant +role:package.customer:admin -.-> role:package:owner +role:package:owner -.-> role:package:admin +role:package:admin -.-> role:package:tenant +role:package:tenant -.-> role:package.customer:tenant +role:package:admin ==> role:domain:owner +role:domain:owner ==> role:package:tenant +role:domain:owner ==> role:domain:admin +role:domain:admin ==> role:package:tenant + +%% granting permissions to roles +role:package:admin ==> perm:domain:INSERT +role:domain:owner ==> perm:domain:DELETE +role:domain:owner ==> perm:domain:UPDATE +role:domain:admin ==> perm:domain:SELECT + +``` diff --git a/src/main/resources/db/changelog/133-test-domain-rbac.sql b/src/main/resources/db/changelog/133-test-domain-rbac.sql index c230bcde..e686dada 100644 --- a/src/main/resources/db/changelog/133-test-domain-rbac.sql +++ b/src/main/resources/db/changelog/133-test-domain-rbac.sql @@ -1,5 +1,5 @@ --liquibase formatted sql --- This code generated was by RbacViewPostgresGenerator at 2024-03-11T09:06:04.558752062. +-- This code generated was by RbacViewPostgresGenerator at 2024-03-11T11:29:11.645391647. -- ============================================================================ --changeset test-domain-rbac-OBJECT:1 endDelimiter:--// diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java index 7c2420ee..46d0878f 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java @@ -118,7 +118,6 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean }); // then - System.out.println("ok"); result.assertExceptionWithRootCauseMessage(org.hibernate.exception.ConstraintViolationException.class); } diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantControllerAcceptanceTest.java index fdf7e693..f56baf34 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantControllerAcceptanceTest.java @@ -73,6 +73,7 @@ class RbacGrantControllerAcceptanceTest extends ContextBasedTest { .contentType("application/json") .body("", hasItem( allOf( + // TODO: should there be a grantedByRole or just a grantedByTrigger? hasEntry("grantedByRoleIdName", "test_customer#xxx.owner"), hasEntry("grantedRoleIdName", "test_customer#xxx.admin"), hasEntry("granteeUserName", "customer-admin@xxx.example.com") @@ -80,6 +81,7 @@ class RbacGrantControllerAcceptanceTest extends ContextBasedTest { )) .body("", hasItem( allOf( + // TODO: should there be a grantedByRole or just a grantedByTrigger? hasEntry("grantedByRoleIdName", "test_customer#yyy.owner"), hasEntry("grantedRoleIdName", "test_customer#yyy.admin"), hasEntry("granteeUserName", "customer-admin@yyy.example.com") diff --git a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityUnitTest.java index 4dfcc183..eca0aec1 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityUnitTest.java @@ -1,6 +1,6 @@ package net.hostsharing.hsadminng.test.cust; -import net.hostsharing.hsadminng.rbac.rbacdef.RbacViewMermaidFlowchart; +import net.hostsharing.hsadminng.rbac.rbacdef.RbacViewMermaidFlowchartGenerator; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -9,7 +9,7 @@ class TestCustomerEntityUnitTest { @Test void definesRbac() { - final var rbacFlowchart = new RbacViewMermaidFlowchart(TestCustomerEntity.rbac()).toString(); + final var rbacFlowchart = new RbacViewMermaidFlowchartGenerator(TestCustomerEntity.rbac()).toString(); assertThat(rbacFlowchart).isEqualTo(""" %%{init:{'flowchart':{'htmlLabels':false}}}%% flowchart TB diff --git a/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityUnitTest.java index 2546a8e8..c5dccfd3 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityUnitTest.java @@ -1,6 +1,6 @@ package net.hostsharing.hsadminng.test.pac; -import net.hostsharing.hsadminng.rbac.rbacdef.RbacViewMermaidFlowchart; +import net.hostsharing.hsadminng.rbac.rbacdef.RbacViewMermaidFlowchartGenerator; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -9,7 +9,7 @@ class TestPackageEntityUnitTest { @Test void definesRbac() { - final var rbacFlowchart = new RbacViewMermaidFlowchart(TestPackageEntity.rbac()).toString(); + final var rbacFlowchart = new RbacViewMermaidFlowchartGenerator(TestPackageEntity.rbac()).toString(); assertThat(rbacFlowchart).isEqualTo(""" %%{init:{'flowchart':{'htmlLabels':false}}}%% flowchart TB -- 2.39.5