RBAC Diagram+PostgreSQL Generator #21

Merged
hsh-michaelhoennig merged 54 commits from experimental-rbacview-generator into master 2024-03-11 12:30:44 +01:00
6 changed files with 91 additions and 50 deletions
Showing only changes of commit f45f88ba77 - Show all commits

View File

@ -4,6 +4,7 @@ import lombok.*;
import lombok.experimental.FieldNameConstants; import lombok.experimental.FieldNameConstants;
import net.hostsharing.hsadminng.errors.DisplayName; import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.persistence.HasUuid; import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable; import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.GenericGenerator; import org.hibernate.annotations.GenericGenerator;
@ -11,6 +12,10 @@ import org.hibernate.annotations.GenericGenerator;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.util.UUID; 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; import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity @Entity
@ -53,4 +58,21 @@ public class HsOfficeContactEntity implements Stringifyable, HasUuid {
public String toShortString() { public String toShortString() {
return label; 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
}
} }

View File

@ -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.HsOfficePartnerPatchResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerResource; 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.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.person.HsOfficePersonEntity;
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity; import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity;
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipRepository; import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipRepository;
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipType; import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipType;
import net.hostsharing.hsadminng.mapper.Mapper; import net.hostsharing.hsadminng.mapper.Mapper;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@ -158,7 +158,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
return entity; return entity;
} }
private <E extends HasUuid> E ref(final Class<E> entityClass, final UUID uuid) { private <E extends RbacObject> E ref(final Class<E> entityClass, final UUID uuid) {
try { try {
return em.getReference(entityClass, uuid); return em.getReference(entityClass, uuid);
} catch (final Throwable exc) { } catch (final Throwable exc) {

View File

@ -91,6 +91,9 @@ public class HsOfficeRelationshipEntity implements HasUuid, Stringifyable {
.importEntityAlias("holderPerson", HsOfficePersonEntity.class, .importEntityAlias("holderPerson", HsOfficePersonEntity.class,
fetchedBySql("select * from hs_office_person as p where p.uuid = ${REF}.relHolderUuid"), fetchedBySql("select * from hs_office_person as p where p.uuid = ${REF}.relHolderUuid"),
dependsOnColumn("relHolderUuid")) dependsOnColumn("relHolderUuid"))
.importEntityAlias("contact", HsOfficeContactEntity.class,
fetchedBySql("select * from hs_office_contact as c where c.uuid = ${REF}.contactUuid"),
dependsOnColumn("contactUuid"))
.createRole(OWNER) .createRole(OWNER)
.withCurrentUserAsOwner() .withCurrentUserAsOwner()
.withPermission(ALL) .withPermission(ALL)
@ -99,9 +102,14 @@ public class HsOfficeRelationshipEntity implements HasUuid, Stringifyable {
.createSubRole(ADMIN) .createSubRole(ADMIN)
.withPermission(EDIT) .withPermission(EDIT)
.createSubRole(AGENT) .createSubRole(AGENT)
.withIncomingSuperRole("holderPerson", ADMIN)
.createSubRole(TENANT) .createSubRole(TENANT)
.withPermission(VIEW) .withPermission(VIEW)
.withIncomingSuperRole("anchorPerson", ADMIN)
.withIncomingSuperRole("holderPerson", ADMIN)
.withIncomingSuperRole("contact", ADMIN)
.withOutgoingSubRole("anchorPerson", REFERRER)
.withOutgoingSubRole("holderPerson", REFERRER)
.withOutgoingSubRole("contact", REFERRER)
.pop(); .pop();
// @formatter:on // @formatter:on
} }

View File

@ -10,6 +10,7 @@ import java.lang.reflect.InvocationTargetException;
import java.util.*; import java.util.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR;
import static org.apache.commons.lang3.StringUtils.uncapitalize;
@Getter @Getter
public class RbacView { public class RbacView {
@ -18,9 +19,9 @@ public class RbacView {
private final EntityAlias entityAlias; private final EntityAlias entityAlias;
private final Set<RbacUserReference> userDefs = new HashSet<>(); private final Set<RbacUserReference> userDefs = new LinkedHashSet<>();
private final Set<RbacRoleDefinition> roleDefs = new HashSet<>(); private final Set<RbacRoleDefinition> roleDefs = new LinkedHashSet<>();
private final Set<RbacPermissionDefinition> permDefs = new HashSet<>(); private final Set<RbacPermissionDefinition> permDefs = new LinkedHashSet<>();
private final Map<String, EntityAlias> entityAliases = new HashMap<>() { private final Map<String, EntityAlias> entityAliases = new HashMap<>() {
@Override @Override
@ -31,8 +32,8 @@ public class RbacView {
return super.put(key, value); return super.put(key, value);
} }
}; };
private final Set<String> updatableColumns = new TreeSet<>(); private final Set<String> updatableColumns = new LinkedHashSet<>();
private final Set<RbacGrantDefinition> grantDefs = new HashSet<>(); private final Set<RbacGrantDefinition> grantDefs = new LinkedHashSet<>();
private SQL identityViewSqlQuery; private SQL identityViewSqlQuery;
private EntityAlias entityAliasProxy; private EntityAlias entityAliasProxy;
@ -340,6 +341,12 @@ public class RbacView {
return this; 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) { public RbacRoleDefinition createSubRole(final Role role) {
final var roleDef = findRbacRole(entityAlias, role).toCreate(); final var roleDef = findRbacRole(entityAlias, role).toCreate();
new RbacGrantDefinition(roleDef, this).toCreate(); new RbacGrantDefinition(roleDef, this).toCreate();
@ -415,6 +422,18 @@ public class RbacView {
boolean isPlaceholder() { boolean isPlaceholder() {
return entityClass == null; 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) { public record Role(String roleName) {

View File

@ -1,6 +1,8 @@
package net.hostsharing.hsadminng.rbac.rbacdef; 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.debitor.HsOfficeDebitorEntity;
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import java.io.IOException; import java.io.IOException;
@ -21,11 +23,9 @@ public class RbacViewMermaidFlowchart {
flowchart.append(""" flowchart.append("""
%%{init:{'flowchart':{'htmlLabels':false}}}%% %%{init:{'flowchart':{'htmlLabels':false}}}%%
flowchart TB flowchart TB
"""); """);
renderEntitySubgraphs(); renderEntitySubgraphs();
renderGrants(); renderGrants();
flowchart.append("```");
} }
private void renderEntitySubgraphs() { private void renderEntitySubgraphs() {
rbacDef.getEntityAliases().values().stream() rbacDef.getEntityAliases().values().stream()
@ -40,12 +40,16 @@ public class RbacViewMermaidFlowchart {
subgraph %{aliasName}["`**%{aliasName}**`"] subgraph %{aliasName}["`**%{aliasName}**`"]
direction TB direction TB
style %{aliasName} fill: %{color} style %{aliasName} fill:%{color},stroke:darkblue,stroke-width:8px
""" """
.replace("%{aliasName}", entity.aliasName()) .replace("%{aliasName}", entity.aliasName())
.replace("%{color}", color )); .replace("%{color}", color ));
rbacDef.getEntityAliases().values().stream()
.filter(e -> e.aliasName().startsWith(entity.aliasName() + "."))
.forEach(this::renderEntitySubgraph);
wrapOutputInSubgraph(entity.aliasName() + ":roles", color, wrapOutputInSubgraph(entity.aliasName() + ":roles", color,
rbacDef.getRoleDefs().stream() rbacDef.getRoleDefs().stream()
.filter(r -> r.getEntityAlias() == entity) .filter(r -> r.getEntityAlias() == entity)
@ -118,21 +122,10 @@ public class RbacViewMermaidFlowchart {
return flowchart.toString(); return flowchart.toString();
} }
public static void main(String[] args) throws IOException { void generateToMarkdownFile() throws IOException {
final Path path = Paths.get("doc", rbacDef.getEntityAlias().simpleName() + ".md");
// 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( Files.writeString(
Paths.get("doc", "hsOfficeDebitor.md"), path,
""" """
### rbac %{entityAlias} %{timestamp} ### rbac %{entityAlias} %{timestamp}
@ -140,10 +133,16 @@ public class RbacViewMermaidFlowchart {
%{flowchart} %{flowchart}
``` ```
""" """
.replace("%{entityAlias}", "contact") .replace("%{entityAlias}", rbacDef.getEntityAlias().aliasName())
.replace("%{timestamp}", LocalDateTime.now().toString()) .replace("%{timestamp}", LocalDateTime.now().toString())
.replace("%{flowchart}", new RbacViewMermaidFlowchart(HsOfficeDebitorEntity.rbac()).toString()), .replace("%{flowchart}", flowchart.toString()),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); 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(HsOfficeDebitorEntity.rbac()).generateToMarkdownFile();
} }
} }

View File

@ -1,7 +1,6 @@
package net.hostsharing.hsadminng.rbac.rbacdef; package net.hostsharing.hsadminng.rbac.rbacdef;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacPermissionDefinition; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacPermissionDefinition;
import org.apache.commons.lang3.StringUtils;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.util.HashSet; import java.util.HashSet;
@ -31,7 +30,7 @@ class RolesGrantsAndPermissionsGenerator {
this.liquibaseTagPrefix = liquibaseTagPrefix; this.liquibaseTagPrefix = liquibaseTagPrefix;
entityClass = rbacDef.getEntityAlias().entityClass(); entityClass = rbacDef.getEntityAlias().entityClass();
simpleEntityName = withoutEntitySuffix(entityClass.getSimpleName()); simpleEntityName = entityClass.getSimpleName();
simpleEntityVarName = uncapitalize(simpleEntityName); simpleEntityVarName = uncapitalize(simpleEntityName);
rawTableName = withoutRvSuffix(entityClass.getAnnotation(Table.class).name()); rawTableName = withoutRvSuffix(entityClass.getAnnotation(Table.class).name());
} }
@ -209,10 +208,6 @@ class RolesGrantsAndPermissionsGenerator {
return tableName.substring(0, tableName.length()-"_rv".length()); 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) { private String toPlPgSqlReference(final RbacView.RbacUserReference userRef) {
return switch (userRef.role) { return switch (userRef.role) {
case CREATOR -> "currentUserUuid()"; case CREATOR -> "currentUserUuid()";
@ -221,20 +216,18 @@ class RolesGrantsAndPermissionsGenerator {
} }
private String toPlPgSqlReference(final PostgresTriggerReference triggerRef, final RbacView.RbacRoleDefinition roleDef) { private String toPlPgSqlReference(final PostgresTriggerReference triggerRef, final RbacView.RbacRoleDefinition roleDef) {
return toSimpleVarName(roleDef.getEntityAlias()) + StringUtils.capitalize(roleDef.getRole().roleName()) + return toVar(roleDef) +
(roleDef.getEntityAlias().isGlobal() ? "()" (roleDef.getEntityAlias().isGlobal() ? "()"
: rbacDef.isMainEntityAlias(roleDef.getEntityAlias()) ? ("(" + triggerRef.name() + ")") : rbacDef.isMainEntityAlias(roleDef.getEntityAlias()) ? ("(" + triggerRef.name() + ")")
: "(" + toTriggerReference(triggerRef, roleDef.getEntityAlias()) + ")"); : "(" + toTriggerReference(triggerRef, roleDef.getEntityAlias()) + ")");
} }
private static String toTriggerReference(final PostgresTriggerReference triggerRef, final RbacView.EntityAlias entityAlias) { private static String toVar(final RbacView.RbacRoleDefinition roleDef) {
return triggerRef.name().toLowerCase() + StringUtils.capitalize(entityAlias.aliasName()); return uncapitalize(roleDef.getEntityAlias().simpleName()) + capitalize(roleDef.getRole().roleName());
} }
private String toSimpleVarName(final RbacView.EntityAlias entityAlias) { private static String toTriggerReference(final PostgresTriggerReference triggerRef, final RbacView.EntityAlias entityAlias) {
return entityAlias.isGlobal() return triggerRef.name().toLowerCase() + capitalize(entityAlias.aliasName());
? entityAlias.aliasName()
: uncapitalize(withoutEntitySuffix(entityAlias.entityClass().getSimpleName()));
} }
private String indent(final int tabs) { private String indent(final int tabs) {