RBAC Diagram+PostgreSQL Generator #21
@ -1,13 +1,13 @@
|
|||||||
package net.hostsharing.hsadminng.rbac.rbacgrant;
|
package net.hostsharing.hsadminng.rbac.rbacgrant;
|
||||||
|
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.springframework.data.annotation.Immutable;
|
import org.springframework.data.annotation.Immutable;
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.Column;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import jakarta.persistence.Id;
|
import jakarta.persistence.Id;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ import java.util.UUID;
|
|||||||
@Immutable
|
@Immutable
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class RawRbacGrantEntity {
|
public class RawRbacGrantEntity implements Comparable {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
private UUID uuid;
|
private UUID uuid;
|
||||||
@ -64,4 +64,9 @@ public class RawRbacGrantEntity {
|
|||||||
// TODO: remove .distinct() once partner.person + partner.contact are removed
|
// TODO: remove .distinct() once partner.person + partner.contact are removed
|
||||||
return roles.stream().map(RawRbacGrantEntity::toDisplay).sorted().distinct().toList();
|
return roles.stream().map(RawRbacGrantEntity::toDisplay).sorted().distinct().toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(final Object o) {
|
||||||
|
return uuid.compareTo(((RawRbacGrantEntity)o).uuid);
|
||||||
|
}
|
||||||
}
|
}
|
@ -8,4 +8,8 @@ import java.util.UUID;
|
|||||||
public interface RawRbacGrantRepository extends Repository<RawRbacGrantEntity, UUID> {
|
public interface RawRbacGrantRepository extends Repository<RawRbacGrantEntity, UUID> {
|
||||||
|
|
||||||
List<RawRbacGrantEntity> findAll();
|
List<RawRbacGrantEntity> findAll();
|
||||||
|
|
||||||
|
List<RawRbacGrantEntity> findByAscendingUuid(UUID ascendingUuid);
|
||||||
|
|
||||||
|
List<RawRbacGrantEntity> findByDescendantUuid(UUID refUuid);
|
||||||
}
|
}
|
@ -0,0 +1,204 @@
|
|||||||
|
package net.hostsharing.hsadminng.rbac.rbacgrant;
|
||||||
|
|
||||||
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import jakarta.persistence.PersistenceContext;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import java.io.BufferedWriter;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static java.util.stream.Collectors.groupingBy;
|
||||||
|
import static java.util.stream.Collectors.joining;
|
||||||
|
import static net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService.Include.*;
|
||||||
|
|
||||||
|
// TODO: cleanup - this code was 'hacked' to quickly fix a specific problem, needs refactoring
|
||||||
|
@Service
|
||||||
|
public class RbacGrantsDiagramService {
|
||||||
|
|
||||||
|
public static void writeToFile(final String title, final String graph, final String fileName) {
|
||||||
|
|
||||||
|
try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))) {
|
||||||
|
writer.write("""
|
||||||
|
### all grants to %s
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
%s
|
||||||
|
```
|
||||||
|
""".formatted(title, graph));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Include {
|
||||||
|
DETAILS,
|
||||||
|
USERS,
|
||||||
|
PERMISSIONS,
|
||||||
|
NOT_ASSUMED,
|
||||||
|
TEST_ENTITIES,
|
||||||
|
NON_TEST_ENTITIES
|
||||||
|
}
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private Context context;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RawRbacGrantRepository rawGrantRepo;
|
||||||
|
|
||||||
|
@PersistenceContext
|
||||||
|
private EntityManager em;
|
||||||
|
|
||||||
|
public String allGrantsToCurrentUser(final EnumSet<Include> includes) {
|
||||||
|
final var graph = new HashSet<RawRbacGrantEntity>();
|
||||||
|
for ( UUID subjectUuid: context.currentSubjectsUuids() ) {
|
||||||
|
traverseGrantsTo(graph, subjectUuid, includes);
|
||||||
|
}
|
||||||
|
return toMermaidFlowchart(graph, includes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void traverseGrantsTo(final Set<RawRbacGrantEntity> graph, final UUID refUuid, final EnumSet<Include> includes) {
|
||||||
|
final var grants = rawGrantRepo.findByAscendingUuid(refUuid);
|
||||||
|
grants.forEach(g -> {
|
||||||
|
if (!includes.contains(PERMISSIONS) && g.getDescendantIdName().startsWith("perm ")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!includes.contains(TEST_ENTITIES) && g.getDescendantIdName().contains(" test_")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!includes.contains(NON_TEST_ENTITIES) && !g.getDescendantIdName().contains(" test_")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
graph.add(g);
|
||||||
|
if (includes.contains(NOT_ASSUMED) || g.isAssumed()) {
|
||||||
|
traverseGrantsTo(graph, g.getDescendantUuid(), includes);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public String allGrantsFrom(final UUID targetObject, final String op, final EnumSet<Include> includes) {
|
||||||
|
final var refUuid = (UUID) em.createNativeQuery("SELECT uuid FROM rbacpermission WHERE objectuuid=:targetObject AND op=:op")
|
||||||
|
.setParameter("targetObject", targetObject)
|
||||||
|
.setParameter("op", op)
|
||||||
|
.getSingleResult();
|
||||||
|
final var graph = new HashSet<RawRbacGrantEntity>();
|
||||||
|
traverseGrantsFrom(graph, refUuid, includes);
|
||||||
|
return toMermaidFlowchart(graph, includes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void traverseGrantsFrom(final Set<RawRbacGrantEntity> graph, final UUID refUuid, final EnumSet<Include> option) {
|
||||||
|
final var grants = rawGrantRepo.findByDescendantUuid(refUuid);
|
||||||
|
grants.forEach(g -> {
|
||||||
|
if (!option.contains(USERS) && g.getAscendantIdName().startsWith("user ")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
graph.add(g);
|
||||||
|
if (option.contains(NOT_ASSUMED) || g.isAssumed()) {
|
||||||
|
traverseGrantsFrom(graph, g.getAscendingUuid(), option);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private String toMermaidFlowchart(final HashSet<RawRbacGrantEntity> graph, final EnumSet<Include> includes) {
|
||||||
|
final var entities =
|
||||||
|
includes.contains(DETAILS)
|
||||||
|
? graph.stream()
|
||||||
|
.flatMap(g -> Stream.of(
|
||||||
|
new Node(g.getAscendantIdName(), g.getAscendingUuid()),
|
||||||
|
new Node(g.getDescendantIdName(), g.getDescendantUuid()))
|
||||||
|
)
|
||||||
|
.collect(groupingBy(RbacGrantsDiagramService::renderEntityIdName))
|
||||||
|
.entrySet().stream()
|
||||||
|
.map(entity -> "subgraph " + quoted(entity.getKey()) + renderSubgraph(entity.getKey()) + "\n\n "
|
||||||
|
+ entity.getValue().stream()
|
||||||
|
.map(n -> renderNode(n.idName(), n.uuid()).replace("\n", "\n "))
|
||||||
|
.sorted()
|
||||||
|
.distinct()
|
||||||
|
.collect(joining("\n\n ")))
|
||||||
|
.collect(joining("\n\nend\n\n"))
|
||||||
|
+ "\n\nend\n\n"
|
||||||
|
: "";
|
||||||
|
|
||||||
|
final var grants = graph.stream()
|
||||||
|
.map(g -> quoted(g.getAscendantIdName()) +
|
||||||
|
(g.isAssumed() ? " --> " : " -.-> ") +
|
||||||
|
quoted(g.getDescendantIdName()))
|
||||||
|
.sorted()
|
||||||
|
.collect(joining("\n"));
|
||||||
|
|
||||||
|
final var avoidCroppedNodeLabels = "%%{init:{'flowchart':{'htmlLabels':false}}}%%\n\n";
|
||||||
|
return (includes.contains(DETAILS) ? avoidCroppedNodeLabels : "")
|
||||||
|
+ "flowchart TB\n\n"
|
||||||
|
+ entities
|
||||||
|
+ grants;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String renderSubgraph(final String entityId) {
|
||||||
|
// this does not work according to Mermaid bug https://github.com/mermaid-js/mermaid/issues/3806
|
||||||
|
// if (entityId.contains("#")) {
|
||||||
|
// final var parts = entityId.split("#");
|
||||||
|
// final var table = parts[0];
|
||||||
|
// final var entity = parts[1];
|
||||||
|
// if (table.equals("entity")) {
|
||||||
|
// return "[" + entity "]";
|
||||||
|
// }
|
||||||
|
// return "[" + table + "\n" + entity + "]";
|
||||||
|
// }
|
||||||
|
return "[" + entityId + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String renderEntityIdName(final Node node) {
|
||||||
|
final var refType = refType(node.idName());
|
||||||
|
if (refType.equals("user")) {
|
||||||
|
return "users";
|
||||||
|
}
|
||||||
|
if (refType.equals("perm")) {
|
||||||
|
return node.idName().split(" ", 4)[3];
|
||||||
|
}
|
||||||
|
if (refType.equals("role")) {
|
||||||
|
final var withoutRolePrefix = node.idName().substring("role:".length());
|
||||||
|
return withoutRolePrefix.substring(0, withoutRolePrefix.lastIndexOf('.'));
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("unknown refType '" + refType + "' in '" + node.idName() + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String renderNode(final String idName, final UUID uuid) {
|
||||||
|
return quoted(idName) + renderNodeContent(idName, uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String renderNodeContent(final String idName, final UUID uuid) {
|
||||||
|
final var refType = refType(idName);
|
||||||
|
|
||||||
|
if (refType.equals("user")) {
|
||||||
|
final var displayName = idName.substring(refType.length()+1);
|
||||||
|
return "(" + displayName + "\nref:" + uuid + ")";
|
||||||
|
}
|
||||||
|
if (refType.equals("role")) {
|
||||||
|
final var roleType = idName.substring(idName.lastIndexOf('.') + 1);
|
||||||
|
return "[" + roleType + "\nref:" + uuid + "]";
|
||||||
|
}
|
||||||
|
if (refType.equals("perm")) {
|
||||||
|
final var roleType = idName.split(" ")[1];
|
||||||
|
return "{{" + roleType + "\nref:" + uuid + "}}";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String refType(final String idName) {
|
||||||
|
return idName.split(" ", 2)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private static String quoted(final String idName) {
|
||||||
|
return idName.replace(" ", ":").replaceAll("@.*", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record Node(String idName, UUID uuid) {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,136 @@
|
|||||||
|
package net.hostsharing.hsadminng.rbac.rbacgrant;
|
||||||
|
|
||||||
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
|
import net.hostsharing.hsadminng.hs.office.test.ContextBasedTestWithCleanup;
|
||||||
|
import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService.Include;
|
||||||
|
import net.hostsharing.test.JpaAttempt;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static java.lang.String.join;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@DataJpaTest
|
||||||
|
@Import( { Context.class, JpaAttempt.class, RbacGrantsDiagramService.class})
|
||||||
|
class RbacGrantsDiagramServiceIntegrationTest extends ContextBasedTestWithCleanup {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
RbacGrantsDiagramService grantsMermaidService;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
HttpServletRequest request;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void allGrantsToCurrentUser() {
|
||||||
|
context("superuser-alex@hostsharing.net", "test_domain#xxx00-aaaa.owner");
|
||||||
|
final var graph = grantsMermaidService.allGrantsToCurrentUser(EnumSet.of(Include.TEST_ENTITIES));
|
||||||
|
|
||||||
|
assertThat(graph).isEqualTo("""
|
||||||
|
flowchart TB
|
||||||
|
|
||||||
|
role:test_package#xxx00.tenant[
|
||||||
|
test_package
|
||||||
|
xxx00.t
|
||||||
|
tenant] --> role:test_customer#xxx.tenant[
|
||||||
|
test_customer
|
||||||
|
xxx.t
|
||||||
|
tenant]
|
||||||
|
role:test_domain#xxx00-aaaa.owner[
|
||||||
|
test_domain
|
||||||
|
xxx00-aaaa.o
|
||||||
|
owner] --> role:test_domain#xxx00-aaaa.admin[
|
||||||
|
test_domain
|
||||||
|
xxx00-aaaa.a
|
||||||
|
admin]
|
||||||
|
role:test_domain#xxx00-aaaa.admin[
|
||||||
|
test_domain
|
||||||
|
xxx00-aaaa.a
|
||||||
|
admin] --> role:test_package#xxx00.tenant[
|
||||||
|
test_package
|
||||||
|
xxx00.t
|
||||||
|
tenant]
|
||||||
|
""".trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void allGrantsToCurrentUserIncludingPermissions() {
|
||||||
|
context("superuser-alex@hostsharing.net", "test_domain#xxx00-aaaa.owner");
|
||||||
|
final var graph = grantsMermaidService.allGrantsToCurrentUser(EnumSet.of(Include.TEST_ENTITIES, Include.PERMISSIONS));
|
||||||
|
|
||||||
|
assertThat(graph).isEqualTo("""
|
||||||
|
flowchart TB
|
||||||
|
|
||||||
|
role:test_domain#xxx00-aaaa.owner[
|
||||||
|
test_domain
|
||||||
|
xxx00-aaaa.o
|
||||||
|
owner] --> perm:*:on:test_domain#xxx00-aaaa{{
|
||||||
|
test_domain
|
||||||
|
xxx00-aaaa
|
||||||
|
*}}
|
||||||
|
role:test_customer#xxx.tenant[
|
||||||
|
test_customer
|
||||||
|
xxx.t
|
||||||
|
tenant] --> perm:view:on:test_customer#xxx{{
|
||||||
|
test_customer
|
||||||
|
xxx
|
||||||
|
view}}
|
||||||
|
role:test_domain#xxx00-aaaa.admin[
|
||||||
|
test_domain
|
||||||
|
xxx00-aaaa.a
|
||||||
|
admin] --> perm:edit:on:test_domain#xxx00-aaaa{{
|
||||||
|
test_domain
|
||||||
|
xxx00-aaaa
|
||||||
|
edit}}
|
||||||
|
role:test_package#xxx00.tenant[
|
||||||
|
test_package
|
||||||
|
xxx00.t
|
||||||
|
tenant] --> role:test_customer#xxx.tenant[
|
||||||
|
test_customer
|
||||||
|
xxx.t
|
||||||
|
tenant]
|
||||||
|
role:test_domain#xxx00-aaaa.owner[
|
||||||
|
test_domain
|
||||||
|
xxx00-aaaa.o
|
||||||
|
owner] --> role:test_domain#xxx00-aaaa.admin[
|
||||||
|
test_domain
|
||||||
|
xxx00-aaaa.a
|
||||||
|
admin]
|
||||||
|
role:test_package#xxx00.tenant[
|
||||||
|
test_package
|
||||||
|
xxx00.t
|
||||||
|
tenant] --> perm:view:on:test_package#xxx00{{
|
||||||
|
test_package
|
||||||
|
xxx00
|
||||||
|
view}}
|
||||||
|
role:test_domain#xxx00-aaaa.admin[
|
||||||
|
test_domain
|
||||||
|
xxx00-aaaa.a
|
||||||
|
admin] --> role:test_package#xxx00.tenant[
|
||||||
|
test_package
|
||||||
|
xxx00.t
|
||||||
|
tenant]
|
||||||
|
""".trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// @Disabled
|
||||||
|
void print() throws IOException {
|
||||||
|
//context("superuser-alex@hostsharing.net", "hs_office_person#FirbySusan.admin");
|
||||||
|
context("superuser-alex@hostsharing.net");
|
||||||
|
|
||||||
|
//final var graph = grantsMermaidService.allGrantsToCurrentUser(EnumSet.of(Include.NON_TEST_ENTITIES, Include.PERMISSIONS));
|
||||||
|
|
||||||
|
final var targetObject = (UUID) em.createNativeQuery("SELECT uuid FROM hs_office_coopassetstransaction WHERE reference='ref 1000101-1'").getSingleResult();
|
||||||
|
final var graph = grantsMermaidService.allGrantsFrom(targetObject, "view", EnumSet.of(Include.USERS));
|
||||||
|
|
||||||
|
RbacGrantsDiagramService.writeToFile(join(";", context.getAssumedRoles()), graph, "doc/all-grants.md");
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user