From 001ab652c79ed99f1f1bfa77cd46224a17b3831f Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 4 Apr 2024 10:00:13 +0200 Subject: [PATCH] working version with conditional generation --- .../relation/HsOfficeRelationEntity.java | 6 +- .../rbac/rbacdef/ConditionGenerator.java | 75 ++++++++++++ .../hsadminng/rbac/rbacdef/RbacView.java | 111 ++++++++++-------- .../RbacViewMermaidFlowchartGenerator.java | 15 ++- .../RolesGrantsAndPermissionsGenerator.java | 32 ++++- .../5033-hs-office-relation-rbac-DEBITOR.md | 91 ++++++++++++++ ...-hs-office-relation-rbac-REPRESENTATIVE.md | 103 ++++++++++++++++ .../5033-hs-office-relation-rbac.md | 7 +- .../5033-hs-office-relation-rbac.sql | 91 ++++++-------- .../rbacdef/ConditionGeneratorUnitTest.java | 46 ++++++++ 10 files changed, 463 insertions(+), 114 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/ConditionGenerator.java create mode 100644 src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac-DEBITOR.md create mode 100644 src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac-REPRESENTATIVE.md create mode 100644 src/test/java/net/hostsharing/hsadminng/rbac/rbacdef/ConditionGeneratorUnitTest.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntity.java index 2202e366..e18a8cd4 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntity.java @@ -15,6 +15,8 @@ import jakarta.persistence.Column; import java.io.IOException; import java.util.UUID; +import static net.hostsharing.hsadminng.rbac.rbacdef.ConditionGenerator.CaseDef.inCaseOf; +import static net.hostsharing.hsadminng.rbac.rbacdef.ConditionGenerator.CaseDef.inOtherCases; 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.Nullable.NOT_NULL; @@ -110,11 +112,11 @@ public class HsOfficeRelationEntity implements RbacObject, Stringifyable { with.permission(DELETE); }) .createSubRole(ADMIN, (with) -> { - with.incomingSuperRole("anchorPerson", ADMIN); with.outgoingSubRole("anchorPerson", OWNER); with.permission(UPDATE); }) .createSubRole(AGENT, (with) -> { + with.incomingSuperRole("anchorPerson", ADMIN); }) .createSubRole(TENANT, (with) -> { with.incomingSuperRole("holderPerson", ADMIN); @@ -125,7 +127,7 @@ public class HsOfficeRelationEntity implements RbacObject, Stringifyable { with.permission(SELECT); }); }), - //FIXME: .inCaseOf("DEBITOR") + inCaseOf("DEBITOR", then -> {}), inOtherCases(then -> { then.createRole(OWNER, (with) -> { with.owningUser(CREATOR); diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/ConditionGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/ConditionGenerator.java new file mode 100644 index 00000000..0ea70b02 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/ConditionGenerator.java @@ -0,0 +1,75 @@ +package net.hostsharing.hsadminng.rbac.rbacdef; + +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import static java.lang.String.join; +import static java.util.Optional.ofNullable; + +public class ConditionGenerator { + + private final String discriminatorColumName; + private final Set allCases; + + public ConditionGenerator(final String discriminatorColumName, final Set allCases) { + this.discriminatorColumName = discriminatorColumName; + this.allCases = allCases; + } + + public String generatePlPgSql(final Set forCases) { + if (forCases.size() == 1) { + if (forCases.iterator().next().isDefaultCase()) { + final var nonDefaultCases = allCases.stream().filter(c -> !c.isDefaultCase()).map(c -> c.val).toList(); + if (nonDefaultCases.size() > 1) { + return discriminatorColumName + " not in ('" + join("', '", nonDefaultCases) + "')"; + } + return discriminatorColumName + " <> '" + nonDefaultCases.getFirst() + "'"; + } + return discriminatorColumName + " = '" + forCases.iterator().next().val + "'"; + } + return discriminatorColumName + " in ('" + forCases.stream().map(c -> c.val).collect(Collectors.joining("', '")) + "')"; + } + + + + public static class CaseDef { + + final String val; + final Consumer def; + + private CaseDef(final String discriminatorColumnValue, final Consumer def) { + this.val = discriminatorColumnValue; + this.def = def; + } + + + public static CaseDef inCaseOf(final String discriminatorColumnValue, final Consumer def) { + return new CaseDef(discriminatorColumnValue, def); + } + + public static CaseDef inOtherCases(final Consumer def) { + return new CaseDef(null, def); + } + + @Override + public int hashCode() { + return ofNullable(val).map(String::hashCode).orElse(0); + } + + @Override + public boolean equals(final Object other) { + if (this == other) + return true; + if (other == null || getClass() != other.getClass()) + return false; + final CaseDef caseDef = (CaseDef) other; + return Objects.equals(val, caseDef.val); + } + + boolean isDefaultCase() { + return val == null; + } + } +} 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 ab5e6b07..d5a225c5 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 net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity; import net.hostsharing.hsadminng.hs.office.sepamandate.HsOfficeSepaMandateEntity; +import net.hostsharing.hsadminng.rbac.rbacdef.ConditionGenerator.CaseDef; import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.test.cust.TestCustomerEntity; import net.hostsharing.hsadminng.test.dom.TestDomainEntity; @@ -31,9 +32,12 @@ 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.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.PERM_TO_ROLE; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.Part.AUTO_FETCH; +import static org.apache.commons.collections4.SetUtils.hashSet; import static org.apache.commons.lang3.StringUtils.uncapitalize; @Getter @@ -60,14 +64,17 @@ public class RbacView { }; private final Set updatableColumns = new LinkedHashSet<>(); private final Set grantDefs = new LinkedHashSet<>(); + private final Set allCases = new LinkedHashSet<>(); + private String discriminatorColumName; + private CaseDef processingCase; private SQL identityViewSqlQuery; private SQL orderBySqlExpression; private EntityAlias rootEntityAliasProxy; private RbacRoleDefinition previousRoleDef; - private final Map cases = new HashMap<>() { + private final Map cases = new LinkedHashMap<>() { @Override - public String put(final String key, final String value) { + public CaseDef put(final String key, final CaseDef value) { if (containsKey(key)) { throw new IllegalArgumentException("duplicate case: " + key); } @@ -247,7 +254,11 @@ public class RbacView { } private RbacPermissionDefinition createPermission(final EntityAlias entityAlias, final Permission permission) { - return new RbacPermissionDefinition(entityAlias, permission, null, true); + return permDefs.stream() + .filter(p -> p.permission == permission && p.entityAlias == entityAlias) + .findFirst() + // .map(g -> g.forCase(processingCase)) TODO: not implemented case dependent + .orElseGet(() -> new RbacPermissionDefinition(entityAlias, permission, null, true)); } public RbacView declarePlaceholderEntityAliases(final String... aliasNames) { @@ -292,7 +303,7 @@ public class RbacView { if (rootEntityAliasProxy != null) { throw new IllegalStateException("there is already an entityAliasProxy: " + rootEntityAliasProxy); } - rootEntityAliasProxy = importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, false, NOT_NULL); + rootEntityAliasProxy = importEntityAliasImpl(aliasName, entityClass, forCase, fetchSql, dependsOnColum, false, NOT_NULL); return this; } @@ -311,7 +322,7 @@ public class RbacView { public RbacView importSubEntityAlias( final String aliasName, final Class entityClass, final SQL fetchSql, final Column dependsOnColum) { - importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, true, NOT_NULL); + importEntityAliasImpl(aliasName, entityClass, usingDefaultCase(), fetchSql, dependsOnColum, true, NOT_NULL); return this; } @@ -345,17 +356,17 @@ public class RbacView { public RbacView importEntityAlias( final String aliasName, final Class entityClass, final Column dependsOnColum, final SQL fetchSql, final Nullable nullable) { - importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, false, nullable); + importEntityAliasImpl(aliasName, entityClass, usingDefaultCase(), fetchSql, dependsOnColum, false, nullable); return this; } private EntityAlias importEntityAliasImpl( - final String aliasName, final Class entityClass, + final String aliasName, final Class entityClass, final ColumnValue forCase, final SQL fetchSql, final Column dependsOnColum, boolean asSubEntity, final Nullable nullable) { final var entityAlias = new EntityAlias(aliasName, entityClass, fetchSql, dependsOnColum, asSubEntity, nullable); entityAliases.put(aliasName, entityAlias); try { - importAsAlias(aliasName, rbacDefinition(entityClass), asSubEntity); + importAsAlias(aliasName, rbacDefinition(entityClass), forCase, asSubEntity); } catch (final ReflectiveOperationException exc) { throw new RuntimeException("cannot import entity: " + entityClass, exc); } @@ -367,7 +378,7 @@ public class RbacView { return (RbacView) entityClass.getMethod("rbac").invoke(null); } - private RbacView importAsAlias(final String aliasName, final RbacView importedRbacView, final boolean asSubEntity) { + private RbacView importAsAlias(final String aliasName, final RbacView importedRbacView, final ColumnValue forCase, final boolean asSubEntity) { final var mapper = new AliasNameMapper(importedRbacView, aliasName, asSubEntity ? entityAliases.keySet() : null); importedRbacView.getEntityAliases().values().stream() @@ -382,7 +393,8 @@ public class RbacView { new RbacRoleDefinition(findEntityAlias(mapper.map(roleDef.entityAlias.aliasName)), roleDef.role); }); importedRbacView.getGrantDefs().forEach(grantDef -> { - if (grantDef.grantType() == RbacGrantDefinition.GrantType.ROLE_TO_ROLE) { + if ( grantDef.matchesCase(forCase) && + grantDef.grantType() == RbacGrantDefinition.GrantType.ROLE_TO_ROLE) { final var importedGrantDef = findOrCreateGrantDef( findRbacRole( mapper.map(grantDef.getSubRoleDef().entityAlias.aliasName), @@ -400,8 +412,15 @@ public class RbacView { } public RbacView switchOnColumn(final String discriminatorColumName, final CaseDef... caseDefs) { + this.discriminatorColumName = discriminatorColumName; + allCases.addAll(stream(caseDefs).toList()); + // FIXME: currently only the default case is executed - stream(caseDefs).filter(caseDef -> caseDef.val == null).findAny().orElseThrow().def.accept(this); + stream(caseDefs).forEach(caseDef -> { + this.processingCase = caseDef; + caseDef.def.accept(this); + this.processingCase = null; + }); return this; } @@ -463,6 +482,12 @@ public class RbacView { } public void generateWithBaseFileName(final String baseFileName) { + if (allCases.size() > 1) { + allCases.forEach(c -> { + final var fileName = baseFileName + (c.isDefaultCase() ? "" : "-" + c.val) + ".md"; + new RbacViewMermaidFlowchartGenerator(this, c).generateToMarkdownFile(Path.of(OUTPUT_BASEDIR, fileName)); + }); + } new RbacViewMermaidFlowchartGenerator(this).generateToMarkdownFile(Path.of(OUTPUT_BASEDIR, baseFileName + ".md")); new RbacViewPostgresGenerator(this).generateToChangeLog(Path.of(OUTPUT_BASEDIR, baseFileName + ".sql")); } @@ -488,25 +513,6 @@ public class RbacView { } - public static class CaseDef { - - private final String val; - private final Consumer def; - - public CaseDef(final String discriminatorColumnValue, final Consumer def) { - this.val = discriminatorColumnValue; - this.def = def; - } - } - - public static CaseDef inCaseOf(final String discriminatorColumnValue, final Consumer def) { - return new CaseDef(discriminatorColumnValue, def); - } - - public static CaseDef inOtherCases(final Consumer def) { - return new CaseDef(null, def); - } - public enum Nullable { NOT_NULL, // DEFAULT NULLABLE @@ -522,8 +528,7 @@ public class RbacView { private final RbacPermissionDefinition permDef; private boolean assumed = true; private boolean toCreate = false; - private String onlyInCaseOf; - private String exceptInCaseOf; + private Set forCases = new HashSet<>(); @Override public String toString() { @@ -537,11 +542,12 @@ public class RbacView { }; } - RbacGrantDefinition(final RbacRoleDefinition subRoleDef, final RbacRoleDefinition superRoleDef) { + RbacGrantDefinition(final RbacRoleDefinition subRoleDef, final RbacRoleDefinition superRoleDef, final CaseDef forCase) { this.userDef = null; this.subRoleDef = subRoleDef; this.superRoleDef = superRoleDef; this.permDef = null; + this.forCases = hashSet(forCase); register(this); } @@ -567,7 +573,7 @@ public class RbacView { @NotNull GrantType grantType() { - return permDef != null ? GrantType.PERM_TO_ROLE + return permDef != null ? PERM_TO_ROLE : userDef != null ? GrantType.ROLE_TO_USER : GrantType.ROLE_TO_ROLE; } @@ -576,8 +582,27 @@ public class RbacView { return assumed; } + + RbacGrantDefinition forCase(final CaseDef processingCase) { + forCases.add(processingCase); + return this; + } + boolean isConditional() { - return onlyInCaseOf != null; + return !forCases.isEmpty() && forCases.size() g.permDef == permDef && g.subRoleDef == roleDef) + .filter(g -> g.permDef == permDef && g.superRoleDef == roleDef) .findFirst() .orElseGet(() -> new RbacGrantDefinition(permDef, roleDef)); } @@ -851,7 +865,8 @@ public class RbacView { return grantDefs.stream() .filter(g -> g.subRoleDef == subRoleDefinition && g.superRoleDef == superRoleDefinition) .findFirst() - .orElseGet(() -> new RbacGrantDefinition(subRoleDefinition, superRoleDefinition)); + .map(g -> g.forCase(processingCase)) + .orElseGet(() -> new RbacGrantDefinition(subRoleDefinition, superRoleDefinition, processingCase)); } record EntityAlias(String aliasName, Class entityClass, SQL fetchSql, Column dependsOnColum, boolean isSubEntity, Nullable nullable) { diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchartGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchartGenerator.java index 4b6e1c14..e4dedc25 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchartGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchartGenerator.java @@ -1,6 +1,7 @@ package net.hostsharing.hsadminng.rbac.rbacdef; import lombok.SneakyThrows; +import net.hostsharing.hsadminng.rbac.rbacdef.ConditionGenerator.CaseDef; import org.apache.commons.lang3.StringUtils; import java.nio.file.*; @@ -15,10 +16,13 @@ public class RbacViewMermaidFlowchartGenerator { public static final String HOSTSHARING_DARK_BLUE = "#274d6e"; public static final String HOSTSHARING_LIGHT_BLUE = "#99bcdb"; private final RbacView rbacDef; + + private final CaseDef forCase; private final StringWriter flowchart = new StringWriter(); - public RbacViewMermaidFlowchartGenerator(final RbacView rbacDef) { + public RbacViewMermaidFlowchartGenerator(final RbacView rbacDef, final CaseDef forCase) { this.rbacDef = rbacDef; + this.forCase = forCase; flowchart.writeLn(""" %%{init:{'flowchart':{'htmlLabels':false}}}%% flowchart TB @@ -26,6 +30,10 @@ public class RbacViewMermaidFlowchartGenerator { renderEntitySubgraphs(); renderGrants(); } + + public RbacViewMermaidFlowchartGenerator(final RbacView rbacDef) { + this(rbacDef, null); + } private void renderEntitySubgraphs() { rbacDef.getEntityAliases().values().stream() .filter(entityAlias -> !rbacDef.isEntityAliasProxy(entityAlias)) @@ -99,6 +107,7 @@ public class RbacViewMermaidFlowchartGenerator { private void renderGrants(final RbacView.RbacGrantDefinition.GrantType grantType, final String comment) { final var grantsOfRequestedType = rbacDef.getGrantDefs().stream() .filter(g -> g.grantType() == grantType) + .filter(g -> g.matchesCase(forCase)) .toList(); if ( !grantsOfRequestedType.isEmpty()) { flowchart.ensureSingleEmptyLine(); @@ -109,9 +118,7 @@ public class RbacViewMermaidFlowchartGenerator { private String grantDef(final RbacView.RbacGrantDefinition grant) { final var arrow = (grant.isToCreate() ? " ==>" : " -.->") - + (grant.isConditional() - ? (grant.isAssumed() ? " |??| " : "|?XX?| ") - : (grant.isAssumed() ? " " : "|XX| ")); + + (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 4c4ce134..c755d1c5 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java @@ -1,11 +1,13 @@ package net.hostsharing.hsadminng.rbac.rbacdef; +import net.hostsharing.hsadminng.rbac.rbacdef.ConditionGenerator.CaseDef; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacPermissionDefinition; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import static java.util.Optional.ofNullable; @@ -189,7 +191,25 @@ class RolesGrantsAndPermissionsGenerator { createRolesWithGrantsSql(plPgSql, REFERRER); generateGrants(plPgSql, ROLE_TO_USER); + generateGrants(plPgSql, ROLE_TO_ROLE); + if (!rbacDef.getAllCases().isEmpty()) { + plPgSql.writeLn(); + final var ifOrElsIf = new AtomicReference<>("IF "); + rbacDef.getAllCases().forEach(caseDef -> { + if (caseDef.val != null) { + plPgSql.writeLn(ifOrElsIf + rbacDef.getDiscriminatorColumName() + " = '" + caseDef.val + "' THEN"); + } else { + plPgSql.writeLn("ELSE"); + } + plPgSql.indented(() -> { + generateGrants(plPgSql, ROLE_TO_ROLE, caseDef); + }); + ifOrElsIf.set("ELSIF "); + }); + plPgSql.writeLn("END IF;"); + } + generateGrants(plPgSql, PERM_TO_ROLE); } @@ -267,9 +287,19 @@ class RolesGrantsAndPermissionsGenerator { return isInsertPermissionGrant; } + private void generateGrants(final StringWriter plPgSql, final RbacGrantDefinition.GrantType grantType, final CaseDef caseDef) { + rbacGrants.stream() + .filter(g -> g.matchesCase(caseDef)) + .filter(g -> g.grantType() == grantType) + .map(this::generateGrant) + .sorted() + .forEach(text -> plPgSql.writeLn(text, with("ref", NEW.name()))); + } + private void generateGrants(final StringWriter plPgSql, final RbacGrantDefinition.GrantType grantType) { plPgSql.ensureSingleEmptyLine(); rbacGrants.stream() + .filter(g -> !g.isConditional()) .filter(g -> g.grantType() == grantType) .map(this::generateGrant) .sorted() @@ -302,7 +332,7 @@ class RolesGrantsAndPermissionsGenerator { .replace("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef())); }; // if (grantDef.isConditional()) { -// return "if " + grantDef.getOnlyInCaseOf() + " then\n" +// return "if " + grantDef.generateCondition() + " then\n" // + " " + grantSql + "\n" // + "end if;"; // } diff --git a/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac-DEBITOR.md b/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac-DEBITOR.md new file mode 100644 index 00000000..f3ffabdf --- /dev/null +++ b/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac-DEBITOR.md @@ -0,0 +1,91 @@ +### rbac relation + +This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually. + +```mermaid +%%{init:{'flowchart':{'htmlLabels':false}}}%% +flowchart TB + +subgraph holderPerson["`**holderPerson**`"] + direction TB + style holderPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph holderPerson:roles[ ] + style holderPerson:roles fill:#99bcdb,stroke:white + + role:holderPerson:OWNER[[holderPerson:OWNER]] + role:holderPerson:ADMIN[[holderPerson:ADMIN]] + role:holderPerson:REFERRER[[holderPerson:REFERRER]] + end +end + +subgraph anchorPerson["`**anchorPerson**`"] + direction TB + style anchorPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph anchorPerson:roles[ ] + style anchorPerson:roles fill:#99bcdb,stroke:white + + role:anchorPerson:OWNER[[anchorPerson:OWNER]] + role:anchorPerson:ADMIN[[anchorPerson:ADMIN]] + role:anchorPerson:REFERRER[[anchorPerson:REFERRER]] + end +end + +subgraph contact["`**contact**`"] + direction TB + style contact fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph contact:roles[ ] + style contact:roles fill:#99bcdb,stroke:white + + role:contact:OWNER[[contact:OWNER]] + role:contact:ADMIN[[contact:ADMIN]] + role:contact:REFERRER[[contact:REFERRER]] + end +end + +subgraph relation["`**relation**`"] + direction TB + style relation fill:#dd4901,stroke:#274d6e,stroke-width:8px + + subgraph relation:roles[ ] + style relation:roles fill:#dd4901,stroke:white + + role:relation:OWNER[[relation:OWNER]] + role:relation:ADMIN[[relation:ADMIN]] + role:relation:AGENT[[relation:AGENT]] + role:relation:TENANT[[relation:TENANT]] + end + + subgraph relation:permissions[ ] + style relation:permissions fill:#dd4901,stroke:white + + perm:relation:DELETE{{relation:DELETE}} + perm:relation:UPDATE{{relation:UPDATE}} + perm:relation:SELECT{{relation:SELECT}} + perm:relation:INSERT{{relation:INSERT}} + end +end + +%% granting roles to users +user:creator ==> role:relation:OWNER + +%% granting roles to roles +role:global:ADMIN -.-> role:anchorPerson:OWNER +role:anchorPerson:OWNER -.-> role:anchorPerson:ADMIN +role:anchorPerson:ADMIN -.-> role:anchorPerson:REFERRER +role:global:ADMIN -.-> role:holderPerson:OWNER +role:holderPerson:OWNER -.-> role:holderPerson:ADMIN +role:holderPerson:ADMIN -.-> role:holderPerson:REFERRER +role:global:ADMIN -.-> role:contact:OWNER +role:contact:OWNER -.-> role:contact:ADMIN +role:contact:ADMIN -.-> role:contact:REFERRER + +%% granting permissions to roles +role:relation:OWNER ==> perm:relation:DELETE +role:relation:ADMIN ==> perm:relation:UPDATE +role:relation:TENANT ==> perm:relation:SELECT +role:anchorPerson:ADMIN ==> perm:relation:INSERT + +``` diff --git a/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac-REPRESENTATIVE.md b/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac-REPRESENTATIVE.md new file mode 100644 index 00000000..067355a8 --- /dev/null +++ b/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac-REPRESENTATIVE.md @@ -0,0 +1,103 @@ +### rbac relation + +This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually. + +```mermaid +%%{init:{'flowchart':{'htmlLabels':false}}}%% +flowchart TB + +subgraph holderPerson["`**holderPerson**`"] + direction TB + style holderPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph holderPerson:roles[ ] + style holderPerson:roles fill:#99bcdb,stroke:white + + role:holderPerson:OWNER[[holderPerson:OWNER]] + role:holderPerson:ADMIN[[holderPerson:ADMIN]] + role:holderPerson:REFERRER[[holderPerson:REFERRER]] + end +end + +subgraph anchorPerson["`**anchorPerson**`"] + direction TB + style anchorPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph anchorPerson:roles[ ] + style anchorPerson:roles fill:#99bcdb,stroke:white + + role:anchorPerson:OWNER[[anchorPerson:OWNER]] + role:anchorPerson:ADMIN[[anchorPerson:ADMIN]] + role:anchorPerson:REFERRER[[anchorPerson:REFERRER]] + end +end + +subgraph contact["`**contact**`"] + direction TB + style contact fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph contact:roles[ ] + style contact:roles fill:#99bcdb,stroke:white + + role:contact:OWNER[[contact:OWNER]] + role:contact:ADMIN[[contact:ADMIN]] + role:contact:REFERRER[[contact:REFERRER]] + end +end + +subgraph relation["`**relation**`"] + direction TB + style relation fill:#dd4901,stroke:#274d6e,stroke-width:8px + + subgraph relation:roles[ ] + style relation:roles fill:#dd4901,stroke:white + + role:relation:OWNER[[relation:OWNER]] + role:relation:ADMIN[[relation:ADMIN]] + role:relation:AGENT[[relation:AGENT]] + role:relation:TENANT[[relation:TENANT]] + end + + subgraph relation:permissions[ ] + style relation:permissions fill:#dd4901,stroke:white + + perm:relation:DELETE{{relation:DELETE}} + perm:relation:UPDATE{{relation:UPDATE}} + perm:relation:SELECT{{relation:SELECT}} + perm:relation:INSERT{{relation:INSERT}} + end +end + +%% granting roles to users +user:creator ==> role:relation:OWNER + +%% granting roles to roles +role:global:ADMIN -.-> role:anchorPerson:OWNER +role:anchorPerson:OWNER -.-> role:anchorPerson:ADMIN +role:anchorPerson:ADMIN -.-> role:anchorPerson:REFERRER +role:global:ADMIN -.-> role:holderPerson:OWNER +role:holderPerson:OWNER -.-> role:holderPerson:ADMIN +role:holderPerson:ADMIN -.-> role:holderPerson:REFERRER +role:global:ADMIN -.-> role:contact:OWNER +role:contact:OWNER -.-> role:contact:ADMIN +role:contact:ADMIN -.-> role:contact:REFERRER +role:global:ADMIN ==> role:relation:OWNER +role:holderPerson:ADMIN ==> role:relation:OWNER +role:relation:OWNER ==> role:relation:ADMIN +role:relation:ADMIN ==> role:anchorPerson:OWNER +role:relation:ADMIN ==> role:relation:AGENT +role:anchorPerson:ADMIN ==> role:relation:AGENT +role:relation:AGENT ==> role:relation:TENANT +role:holderPerson:ADMIN ==> role:relation:TENANT +role:contact:ADMIN ==> role:relation:TENANT +role:relation:TENANT ==> role:anchorPerson:REFERRER +role:relation:TENANT ==> role:holderPerson:REFERRER +role:relation:TENANT ==> role:contact:REFERRER + +%% granting permissions to roles +role:relation:OWNER ==> perm:relation:DELETE +role:relation:ADMIN ==> perm:relation:UPDATE +role:relation:TENANT ==> perm:relation:SELECT +role:anchorPerson:ADMIN ==> perm:relation:INSERT + +``` diff --git a/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.md b/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.md index 8014cdaf..b598df88 100644 --- a/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.md +++ b/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.md @@ -82,16 +82,19 @@ role:global:ADMIN -.-> role:contact:OWNER role:contact:OWNER -.-> role:contact:ADMIN role:contact:ADMIN -.-> role:contact:REFERRER role:global:ADMIN ==> role:relation:OWNER +role:holderPerson:ADMIN ==> role:relation:OWNER role:relation:OWNER ==> role:relation:ADMIN -role:anchorPerson:ADMIN ==> role:relation:ADMIN +role:relation:ADMIN ==> role:anchorPerson:OWNER role:relation:ADMIN ==> role:relation:AGENT -role:holderPerson:ADMIN ==> role:relation:AGENT +role:anchorPerson:ADMIN ==> role:relation:AGENT role:relation:AGENT ==> role:relation:TENANT role:holderPerson:ADMIN ==> role:relation:TENANT role:contact:ADMIN ==> role:relation:TENANT role:relation:TENANT ==> role:anchorPerson:REFERRER role:relation:TENANT ==> role:holderPerson:REFERRER role:relation:TENANT ==> role:contact:REFERRER +role:anchorPerson:ADMIN ==> role:relation:ADMIN +role:holderPerson:ADMIN ==> role:relation:AGENT %% granting permissions to roles role:relation:OWNER ==> perm:relation:DELETE diff --git a/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.sql b/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.sql index ff890a59..50cf960e 100644 --- a/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.sql +++ b/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.sql @@ -50,38 +50,51 @@ begin perform createRoleWithGrants( hsOfficeRelationOWNER(NEW), permissions => array['DELETE'], - incomingSuperRoles => array[globalADMIN()], userUuids => array[currentUserUuid()] ); perform createRoleWithGrants( hsOfficeRelationADMIN(NEW), - permissions => array['UPDATE'], - incomingSuperRoles => array[ - hsOfficePersonADMIN(newAnchorPerson), - hsOfficeRelationOWNER(NEW)] + permissions => array['UPDATE'] ); perform createRoleWithGrants( - hsOfficeRelationAGENT(NEW), - incomingSuperRoles => array[ - hsOfficePersonADMIN(newHolderPerson), - hsOfficeRelationADMIN(NEW)] + hsOfficeRelationAGENT(NEW) ); perform createRoleWithGrants( hsOfficeRelationTENANT(NEW), - permissions => array['SELECT'], - incomingSuperRoles => array[ - hsOfficeContactADMIN(newContact), - hsOfficePersonADMIN(newHolderPerson), - hsOfficeRelationAGENT(NEW)], - outgoingSubRoles => array[ - hsOfficeContactREFERRER(newContact), - hsOfficePersonREFERRER(newAnchorPerson), - hsOfficePersonREFERRER(newHolderPerson)] + permissions => array['SELECT'] ); + IF type = 'REPRESENTATIVE' THEN + call grantRoleToRole(hsOfficeContactREFERRER(newContact), hsOfficeRelationTENANT(NEW)); + call grantRoleToRole(hsOfficePersonOWNER(newAnchorPerson), hsOfficeRelationADMIN(NEW)); + call grantRoleToRole(hsOfficePersonREFERRER(newAnchorPerson), hsOfficeRelationTENANT(NEW)); + call grantRoleToRole(hsOfficePersonREFERRER(newHolderPerson), hsOfficeRelationTENANT(NEW)); + call grantRoleToRole(hsOfficeRelationADMIN(NEW), hsOfficeRelationOWNER(NEW)); + call grantRoleToRole(hsOfficeRelationAGENT(NEW), hsOfficePersonADMIN(newAnchorPerson)); + call grantRoleToRole(hsOfficeRelationAGENT(NEW), hsOfficeRelationADMIN(NEW)); + call grantRoleToRole(hsOfficeRelationOWNER(NEW), globalAdmin()); + call grantRoleToRole(hsOfficeRelationOWNER(NEW), hsOfficePersonADMIN(newHolderPerson)); + call grantRoleToRole(hsOfficeRelationTENANT(NEW), hsOfficeContactADMIN(newContact)); + call grantRoleToRole(hsOfficeRelationTENANT(NEW), hsOfficePersonADMIN(newHolderPerson)); + call grantRoleToRole(hsOfficeRelationTENANT(NEW), hsOfficeRelationAGENT(NEW)); + ELSIF type = 'DEBITOR' THEN + ELSE + call grantRoleToRole(hsOfficeContactREFERRER(newContact), hsOfficeRelationTENANT(NEW)); + call grantRoleToRole(hsOfficePersonREFERRER(newAnchorPerson), hsOfficeRelationTENANT(NEW)); + call grantRoleToRole(hsOfficePersonREFERRER(newHolderPerson), hsOfficeRelationTENANT(NEW)); + call grantRoleToRole(hsOfficeRelationADMIN(NEW), hsOfficePersonADMIN(newAnchorPerson)); + call grantRoleToRole(hsOfficeRelationADMIN(NEW), hsOfficeRelationOWNER(NEW)); + call grantRoleToRole(hsOfficeRelationAGENT(NEW), hsOfficePersonADMIN(newHolderPerson)); + call grantRoleToRole(hsOfficeRelationAGENT(NEW), hsOfficeRelationADMIN(NEW)); + call grantRoleToRole(hsOfficeRelationOWNER(NEW), globalAdmin()); + call grantRoleToRole(hsOfficeRelationTENANT(NEW), hsOfficeContactADMIN(newContact)); + call grantRoleToRole(hsOfficeRelationTENANT(NEW), hsOfficePersonADMIN(newHolderPerson)); + call grantRoleToRole(hsOfficeRelationTENANT(NEW), hsOfficeRelationAGENT(NEW)); + END IF; + call leaveTriggerForObjectUuid(NEW.uuid); end; $$; @@ -118,48 +131,12 @@ create or replace procedure updateRbacRulesForHsOfficeRelation( NEW hs_office_relation ) language plpgsql as $$ - -declare - oldHolderPerson hs_office_person; - newHolderPerson hs_office_person; - oldAnchorPerson hs_office_person; - newAnchorPerson hs_office_person; - oldContact hs_office_contact; - newContact hs_office_contact; - begin - call enterTriggerForObjectUuid(NEW.uuid); - - SELECT * FROM hs_office_person WHERE uuid = OLD.holderUuid INTO oldHolderPerson; - assert oldHolderPerson.uuid is not null, format('oldHolderPerson must not be null for OLD.holderUuid = %s', OLD.holderUuid); - - SELECT * FROM hs_office_person WHERE uuid = NEW.holderUuid INTO newHolderPerson; - assert newHolderPerson.uuid is not null, format('newHolderPerson must not be null for NEW.holderUuid = %s', NEW.holderUuid); - - SELECT * FROM hs_office_person WHERE uuid = OLD.anchorUuid INTO oldAnchorPerson; - assert oldAnchorPerson.uuid is not null, format('oldAnchorPerson must not be null for OLD.anchorUuid = %s', OLD.anchorUuid); - - SELECT * FROM hs_office_person WHERE uuid = NEW.anchorUuid INTO newAnchorPerson; - assert newAnchorPerson.uuid is not null, format('newAnchorPerson must not be null for NEW.anchorUuid = %s', NEW.anchorUuid); - - SELECT * FROM hs_office_contact WHERE uuid = OLD.contactUuid INTO oldContact; - assert oldContact.uuid is not null, format('oldContact must not be null for OLD.contactUuid = %s', OLD.contactUuid); - - SELECT * FROM hs_office_contact WHERE uuid = NEW.contactUuid INTO newContact; - assert newContact.uuid is not null, format('newContact must not be null for NEW.contactUuid = %s', NEW.contactUuid); - - - if NEW.contactUuid <> OLD.contactUuid then - - call revokeRoleFromRole(hsOfficeRelationTENANT(OLD), hsOfficeContactADMIN(oldContact)); - call grantRoleToRole(hsOfficeRelationTENANT(NEW), hsOfficeContactADMIN(newContact)); - - call revokeRoleFromRole(hsOfficeContactREFERRER(oldContact), hsOfficeRelationTENANT(OLD)); - call grantRoleToRole(hsOfficeContactREFERRER(newContact), hsOfficeRelationTENANT(NEW)); + if NEW.contactUuid is distinct from OLD.contactUuid then + delete from rbacgrants g where g.grantedbytriggerof = OLD.uuid; + call buildRbacSystemForHsOfficeRelation(NEW); end if; - - call leaveTriggerForObjectUuid(NEW.uuid); end; $$; /* diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacdef/ConditionGeneratorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacdef/ConditionGeneratorUnitTest.java new file mode 100644 index 00000000..030a496f --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacdef/ConditionGeneratorUnitTest.java @@ -0,0 +1,46 @@ +package net.hostsharing.hsadminng.rbac.rbacdef; + +import net.hostsharing.hsadminng.rbac.rbacdef.ConditionGenerator.CaseDef; +import org.junit.jupiter.api.Test; + +import java.util.function.Consumer; + +import static net.hostsharing.hsadminng.rbac.rbacdef.ConditionGenerator.CaseDef.inCaseOf; +import static net.hostsharing.hsadminng.rbac.rbacdef.ConditionGenerator.CaseDef.inOtherCases; +import static org.apache.commons.collections4.SetUtils.hashSet; +import static org.assertj.core.api.Assertions.assertThat; + +class ConditionGeneratorUnitTest { + + static final Consumer CONSUMER_FAKE = null; + static final CaseDef CASE_A = inCaseOf("A", CONSUMER_FAKE); + static final CaseDef CASE_B = inCaseOf("B", CONSUMER_FAKE); + static final CaseDef DEFAULT_CASE = inOtherCases(CONSUMER_FAKE); + + static final ConditionGenerator GENERATOR_WITH_A_B_AND_DEFAULT = + new ConditionGenerator("someCol", hashSet(CASE_A, CASE_B, DEFAULT_CASE)); + + @Test + void onlyCase_A_from_A_B_and_DEFAULT() { + assertThat(GENERATOR_WITH_A_B_AND_DEFAULT.generatePlPgSql(hashSet(CASE_A))) + .isEqualTo("someCol = 'A'"); + } + + @Test + void case_A_and_B_from_A_B_and_DEFAULT() { + assertThat(GENERATOR_WITH_A_B_AND_DEFAULT.generatePlPgSql(hashSet(CASE_A, CASE_B))) + .isEqualTo("someCol in ('A', 'B')"); + } + + @Test + void case_DEFAULT_from_A_B_and_DEFAULT() { + assertThat(GENERATOR_WITH_A_B_AND_DEFAULT.generatePlPgSql(hashSet(DEFAULT_CASE))) + .isEqualTo("someCol not in ('A', 'B')"); + } + + @Test + void case_A_and_DEFAULT_from_A_B_and_DEFAULT() { + assertThat(GENERATOR_WITH_A_B_AND_DEFAULT.generatePlPgSql(hashSet(CASE_A, DEFAULT_CASE))) + .isEqualTo("someCol <> 'B'"); + } +}