introduce RbacObject and initial test for RbacViewMermaidFlowchart

This commit is contained in:
Michael Hoennig 2024-02-25 07:15:15 +01:00
parent 8d3fb4e951
commit fc1cc5815f
9 changed files with 120 additions and 29 deletions

View File

@ -1,6 +1,6 @@
package net.hostsharing.hsadminng.errors; package net.hostsharing.hsadminng.errors;
import net.hostsharing.hsadminng.persistence.HasUuid; import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
import java.util.UUID; import java.util.UUID;
@ -8,7 +8,7 @@ public class ReferenceNotFoundException extends RuntimeException {
private final Class<?> entityClass; private final Class<?> entityClass;
private final UUID uuid; private final UUID uuid;
public <E extends HasUuid> ReferenceNotFoundException(final Class<E> entityClass, final UUID uuid, final Throwable exc) { public <E extends RbacObject> ReferenceNotFoundException(final Class<E> entityClass, final UUID uuid, final Throwable exc) {
super(exc); super(exc);
this.entityClass = entityClass; this.entityClass = entityClass;
this.uuid = uuid; this.uuid = uuid;

View File

@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.persistence; package net.hostsharing.hsadminng.persistence;
import java.util.UUID; import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
public interface HasUuid { // TODO: remove this interface, I just wanted to avoid to many changes in that PR
UUID getUuid(); public interface HasUuid extends RbacObject {
} }

View File

@ -3,7 +3,7 @@ package net.hostsharing.hsadminng.rbac.rbacdef;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity; 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 jakarta.validation.constraints.NotNull;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
@ -37,11 +37,11 @@ public class RbacView {
private SQL identityViewSqlQuery; private SQL identityViewSqlQuery;
private EntityAlias entityAliasProxy; private EntityAlias entityAliasProxy;
public static <E extends HasUuid> RbacView rbacViewFor(final String alias, final Class<? extends HasUuid> entityClass) { public static <E extends RbacObject> RbacView rbacViewFor(final String alias, final Class<E> entityClass) {
return new RbacView(alias, entityClass); return new RbacView(alias, entityClass);
} }
RbacView(final String alias, final Class<? extends HasUuid> entityClass) { RbacView(final String alias, final Class<? extends RbacObject> entityClass) {
entityAlias = new EntityAlias(alias, entityClass); entityAlias = new EntityAlias(alias, entityClass);
entityAliases.put(alias, entityAlias); entityAliases.put(alias, entityAlias);
new RbacUserReference(CREATOR); new RbacUserReference(CREATOR);
@ -72,14 +72,14 @@ public class RbacView {
return permDef; return permDef;
} }
public <EC extends HasUuid> RbacView declarePlaceholderEntityAliases(final String... aliasNames) { public <EC extends RbacObject> RbacView declarePlaceholderEntityAliases(final String... aliasNames) {
for ( String alias: aliasNames ) { for ( String alias: aliasNames ) {
entityAliases.put(alias, new EntityAlias(alias)); entityAliases.put(alias, new EntityAlias(alias));
} }
return this; return this;
} }
public <EC extends HasUuid> RbacView importProxyEntity( public <EC extends RbacObject> RbacView importProxyEntity(
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 ( entityAliasProxy != null ) { if ( entityAliasProxy != null ) {
throw new IllegalStateException("there is already an entityAliasProxy: " + entityAliasProxy); throw new IllegalStateException("there is already an entityAliasProxy: " + entityAliasProxy);
@ -105,7 +105,7 @@ public class RbacView {
return entityAlias; return entityAlias;
} }
private static RbacView rbacDefinition(final Class<? extends HasUuid> entityClass) private static RbacView rbacDefinition(final Class<? extends RbacObject> entityClass)
throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
return (RbacView) entityClass.getMethod("rbac").invoke(null); return (RbacView) entityClass.getMethod("rbac").invoke(null);
} }
@ -398,13 +398,13 @@ public class RbacView {
return findRbacRole(findEntityAlias(entityAliasName), role); return findRbacRole(findEntityAlias(entityAliasName), role);
} }
record EntityAlias(String aliasName, Class<? extends HasUuid> entityClass, SQL fetchSql, Column dependsOnColum) { record EntityAlias(String aliasName, Class<? extends RbacObject> entityClass, SQL fetchSql, Column dependsOnColum) {
public EntityAlias(final String aliasName) { public EntityAlias(final String aliasName) {
this(aliasName, null, null, null); this(aliasName, null, null, null);
} }
public EntityAlias(final String aliasName, final Class<? extends HasUuid> entityClass) { public EntityAlias(final String aliasName, final Class<? extends RbacObject> entityClass) {
this(aliasName, entityClass, null, null); this(aliasName, entityClass, null, null);
} }

View File

@ -19,15 +19,10 @@ public class RbacViewMermaidFlowchart {
public RbacViewMermaidFlowchart(final RbacView rbacDef) { public RbacViewMermaidFlowchart(final RbacView rbacDef) {
this.rbacDef = rbacDef; this.rbacDef = rbacDef;
flowchart.append(""" flowchart.append("""
### rbac %{entityAlias} %{timestamp}
```mermaid
%%{init:{'flowchart':{'htmlLabels':false}}}%% %%{init:{'flowchart':{'htmlLabels':false}}}%%
flowchart TB flowchart TB
""" """);
.replace("%{entityAlias}", rbacDef.getEntityAlias().aliasName())
.replace("%{timestamp}", LocalDateTime.now().toString()));
renderEntitySubgraphs(); renderEntitySubgraphs();
renderGrants(); renderGrants();
flowchart.append("```"); flowchart.append("```");
@ -51,13 +46,13 @@ public class RbacViewMermaidFlowchart {
.replace("%{aliasName}", entity.aliasName()) .replace("%{aliasName}", entity.aliasName())
.replace("%{color}", color )); .replace("%{color}", color ));
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)
.map(r -> " " + roleDef(r)) .map(r -> " " + roleDef(r))
.collect(joining("\n"))); .collect(joining("\n")));
wrapOutputInSubgraph(entity.aliasName() + "permissions", color, wrapOutputInSubgraph(entity.aliasName() + ":permissions", color,
rbacDef.getPermDefs().stream() rbacDef.getPermDefs().stream()
.filter(p -> p.getEntityAlias() == entity) .filter(p -> p.getEntityAlias() == entity)
.map(p -> " " + permDef(p) ) .map(p -> " " + permDef(p) )
@ -138,7 +133,17 @@ public class RbacViewMermaidFlowchart {
Files.writeString( Files.writeString(
Paths.get("doc", "hsOfficeDebitor.md"), Paths.get("doc", "hsOfficeDebitor.md"),
new RbacViewMermaidFlowchart(HsOfficeDebitorEntity.rbac()).toString(), """
### 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); StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
} }
} }

View File

@ -0,0 +1,8 @@
package net.hostsharing.hsadminng.rbac.rbacobject;
import java.util.UUID;
public interface RbacObject {
UUID getUuid();
}

View File

@ -4,17 +4,24 @@ import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
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;
@Entity @Entity
@Table(name = "test_customer_rv") @Table(name = "test_customer_rv")
@Getter @Getter
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class TestCustomerEntity { public class TestCustomerEntity implements RbacObject {
@Id @Id
@GeneratedValue @GeneratedValue
@ -25,4 +32,23 @@ public class TestCustomerEntity {
@Column(name = "adminusername") @Column(name = "adminusername")
private String 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
}
} }

View File

@ -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.HsOfficeRelationshipEntity;
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipType; import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipType;
import net.hostsharing.hsadminng.hs.office.sepamandate.HsOfficeSepaMandateEntity; 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 net.hostsharing.test.JpaAttempt;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils; 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 { try {
//System.out.println("persisting #" + entity.hashCode() + ": " + entity); //System.out.println("persisting #" + entity.hashCode() + ": " + entity);
em.persist(entity); em.persist(entity);
@ -591,7 +591,7 @@ public class ImportOfficeData extends ContextBasedTest {
}).assertSuccessful(); }).assertSuccessful();
} }
private <E extends HasUuid> void updateLegacyIds( private <E extends RbacObject> void updateLegacyIds(
Map<Integer, E> entities, Map<Integer, E> entities,
final String legacyIdTable, final String legacyIdTable,
final String legacyIdColumn) { final String legacyIdColumn) {

View File

@ -4,6 +4,7 @@ import net.hostsharing.hsadminng.context.ContextBasedTest;
import net.hostsharing.hsadminng.persistence.HasUuid; import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantEntity; import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantEntity;
import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantRepository; 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.RbacRoleEntity;
import net.hostsharing.hsadminng.rbac.rbacrole.RbacRoleRepository; import net.hostsharing.hsadminng.rbac.rbacrole.RbacRoleRepository;
import net.hostsharing.test.JpaAttempt; import net.hostsharing.test.JpaAttempt;
@ -43,7 +44,7 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
@Autowired @Autowired
JpaAttempt jpaAttempt; JpaAttempt jpaAttempt;
private TreeMap<UUID, Class<? extends HasUuid>> entitiesToCleanup = new TreeMap<>(); private TreeMap<UUID, Class<? extends RbacObject>> entitiesToCleanup = new TreeMap<>();
private static Long latestIntialTestDataSerialId; private static Long latestIntialTestDataSerialId;
private static boolean countersInitialized = false; private static boolean countersInitialized = false;
@ -61,7 +62,7 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
return uuidToCleanup; return uuidToCleanup;
} }
public <E extends HasUuid> E toCleanup(final E entity) { public <E extends RbacObject> E toCleanup(final E entity) {
out.println("toCleanup(" + entity.getClass() + ", " + entity.getUuid()); out.println("toCleanup(" + entity.getClass() + ", " + entity.getUuid());
if ( entity.getUuid() == null ) { if ( entity.getUuid() == null ) {
throw new IllegalArgumentException("only persisted entities with valid uuid allowed"); throw new IllegalArgumentException("only persisted entities with valid uuid allowed");

View File

@ -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
""");
}
}