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
18 changed files with 203 additions and 97 deletions
Showing only changes of commit d40cf019cc - Show all commits

View File

@ -197,7 +197,7 @@ public class RbacView {
});
importedRbacView.getGrantDefs().forEach(grantDef -> {
if (grantDef.grantType() == RbacGrantDefinition.GrantType.ROLE_TO_ROLE) {
findOrCreateGrantDef(
final var importedGrantDef = findOrCreateGrantDef(
findRbacRole(
mapper.map(grantDef.getSubRoleDef().entityAlias.aliasName),
grantDef.getSubRoleDef().getRole()),
@ -205,6 +205,9 @@ public class RbacView {
mapper.map(grantDef.getSuperRoleDef().entityAlias.aliasName),
grantDef.getSuperRoleDef().getRole())
);
if (!grantDef.isAssumed()) {
importedGrantDef.unassumed();
}
}
});
return this;

View File

@ -109,9 +109,8 @@ public class RbacViewMermaidFlowchart {
}
private String grantDef(final RbacView.RbacGrantDefinition grant) {
final var arrow = grant.isToCreate()
? grant.isAssumed() ? " ==> " : " == /// ==> "
: grant.isAssumed() ? " -.-> " : " -.- /// -.-> ";
final var arrow = (grant.isToCreate() ? " ==>" : " -.->")
+ (grant.isAssumed() ? " " : "|XX| ");
return switch (grant.grantType()) {
case ROLE_TO_USER ->
// TODO: other user types not implemented yet

View File

@ -230,7 +230,8 @@ class RolesGrantsAndPermissionsGenerator {
private String generateGrant(RbacView.RbacGrantDefinition grantDef) {
return switch (grantDef.grantType()) {
case ROLE_TO_USER -> throw new IllegalArgumentException("unexpected grant");
case ROLE_TO_ROLE -> "call grantRoleToRole(${subRoleRef}, ${superRoleRef});"
case ROLE_TO_ROLE -> "call grantRoleToRole(${subRoleRef}, ${superRoleRef}${assumed});"
.replace("${assumed}", grantDef.isAssumed() ? "" : ", unassumed()")
.replace("${subRoleRef}", roleRef(NEW, grantDef.getSubRoleDef()))
.replace("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef()));
case PERM_TO_ROLE ->
@ -345,12 +346,11 @@ class RolesGrantsAndPermissionsGenerator {
private void generateIncomingSuperRolesForRole(final StringWriter plPgSql, final RbacView.Role role) {
final var incomingGrants = findIncomingSuperRolesForRole(rbacDef.getRootEntityAlias(), role);
if (!incomingGrants.isEmpty()) {
final var arraElements = incomingGrants.stream()
.map(RbacView.RbacGrantDefinition::getSuperRoleDef)
.map(r -> toPlPgSqlReference(NEW, r))
final var arrayElements = incomingGrants.stream()
.map(g -> toPlPgSqlReference(NEW, g.getSuperRoleDef(), g.isAssumed()))
.toList();
plPgSql.indented(() ->
plPgSql.writeLn("incomingSuperRoles => array[" + joinArrayElements(arraElements, 1) + "],\n"));
plPgSql.writeLn("incomingSuperRoles => array[" + joinArrayElements(arrayElements, 1) + "],\n"));
rbacGrants.removeAll(incomingGrants);
}
}
@ -359,8 +359,7 @@ class RolesGrantsAndPermissionsGenerator {
final var outgoingGrants = findOutgoingSuperRolesForRole(rbacDef.getRootEntityAlias(), role);
if (!outgoingGrants.isEmpty()) {
final var arrayElements = outgoingGrants.stream()
.map(RbacView.RbacGrantDefinition::getSubRoleDef)
.map(r -> toPlPgSqlReference(NEW, r))
.map(g -> toPlPgSqlReference(NEW, g.getSubRoleDef(), g.isAssumed()))
.toList();
plPgSql.indented(() ->
plPgSql.writeLn("outgoingSubRoles => array[" + joinArrayElements(arrayElements, 1) + "],\n"));
@ -485,14 +484,18 @@ class RolesGrantsAndPermissionsGenerator {
};
}
private String toPlPgSqlReference(final PostgresTriggerReference triggerRef, final RbacView.RbacRoleDefinition roleDef) {
return toVar(roleDef) +
(roleDef.getEntityAlias().isGlobal() ? "()"
private String toPlPgSqlReference(
final PostgresTriggerReference triggerRef,
final RbacView.RbacRoleDefinition roleDef,
final boolean assumed) {
final var assumedArg = assumed ? "" : ", unassumed()";
return toRoleRef(roleDef) +
(roleDef.getEntityAlias().isGlobal() ? ( assumed ? "()" : "(unassumed())")
: rbacDef.isRootEntityAlias(roleDef.getEntityAlias()) ? ("(" + triggerRef.name() + ")")
: "(" + toTriggerReference(triggerRef, roleDef.getEntityAlias()) + ")");
: "(" + toTriggerReference(triggerRef, roleDef.getEntityAlias()) + assumedArg + ")");
}
private static String toVar(final RbacView.RbacRoleDefinition roleDef) {
private static String toRoleRef(final RbacView.RbacRoleDefinition roleDef) {
return uncapitalize(roleDef.getEntityAlias().simpleName()) + capitalize(roleDef.getRole().roleName());
}

View File

@ -43,8 +43,8 @@ public class TestCustomerEntity implements HasUuid {
.withUpdatableColumns("reference", "prefix", "adminUserName")
.createRole(OWNER, (with) -> {
with.owningUser(CREATOR);
with.incomingSuperRole(GLOBAL, ADMIN);
with.owningUser(CREATOR).unassumed();
with.incomingSuperRole(GLOBAL, ADMIN).unassumed();
with.permission(DELETE);
})
.createSubRole(ADMIN, (with) -> {

View File

@ -15,7 +15,6 @@ import java.util.UUID;
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.RbacUserReference.UserRole.CREATOR;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
@ -58,7 +57,7 @@ public class TestPackageEntity implements HasUuid {
.toRole("customer", ADMIN).grantPermission("package", INSERT)
.createRole(OWNER, (with) -> {
with.incomingSuperRole("customer", ADMIN).unassumed();
with.incomingSuperRole("customer", ADMIN);
with.permission(DELETE);
with.permission(UPDATE);
})

View File

@ -203,15 +203,33 @@ create type RbacRoleDescriptor as
(
objectTable varchar(63), -- for human readability and easier debugging
objectUuid uuid,
roleType RbacRoleType
roleType RbacRoleType,
assumed boolean
);
create or replace function roleDescriptor(objectTable varchar(63), objectUuid uuid, roleType RbacRoleType)
create or replace function assumed()
returns boolean
stable -- leakproof
language sql as $$
select true;
$$;
create or replace function unassumed()
returns boolean
stable -- leakproof
language sql as $$
select false;
$$;
create or replace function roleDescriptor(
objectTable varchar(63), objectUuid uuid, roleType RbacRoleType,
assumed boolean = true) -- just for DSL readability, belongs actually to the grant
returns RbacRoleDescriptor
returns null on null input
stable -- leakproof
language sql as $$
select objectTable, objectUuid, roleType::RbacRoleType;
select objectTable, objectUuid, roleType::RbacRoleType, assumed;
$$;
create or replace function createRole(roleDescriptor RbacRoleDescriptor)

View File

@ -13,24 +13,6 @@ begin
return createPermissions(forObjectUuid, permitOps);
end; $$;
create or replace function toRoleUuids(roleDescriptors RbacRoleDescriptor[])
returns uuid[]
language plpgsql
strict as $$
declare
superRoleDescriptor RbacRoleDescriptor;
superRoleUuids uuid[] := array []::uuid[];
begin
foreach superRoleDescriptor in array roleDescriptors
loop
if superRoleDescriptor is not null then
superRoleUuids := superRoleUuids || getRoleId(superRoleDescriptor, 'fail');
end if;
end loop;
return superRoleUuids;
end; $$;
-- =================================================================
-- CREATE ROLE
@ -50,25 +32,29 @@ create or replace function createRoleWithGrants(
language plpgsql as $$
declare
roleUuid uuid;
superRoleUuid uuid;
subRoleDesc RbacRoleDescriptor;
superRoleDesc RbacRoleDescriptor;
subRoleUuid uuid;
superRoleUuid uuid;
userUuid uuid;
grantedByRoleUuid uuid;
begin
roleUuid := createRole(roleDescriptor);
if cardinality(permissions) >0 then
if cardinality(permissions) > 0 then
call grantPermissionsToRole(roleUuid, toPermissionUuids(roleDescriptor.objectuuid, permissions));
end if;
foreach superRoleUuid in array toRoleUuids(incomingSuperRoles)
foreach superRoleDesc in array incomingSuperRoles
loop
call grantRoleToRole(roleUuid, superRoleUuid);
superRoleUuid = getRoleId(superRoleDesc, 'fail');
call grantRoleToRole(roleUuid, superRoleUuid, superRoleDesc.assumed);
end loop;
foreach subRoleUuid in array toRoleUuids(outgoingSubRoles)
foreach subRoleDesc in array outgoingSubRoles
loop
call grantRoleToRole(subRoleUuid, roleUuid);
subRoleUuid = getRoleId(subRoleDesc, 'fail');
call grantRoleToRole(subRoleUuid, roleUuid, subRoleDesc.assumed);
end loop;
if cardinality(userUuids) > 0 then

View File

@ -35,50 +35,50 @@ end; $$;
--changeset rbac-generators-ROLE-DESCRIPTORS:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
create or replace procedure generateRbacRoleDescriptors(prefix text, targetTable text)
create procedure generateRbacRoleDescriptors(prefix text, targetTable text)
language plpgsql as $$
declare
sql text;
begin
sql = format($sql$
create or replace function %1$sOwner(entity %2$s)
create or replace function %1$sOwner(entity %2$s, assumed boolean = true)
returns RbacRoleDescriptor
language plpgsql
strict as $f$
begin
return roleDescriptor('%2$s', entity.uuid, 'owner');
return roleDescriptor('%2$s', entity.uuid, 'owner', assumed);
end; $f$;
create or replace function %1$sAdmin(entity %2$s)
create or replace function %1$sAdmin(entity %2$s, assumed boolean = true)
returns RbacRoleDescriptor
language plpgsql
strict as $f$
begin
return roleDescriptor('%2$s', entity.uuid, 'admin');
return roleDescriptor('%2$s', entity.uuid, 'admin', assumed);
end; $f$;
create or replace function %1$sAgent(entity %2$s)
create or replace function %1$sAgent(entity %2$s, assumed boolean = true)
returns RbacRoleDescriptor
language plpgsql
strict as $f$
begin
return roleDescriptor('%2$s', entity.uuid, 'agent');
return roleDescriptor('%2$s', entity.uuid, 'agent', assumed);
end; $f$;
create or replace function %1$sTenant(entity %2$s)
create or replace function %1$sTenant(entity %2$s, assumed boolean = true)
returns RbacRoleDescriptor
language plpgsql
strict as $f$
begin
return roleDescriptor('%2$s', entity.uuid, 'tenant');
return roleDescriptor('%2$s', entity.uuid, 'tenant', assumed);
end; $f$;
create or replace function %1$sGuest(entity %2$s)
create or replace function %1$sGuest(entity %2$s, assumed boolean = true)
returns RbacRoleDescriptor
language plpgsql
strict as $f$
begin
return roleDescriptor('%2$s', entity.uuid, 'guest');
return roleDescriptor('%2$s', entity.uuid, 'guest', assumed);
end; $f$;
$sql$, prefix, targetTable);

View File

@ -109,12 +109,12 @@ commit;
/*
A global administrator role.
*/
create or replace function globalAdmin()
create or replace function globalAdmin(assumed boolean = true)
returns RbacRoleDescriptor
returns null on null input
stable -- leakproof
language sql as $$
select 'global', (select uuid from RbacObject where objectTable = 'global'), 'admin'::RbacRoleType;
select 'global', (select uuid from RbacObject where objectTable = 'global'), 'admin'::RbacRoleType, assumed;
$$;
begin transaction;

View File

@ -0,0 +1,41 @@
### rbac customer 2024-03-08T13:03:39.397294085
```mermaid
%%{init:{'flowchart':{'htmlLabels':false}}}%%
flowchart TB
subgraph customer["`**customer**`"]
direction TB
style customer fill:#dd4901,stroke:#274d6e,stroke-width:8px
subgraph customer:roles[ ]
style customer:roles fill:#dd4901,stroke:white
role:customer:owner[[customer:owner]]
role:customer:admin[[customer:admin]]
role:customer:tenant[[customer:tenant]]
end
subgraph customer:permissions[ ]
style customer:permissions fill:#dd4901,stroke:white
perm:customer:DELETE{{customer:DELETE}}
perm:customer:UPDATE{{customer:UPDATE}}
perm:customer:SELECT{{customer:SELECT}}
end
end
%% granting roles to users
user:creator ==>|XX| role:customer:owner
%% granting roles to roles
role:global:admin ==>|XX| role:customer:owner
role:customer:owner ==> role:customer:admin
role:customer:admin ==> role:customer:tenant
%% granting permissions to roles
role:customer:owner ==> perm:customer:DELETE
role:customer:admin ==> perm:customer:UPDATE
role:customer:tenant ==> perm:customer:SELECT
```

View File

@ -1,5 +1,5 @@
--liquibase formatted sql
-- This code generated was by RbacViewPostgresGenerator at 2024-03-08T08:48:56.112505380.
-- This code generated was by RbacViewPostgresGenerator at 2024-03-08T13:03:39.428165899.
-- ============================================================================
--changeset test-customer-rbac-OBJECT:1 endDelimiter:--//
@ -37,7 +37,7 @@ begin
testCustomerOwner(NEW),
permissions => array['DELETE'],
userUuids => array[currentUserUuid()],
incomingSuperRoles => array[globalAdmin()]
incomingSuperRoles => array[globalAdmin(unassumed())]
);
perform createRoleWithGrants(

View File

@ -0,0 +1,57 @@
### rbac package 2024-03-08T13:03:39.472333368
```mermaid
%%{init:{'flowchart':{'htmlLabels':false}}}%%
flowchart TB
subgraph package["`**package**`"]
direction TB
style package fill:#dd4901,stroke:#274d6e,stroke-width:8px
subgraph package:roles[ ]
style package:roles fill:#dd4901,stroke:white
role:package:owner[[package:owner]]
role:package:admin[[package:admin]]
role:package:tenant[[package:tenant]]
end
subgraph package:permissions[ ]
style package:permissions fill:#dd4901,stroke:white
perm:package:INSERT{{package:INSERT}}
perm:package:DELETE{{package:DELETE}}
perm:package:UPDATE{{package:UPDATE}}
perm:package:SELECT{{package:SELECT}}
end
end
subgraph customer["`**customer**`"]
direction TB
style customer fill:#99bcdb,stroke:#274d6e,stroke-width:8px
subgraph customer:roles[ ]
style customer:roles fill:#99bcdb,stroke:white
role:customer:owner[[customer:owner]]
role:customer:admin[[customer:admin]]
role:customer:tenant[[customer:tenant]]
end
end
%% granting roles to roles
role:global:admin -.->|XX| role:customer:owner
role:customer:owner -.-> role:customer:admin
role:customer:admin -.-> role:customer:tenant
role:customer:admin ==> role:package:owner
role:package:owner ==> role:package:admin
role:package:admin ==> role:package:tenant
role:package:tenant ==> role:customer:tenant
%% granting permissions to roles
role:customer:admin ==> perm:package:INSERT
role:package:owner ==> perm:package:DELETE
role:package:owner ==> perm:package:UPDATE
role:package:tenant ==> perm:package:SELECT
```

View File

@ -1,5 +1,5 @@
--liquibase formatted sql
-- This code generated was by RbacViewPostgresGenerator at 2024-03-08T08:48:56.148164198.
-- This code generated was by RbacViewPostgresGenerator at 2024-03-08T13:03:39.473061981.
-- ============================================================================
--changeset test-package-rbac-OBJECT:1 endDelimiter:--//

View File

@ -6,14 +6,31 @@ import org.junit.jupiter.api.TestInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
@Import(RbacGrantsDiagramService.class)
public abstract class ContextBasedTest {
@Autowired
protected Context context;
@PersistenceContext
protected EntityManager em; // just to be used in subclasses
/**
* To generate a flowchart diagram from the database use something like this in a defined context:
<pre>
RbacGrantsDiagramService.writeToFile(
"title",
diagramService.allGrantsToCurrentUser(of(RbacGrantsDiagramService.Include.USERS, RbacGrantsDiagramService.Include.TEST_ENTITIES, RbacGrantsDiagramService.Include.NOT_ASSUMED, RbacGrantsDiagramService.Include.DETAILS, RbacGrantsDiagramService.Include.PERMISSIONS)),
"filename.md
);
</pre>
*/
@Autowired
protected RbacGrantsDiagramService diagramService;
protected RbacGrantsDiagramService diagramService; // just to be used in subclasses
TestInfo test;

View File

@ -36,10 +36,10 @@ class TestCustomerEntityTest {
end
%% granting roles to users
user:creator ==> role:customer:owner
user:creator ==>|XX| role:customer:owner
%% granting roles to roles
role:global:admin ==> role:customer:owner
role:global:admin ==>|XX| role:customer:owner
role:customer:owner ==> role:customer:admin
role:customer:admin ==> role:customer:tenant

View File

@ -2,8 +2,6 @@ package net.hostsharing.hsadminng.test.cust;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.context.ContextBasedTest;
import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService;
import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService.Include;
import net.hostsharing.test.JpaAttempt;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@ -12,14 +10,11 @@ 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.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.PersistenceException;
import jakarta.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.UUID;
import static java.util.EnumSet.of;
import static net.hostsharing.test.JpaAttempt.attempt;
import static org.assertj.core.api.Assertions.assertThat;
@ -30,9 +25,6 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest {
@Autowired
TestCustomerRepository testCustomerRepository;
@PersistenceContext
EntityManager em;
@MockBean
HttpServletRequest request;
@ -118,15 +110,15 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest {
}
@Test
public void globalAdmin_withAssumedglobalAdminRole_canViewAllCustomers() {
public void globalAdmin_withAssumedCustomerOwnerRole_canViewExactlyThatCustomer() {
given:
context("superuser-alex@hostsharing.net", "global#global.admin");
context("superuser-alex@hostsharing.net", "test_customer#yyy.owner");
// when
final var result = testCustomerRepository.findCustomerByOptionalPrefixLike(null);
then:
allTheseCustomersAreReturned(result, "xxx", "yyy", "zzz");
allTheseCustomersAreReturned(result, "yyy");
}
@Test
@ -144,11 +136,6 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest {
@Test
public void customerAdmin_withAssumedOwnedPackageAdminRole_canViewOnlyItsOwnCustomer() {
context("customer-admin@xxx.example.com");
RbacGrantsDiagramService.writeToFile(
"customerAdmin_withAssumedOwnedPackageAdminRole_canViewOnlyItsOwnCustomer",
diagramService.allGrantsToCurrentUser(of(Include.USERS, Include.TEST_ENTITIES, Include.NOT_ASSUMED, Include.DETAILS, Include.PERMISSIONS)),
"doc/customerAdmin_withAssumedOwnedPackageAdminRole_canViewOnlyItsOwnCustomer.md"
);
context("customer-admin@xxx.example.com", "test_package#xxx00.admin");

View File

@ -35,7 +35,7 @@ class TestPackageEntityTest {
perm:package:SELECT{{package:SELECT}}
end
end
subgraph customer["`**customer**`"]
direction TB
style customer fill:#99bcdb,stroke:#274d6e,stroke-width:8px
@ -48,19 +48,16 @@ class TestPackageEntityTest {
role:customer:tenant[[customer:tenant]]
end
end
%% granting roles to users
user:creator ==> role:package:owner
%% granting roles to roles
role:global:admin -.-> role:customer:owner
role:global:admin -.->|XX| role:customer:owner
role:customer:owner -.-> role:customer:admin
role:customer:admin -.-> role:customer:tenant
role:customer:admin == /// ==> role:package:owner
role:customer:admin ==> role:package:owner
role:package:owner ==> role:package:admin
role:package:admin ==> role:package:tenant
role:package:tenant ==> role:customer:tenant
%% granting permissions to roles
role:customer:admin ==> perm:package:INSERT
role:package:owner ==> perm:package:DELETE

View File

@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.test.pac;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.context.ContextBasedTest;
import net.hostsharing.test.JpaAttempt;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@ -19,10 +20,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@Import( { Context.class, JpaAttempt.class })
class TestPackageRepositoryIntegrationTest {
@Autowired
Context context;
class TestPackageRepositoryIntegrationTest extends ContextBasedTest {
@Autowired
TestPackageRepository testPackageRepository;
@ -40,9 +38,10 @@ class TestPackageRepositoryIntegrationTest {
class FindAllByOptionalNameLike {
@Test
public void globalAdmin_withoutAssumedRole_canNotViewAnyPackages_becauseThoseGrantsAreNotassumedd() {
public void globalAdmin_withoutAssumedRole_canNotViewAnyPackages_becauseThoseGrantsAreNotAssumed() {
// given
context.define("superuser-alex@hostsharing.net");
// alex is not just global-admin but lso the creating user, thus we use fran
context.define("superuser-fran@hostsharing.net");
// when
final var result = testPackageRepository.findAllByOptionalNameLike(null);
@ -52,7 +51,7 @@ class TestPackageRepositoryIntegrationTest {
}
@Test
public void globalAdmin_withAssumedglobalAdminRole__canNotViewAnyPackages_becauseThoseGrantsAreNotassumedd() {
public void globalAdmin_withAssumedglobalAdminRole__canNotViewAnyPackages_becauseThoseGrantsAreNotAssumed() {
given:
context.define("superuser-alex@hostsharing.net", "global#global.admin");