From f11edc082d81fb6bfd889b3b54db5590aa4cd624 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 22 Feb 2024 18:30:31 +0100 Subject: [PATCH] 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])