diff --git a/src/main/java/net/hostsharing/hsadminng/errors/ReferenceNotFoundException.java b/src/main/java/net/hostsharing/hsadminng/errors/ReferenceNotFoundException.java index e20d1357..deeae9f8 100644 --- a/src/main/java/net/hostsharing/hsadminng/errors/ReferenceNotFoundException.java +++ b/src/main/java/net/hostsharing/hsadminng/errors/ReferenceNotFoundException.java @@ -1,6 +1,6 @@ package net.hostsharing.hsadminng.errors; -import net.hostsharing.hsadminng.persistence.HasUuid; +import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import java.util.UUID; @@ -8,7 +8,7 @@ public class ReferenceNotFoundException extends RuntimeException { private final Class entityClass; private final UUID uuid; - public ReferenceNotFoundException(final Class entityClass, final UUID uuid, final Throwable exc) { + public ReferenceNotFoundException(final Class entityClass, final UUID uuid, final Throwable exc) { super(exc); this.entityClass = entityClass; this.uuid = uuid; diff --git a/src/main/java/net/hostsharing/hsadminng/persistence/HasUuid.java b/src/main/java/net/hostsharing/hsadminng/persistence/HasUuid.java index 1f3ead14..03e6abf3 100644 --- a/src/main/java/net/hostsharing/hsadminng/persistence/HasUuid.java +++ b/src/main/java/net/hostsharing/hsadminng/persistence/HasUuid.java @@ -1,7 +1,7 @@ package net.hostsharing.hsadminng.persistence; -import java.util.UUID; +import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; -public interface HasUuid { - UUID getUuid(); +// TODO: remove this interface, I just wanted to avoid to many changes in that PR +public interface HasUuid extends RbacObject { } diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java index df377ab6..b89ac18b 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -3,7 +3,7 @@ package net.hostsharing.hsadminng.rbac.rbacdef; import lombok.EqualsAndHashCode; import lombok.Getter; import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity; -import net.hostsharing.hsadminng.persistence.HasUuid; +import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import jakarta.validation.constraints.NotNull; import java.lang.reflect.InvocationTargetException; @@ -37,11 +37,11 @@ public class RbacView { private SQL identityViewSqlQuery; private EntityAlias entityAliasProxy; - public static RbacView rbacViewFor(final String alias, final Class entityClass) { + public static RbacView rbacViewFor(final String alias, final Class entityClass) { return new RbacView(alias, entityClass); } - RbacView(final String alias, final Class entityClass) { + RbacView(final String alias, final Class entityClass) { entityAlias = new EntityAlias(alias, entityClass); entityAliases.put(alias, entityAlias); new RbacUserReference(CREATOR); @@ -72,14 +72,14 @@ public class RbacView { return permDef; } - public RbacView declarePlaceholderEntityAliases(final String... aliasNames) { + public RbacView declarePlaceholderEntityAliases(final String... aliasNames) { for ( String alias: aliasNames ) { entityAliases.put(alias, new EntityAlias(alias)); } return this; } - public RbacView importProxyEntity( + public RbacView importProxyEntity( final String aliasName, final Class entityClass, final SQL fetchSql, final Column dependsOnColum) { if ( entityAliasProxy != null ) { throw new IllegalStateException("there is already an entityAliasProxy: " + entityAliasProxy); @@ -105,7 +105,7 @@ public class RbacView { return entityAlias; } - private static RbacView rbacDefinition(final Class entityClass) + private static RbacView rbacDefinition(final Class entityClass) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { return (RbacView) entityClass.getMethod("rbac").invoke(null); } @@ -398,13 +398,13 @@ public class RbacView { return findRbacRole(findEntityAlias(entityAliasName), role); } - record EntityAlias(String aliasName, Class entityClass, SQL fetchSql, Column dependsOnColum) { + record EntityAlias(String aliasName, Class entityClass, SQL fetchSql, Column dependsOnColum) { public EntityAlias(final String aliasName) { this(aliasName, null, null, null); } - public EntityAlias(final String aliasName, final Class entityClass) { + public EntityAlias(final String aliasName, final Class entityClass) { this(aliasName, entityClass, null, null); } diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java index 8a6e6ff7..5c63334f 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchart.java @@ -19,15 +19,10 @@ public class RbacViewMermaidFlowchart { public RbacViewMermaidFlowchart(final RbacView rbacDef) { this.rbacDef = rbacDef; flowchart.append(""" - ### rbac %{entityAlias} %{timestamp} - - ```mermaid %%{init:{'flowchart':{'htmlLabels':false}}}%% flowchart TB - """ - .replace("%{entityAlias}", rbacDef.getEntityAlias().aliasName()) - .replace("%{timestamp}", LocalDateTime.now().toString())); + """); renderEntitySubgraphs(); renderGrants(); flowchart.append("```"); @@ -51,13 +46,13 @@ public class RbacViewMermaidFlowchart { .replace("%{aliasName}", entity.aliasName()) .replace("%{color}", color )); - wrapOutputInSubgraph(entity.aliasName() + ".roles", color, + wrapOutputInSubgraph(entity.aliasName() + ":roles", color, rbacDef.getRoleDefs().stream() .filter(r -> r.getEntityAlias() == entity) .map(r -> " " + roleDef(r)) .collect(joining("\n"))); - wrapOutputInSubgraph(entity.aliasName() + "permissions", color, + wrapOutputInSubgraph(entity.aliasName() + ":permissions", color, rbacDef.getPermDefs().stream() .filter(p -> p.getEntityAlias() == entity) .map(p -> " " + permDef(p) ) @@ -138,7 +133,17 @@ public class RbacViewMermaidFlowchart { Files.writeString( Paths.get("doc", "hsOfficeDebitor.md"), - new RbacViewMermaidFlowchart(HsOfficeDebitorEntity.rbac()).toString(), - StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + """ + ### rbac %{entityAlias} %{timestamp} + + ```mermaid + %{flowchart} + ``` + """ + .replace("%{entityAlias}", "contact") + .replace("%{timestamp}", LocalDateTime.now().toString()) + .replace("%{flowchart}", new RbacViewMermaidFlowchart(HsOfficeDebitorEntity.rbac()).toString()), + + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); } } diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacobject/RbacObject.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacobject/RbacObject.java new file mode 100644 index 00000000..4d7646d1 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacobject/RbacObject.java @@ -0,0 +1,8 @@ +package net.hostsharing.hsadminng.rbac.rbacobject; + + +import java.util.UUID; + +public interface RbacObject { + UUID getUuid(); +} diff --git a/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java b/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java index 1f2bb0e1..3d5e6f19 100644 --- a/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java @@ -4,17 +4,24 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; +import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import jakarta.persistence.*; import java.util.UUID; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; + @Entity @Table(name = "test_customer_rv") @Getter @Setter @NoArgsConstructor @AllArgsConstructor -public class TestCustomerEntity { +public class TestCustomerEntity implements RbacObject { @Id @GeneratedValue @@ -25,4 +32,23 @@ public class TestCustomerEntity { @Column(name = "adminusername") private String adminUserName; + + + + public static RbacView rbac() { + // @formatter:off + return rbacViewFor("contact", TestCustomerEntity.class) + .withIdentityView(RbacView.SQL.query("target.prefix")) + .withUpdatableColumns("reference", "prefix", "adminUserName") + .createRole(OWNER) + .withPermission(ALL) + .withCurrentUserAsOwner() + .withIncomingSuperRole(GLOBAL, ADMIN) + .createSubRole(ADMIN) + .withPermission(RbacView.Permission.custom("add-package")) + .createSubRole(TENANT) + .withPermission(VIEW) + .pop(); + // @formatter:on + } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java b/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java index 325317b2..929aa919 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java @@ -21,7 +21,7 @@ import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType; import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity; import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipType; import net.hostsharing.hsadminng.hs.office.sepamandate.HsOfficeSepaMandateEntity; -import net.hostsharing.hsadminng.persistence.HasUuid; +import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.test.JpaAttempt; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; @@ -520,7 +520,7 @@ public class ImportOfficeData extends ContextBasedTest { } - private void persist(final Integer id, final HasUuid entity) { + private void persist(final Integer id, final RbacObject entity) { try { //System.out.println("persisting #" + entity.hashCode() + ": " + entity); em.persist(entity); @@ -591,7 +591,7 @@ public class ImportOfficeData extends ContextBasedTest { }).assertSuccessful(); } - private void updateLegacyIds( + private void updateLegacyIds( Map entities, final String legacyIdTable, final String legacyIdColumn) { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/test/ContextBasedTestWithCleanup.java b/src/test/java/net/hostsharing/hsadminng/hs/office/test/ContextBasedTestWithCleanup.java index 9b6c14ed..968e5416 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/test/ContextBasedTestWithCleanup.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/test/ContextBasedTestWithCleanup.java @@ -4,6 +4,7 @@ import net.hostsharing.hsadminng.context.ContextBasedTest; import net.hostsharing.hsadminng.persistence.HasUuid; import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantEntity; import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantRepository; +import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.rbac.rbacrole.RbacRoleEntity; import net.hostsharing.hsadminng.rbac.rbacrole.RbacRoleRepository; import net.hostsharing.test.JpaAttempt; @@ -43,7 +44,7 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest { @Autowired JpaAttempt jpaAttempt; - private TreeMap> entitiesToCleanup = new TreeMap<>(); + private TreeMap> entitiesToCleanup = new TreeMap<>(); private static Long latestIntialTestDataSerialId; private static boolean countersInitialized = false; @@ -61,7 +62,7 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest { return uuidToCleanup; } - public E toCleanup(final E entity) { + public E toCleanup(final E entity) { out.println("toCleanup(" + entity.getClass() + ", " + entity.getUuid()); if ( entity.getUuid() == null ) { throw new IllegalArgumentException("only persisted entities with valid uuid allowed"); diff --git a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityTest.java b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityTest.java new file mode 100644 index 00000000..c7f9800a --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityTest.java @@ -0,0 +1,51 @@ +package net.hostsharing.hsadminng.test.cust; + +import net.hostsharing.hsadminng.rbac.rbacdef.RbacViewMermaidFlowchart; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class TestCustomerEntityTest { + + @Test + void definesRbac() { + final var rbacFlowchart = new RbacViewMermaidFlowchart(TestCustomerEntity.rbac()).toString(); + assertThat(rbacFlowchart).isEqualToIgnoringWhitespace(""" + %%{init:{'flowchart':{'htmlLabels':false}}}%% + flowchart TB + + subgraph contact["`**contact**`"] + direction TB + style contact fill:#dd4901,stroke:darkblue,stroke-width:8px + + subgraph contact:roles[ ] + style contact:roles fill: #dd4901 + + role:contact:owner[[contact:owner]] + role:contact:admin[[contact:admin]] + role:contact:tenant[[contact:tenant]] + end + + subgraph contact:permissions[ ] + style contact:permissions fill: #dd4901 + + perm:contact:*{{contact:*}} + perm:contact:add-package{{contact:add-package}} + perm:contact:view{{contact:view}} + end + end + + role:contact:owner ==> perm:contact:* + role:contact:owner ==> perm:contact:* + user:creator ==> role:contact:owner + role:global:admin ==> role:contact:owner + role:global:admin ==> role:contact:owner + role:contact:owner ==> role:contact:admin + role:contact:admin ==> perm:contact:add-package + role:contact:admin ==> perm:contact:add-package + role:contact:admin ==> role:contact:tenant + role:contact:tenant ==> perm:contact:view + role:contact:tenant ==> perm:contact:view + """); + } +}