insert (into) table permission

This commit is contained in:
Michael Hoennig 2024-03-05 09:26:39 +01:00
parent fa15378fd2
commit b2cea1e882
15 changed files with 464 additions and 111 deletions

View File

@ -5,6 +5,7 @@ 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.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
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.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@ -17,7 +18,6 @@ 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.Permission.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR; 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.Role.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.projection;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; 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;
@ -67,7 +67,7 @@ public class HsOfficePersonEntity implements HasUuid, Stringifyable {
public static RbacView rbac() { public static RbacView rbac() {
return rbacViewFor("person", HsOfficePersonEntity.class) return rbacViewFor("person", HsOfficePersonEntity.class)
.withIdentityView(projection("concat(tradeName, familyName, givenName)")) .withIdentityView(SQL.projection("concat(tradeName, familyName, givenName)"))
.withUpdatableColumns("personType", "tradeName", "givenName", "familyName") .withUpdatableColumns("personType", "tradeName", "givenName", "familyName")
.createRole(OWNER, (with) -> { .createRole(OWNER, (with) -> {
with.permission(ALL); with.permission(ALL);

View File

@ -85,7 +85,7 @@ public class HsOfficeRelationshipEntity implements HasUuid, Stringifyable {
|| '-with-' || target.relType || '-' || '-with-' || target.relType || '-'
|| (select idName from hs_office_person_iv p where p.uuid = relHolderUuid) || (select idName from hs_office_person_iv p where p.uuid = relHolderUuid)
""")) """))
.withRestrictedViewOrderedBy(SQL.expression( .withRestrictedViewOrderBy(SQL.expression(
"(select idName from hs_office_person_iv p where p.uuid = target.relHolderUuid)")) "(select idName from hs_office_person_iv p where p.uuid = target.relHolderUuid)"))
.withUpdatableColumns("contactUuid") .withUpdatableColumns("contactUuid")
.importEntityAlias("anchorPerson", HsOfficePersonEntity.class, .importEntityAlias("anchorPerson", HsOfficePersonEntity.class,

View File

@ -0,0 +1,76 @@
package net.hostsharing.hsadminng.rbac.rbacdef;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.PERM_TO_ROLE;
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with;
public class InsertTriggerGenerator {
private final RbacView rbacDef;
private final String liquibaseTagPrefix;
public InsertTriggerGenerator(final RbacView rbacDef, final String liqibaseTagPrefix) {
this.rbacDef = rbacDef;
this.liquibaseTagPrefix = liqibaseTagPrefix;
}
void generateTo(final StringWriter plPgSql) {
generateLiquibaseChangesetHeader(plPgSql);
generateGrantInsertRoleToExistingCustomers(plPgSql);
rbacDef.getGrantDefs().stream()
.filter(g -> g.isToCreate() && g.grantType() == PERM_TO_ROLE &&
g.getPermDef().getPermission() == RbacView.Permission.INSERT )
.forEach(g -> {
plPgSql.writeLn("""
/**
Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable}.
*/
create trigger ${rawSubTable}_it
before insert
on ${rawSubTable}
for each row
when ( hasInsertPermission(NEW.${referenceColumn}, 'INSERT', '${rawSubTable}') )
execute procedure insertNotAllowedForCurrentSubjects('${rawSubTable}');
""",
with("rawSubTable", g.getPermDef().entityAlias.getRawTableName()),
with("referenceColumn", g.getSuperRoleDef().getEntityAlias().dependsOnColumName() ));
});
plPgSql.writeLn("--//");
}
private void generateLiquibaseChangesetHeader(final StringWriter plPgSql) {
plPgSql.writeLn("""
-- ============================================================================
--changeset ${liquibaseTagPrefix}-rbac-INSERT:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
""",
with("liquibaseTagPrefix", liquibaseTagPrefix));
}
private void generateGrantInsertRoleToExistingCustomers(final StringWriter plPgSql) {
plPgSql.writeLn("""
/*
Creates an INSERT INTO ${rawSubTableName} permission for the related ${rawSuperTableName} row.
*/
do language plpgsql $$
declare
row ${rawSuperTableName};
permissionUuids uuid[];
roleUuid uuid;
begin
FOR row IN SELECT * FROM ${rawSuperTableName}
LOOP
roleUuid := ${rawSuperRoleDescriptor}(row);
permissionUuids := createPermissions(row.uuid, array ['INSERT:${rawSubTableName}']);
call grantPermissionsToRole(roleUuid, permissionUuids);
END LOOP;
END;
$$;
""",
with("rawSubTableName", "test_package"), // TODO
with("rawSuperTableName", "test_customer"), // TODO
with("rawSuperRoleDescriptor", "testCustomerAdmin") // TODO
);
}
}

View File

@ -0,0 +1,28 @@
package net.hostsharing.hsadminng.rbac.rbacdef;
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with;
public class RbacObjectGenerator {
private final String liquibaseTagPrefix;
private final String rawTableName;
public RbacObjectGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) {
this.liquibaseTagPrefix = liquibaseTagPrefix;
this.rawTableName = rbacDef.getRootEntityAlias().getRawTableName();
}
void generateTo(final StringWriter plPgSql) {
plPgSql.writeLn("""
-- ============================================================================
--changeset ${liquibaseTagPrefix}-rbac-OBJECT:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
call generateRelatedRbacObject('${rawTableName}');
--//
""",
with("liquibaseTagPrefix", liquibaseTagPrefix),
with("rawTableName", rawTableName));
}
}

View File

@ -2,6 +2,7 @@ package net.hostsharing.hsadminng.rbac.rbacdef;
import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.joining;
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.indented;
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with; import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with;
public class RbacRestrictedViewGenerator { public class RbacRestrictedViewGenerator {
@ -33,9 +34,9 @@ public class RbacRestrictedViewGenerator {
""", """,
with("liquibaseTagPrefix", liquibaseTagPrefix), with("liquibaseTagPrefix", liquibaseTagPrefix),
with("orderBy", rbacDef.getOrderBySqlExpression().sql), with("orderBy", rbacDef.getOrderBySqlExpression().sql),
with("updates", rbacDef.getUpdatableColumns().stream() with("updates", indented(rbacDef.getUpdatableColumns().stream()
.map(c -> c + " = new." + c) .map(c -> c + " = new." + c)
.collect(joining("\n"))), .collect(joining(",\n")), 2)),
with("rawTableName", rawTableName)); with("rawTableName", rawTableName));
} }
} }

View File

@ -1,18 +1,30 @@
package net.hostsharing.hsadminng.rbac.rbacdef; package net.hostsharing.hsadminng.rbac.rbacdef;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.util.function.Consumer;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
import net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionEntity;
import net.hostsharing.hsadminng.hs.office.coopshares.HsOfficeCoopSharesTransactionEntity;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerDetailsEntity;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity;
import net.hostsharing.hsadminng.hs.office.sepamandate.HsOfficeSepaMandateEntity;
import net.hostsharing.hsadminng.persistence.HasUuid; import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
import net.hostsharing.hsadminng.test.cust.TestCustomerEntity;
import net.hostsharing.hsadminng.test.pac.TestPackageEntity;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.util.*; import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Stream; import java.util.stream.Stream;
import static java.lang.reflect.Modifier.isStatic; import static java.lang.reflect.Modifier.isStatic;
@ -36,7 +48,7 @@ public class RbacView {
@Override @Override
public EntityAlias put(final String key, final EntityAlias value) { public EntityAlias put(final String key, final EntityAlias value) {
if ( containsKey(key) ) { if (containsKey(key)) {
throw new IllegalArgumentException("duplicate entityAlias: " + key); throw new IllegalArgumentException("duplicate entityAlias: " + key);
} }
return super.put(key, value); return super.put(key, value);
@ -60,6 +72,7 @@ public class RbacView {
new RbacUserReference(CREATOR); new RbacUserReference(CREATOR);
entityAliases.put("global", new EntityAlias("global")); entityAliases.put("global", new EntityAlias("global"));
} }
public RbacView withUpdatableColumns(final String... columnNames) { public RbacView withUpdatableColumns(final String... columnNames) {
Collections.addAll(updatableColumns, columnNames); Collections.addAll(updatableColumns, columnNames);
return this; return this;
@ -70,7 +83,7 @@ public class RbacView {
return this; return this;
} }
public RbacView withRestrictedViewOrderedBy(final SQL orderBySqlExpression) { public RbacView withRestrictedViewOrderBy(final SQL orderBySqlExpression) {
this.orderBySqlExpression = orderBySqlExpression; this.orderBySqlExpression = orderBySqlExpression;
return this; return this;
} }
@ -106,21 +119,22 @@ public class RbacView {
} }
private RbacPermissionDefinition createPermission(final EntityAlias entityAlias, final Permission permission) { private RbacPermissionDefinition createPermission(final EntityAlias entityAlias, final Permission permission) {
final RbacPermissionDefinition permDef = new RbacPermissionDefinition(entityAlias, permission, true); return new RbacPermissionDefinition(entityAlias, permission, null, true);
permDefs.add(permDef);
return permDef;
} }
public <EC extends RbacObject> 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 RbacObject> RbacView importRootEntityAliasProxy( public <EC extends RbacObject> RbacView importRootEntityAliasProxy(
final String aliasName, final Class<? extends HasUuid> entityClass, final SQL fetchSql, final Column dependsOnColum) { final String aliasName,
if ( rootEntityAliasProxy != null ) { final Class<? extends HasUuid> entityClass,
final SQL fetchSql,
final Column dependsOnColum) {
if (rootEntityAliasProxy != null) {
throw new IllegalStateException("there is already an entityAliasProxy: " + rootEntityAliasProxy); throw new IllegalStateException("there is already an entityAliasProxy: " + rootEntityAliasProxy);
} }
rootEntityAliasProxy = importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, false); rootEntityAliasProxy = importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, false);
@ -155,7 +169,7 @@ public class RbacView {
entityAliases.put(aliasName, entityAlias); entityAliases.put(aliasName, entityAlias);
try { try {
importAsAlias(aliasName, rbacDefinition(entityClass), asSubEntity); importAsAlias(aliasName, rbacDefinition(entityClass), asSubEntity);
} catch ( final ReflectiveOperationException exc) { } catch (final ReflectiveOperationException exc) {
throw new RuntimeException("cannot import entity: " + entityClass, exc); throw new RuntimeException("cannot import entity: " + entityClass, exc);
} }
return entityAlias; return entityAlias;
@ -178,13 +192,17 @@ public class RbacView {
entityAliases.put(mappedAliasName, new EntityAlias(mappedAliasName, entityAlias.entityClass)); entityAliases.put(mappedAliasName, new EntityAlias(mappedAliasName, entityAlias.entityClass));
}); });
importedRbacView.getRoleDefs().forEach(roleDef -> { importedRbacView.getRoleDefs().forEach(roleDef -> {
new RbacRoleDefinition( findEntityAlias(mapper.map(roleDef.entityAlias.aliasName)), roleDef.role); new RbacRoleDefinition(findEntityAlias(mapper.map(roleDef.entityAlias.aliasName)), roleDef.role);
}); });
importedRbacView.getGrantDefs().forEach(grantDef -> { importedRbacView.getGrantDefs().forEach(grantDef -> {
if (grantDef.grantType() == RbacGrantDefinition.GrantType.ROLE_TO_ROLE) { if (grantDef.grantType() == RbacGrantDefinition.GrantType.ROLE_TO_ROLE) {
findOrCreateGrantDef( findOrCreateGrantDef(
findRbacRole(mapper.map(grantDef.getSubRoleDef().entityAlias.aliasName), grantDef.getSubRoleDef().getRole()), findRbacRole(
findRbacRole(mapper.map(grantDef.getSuperRoleDef().entityAlias.aliasName), grantDef.getSuperRoleDef().getRole()) mapper.map(grantDef.getSubRoleDef().entityAlias.aliasName),
grantDef.getSubRoleDef().getRole()),
findRbacRole(
mapper.map(grantDef.getSuperRoleDef().entityAlias.aliasName),
grantDef.getSuperRoleDef().getRole())
); );
} }
}); });
@ -199,16 +217,19 @@ public class RbacView {
return new RbacExampleRole(entityAlias, role); return new RbacExampleRole(entityAlias, role);
} }
private RbacGrantDefinition grantRoleToUser(final RbacRoleDefinition roleDefinition, final RbacUserReference user) { private RbacGrantDefinition grantRoleToUser(final RbacRoleDefinition roleDefinition, final RbacUserReference user) {
return findOrCreateGrantDef(roleDefinition, user).toCreate(); return findOrCreateGrantDef(roleDefinition, user).toCreate();
} }
private RbacGrantDefinition grantPermissionToRole(final RbacPermissionDefinition permDef , final RbacRoleDefinition roleDef) { private RbacGrantDefinition grantPermissionToRole(
final RbacPermissionDefinition permDef,
final RbacRoleDefinition roleDef) {
return findOrCreateGrantDef(permDef, roleDef).toCreate(); return findOrCreateGrantDef(permDef, roleDef).toCreate();
} }
private RbacGrantDefinition grantSubRoleToSuperRole(final RbacRoleDefinition subRoleDefinition, final RbacRoleDefinition superRoleDefinition) { private RbacGrantDefinition grantSubRoleToSuperRole(
final RbacRoleDefinition subRoleDefinition,
final RbacRoleDefinition superRoleDefinition) {
return findOrCreateGrantDef(subRoleDefinition, superRoleDefinition).toCreate(); return findOrCreateGrantDef(subRoleDefinition, superRoleDefinition).toCreate();
} }
@ -220,6 +241,13 @@ public class RbacView {
return entityAlias == rootEntityAliasProxy; return entityAlias == rootEntityAliasProxy;
} }
public SQL getOrderBySqlExpression() {
if (orderBySqlExpression == null) {
return identityViewSqlQuery;
}
return orderBySqlExpression;
}
public void generateWithBaseFileName(final String baseFileName) { public void generateWithBaseFileName(final String baseFileName) {
new RbacViewMermaidFlowchart(this).generateToMarkdownFile(Path.of(OUTPUT_BASEDIR, baseFileName + "-generated.md")); new RbacViewMermaidFlowchart(this).generateToMarkdownFile(Path.of(OUTPUT_BASEDIR, baseFileName + "-generated.md"));
new RbacViewPostgresGenerator(this).generateToChangeLog(Path.of(OUTPUT_BASEDIR, baseFileName + "-generated.sql")); new RbacViewPostgresGenerator(this).generateToChangeLog(Path.of(OUTPUT_BASEDIR, baseFileName + "-generated.sql"));
@ -238,16 +266,25 @@ public class RbacView {
return RbacView.this; return RbacView.this;
} }
public RbacView grantPermission(final String entityAliasName, final Permission perm) {
final var entityAlias = findEntityAlias(entityAliasName);
final var forTable = entityAlias.getRawTableName();
findOrCreateGrantDef(findRbacPerm(entityAlias, perm, forTable), superRoleDef).toCreate();
return RbacView.this;
}
} }
@Getter @Getter
@EqualsAndHashCode @EqualsAndHashCode
public class RbacGrantDefinition { public class RbacGrantDefinition {
private final RbacUserReference userDef; private final RbacUserReference userDef;
private final RbacRoleDefinition superRoleDef; private final RbacRoleDefinition superRoleDef;
private final RbacRoleDefinition subRoleDef; private final RbacRoleDefinition subRoleDef;
private final RbacPermissionDefinition permDef; private final RbacPermissionDefinition permDef;
private boolean toCreate; private boolean assumed = true;
private boolean toCreate = false;
@Override @Override
public String toString() { public String toString() {
@ -295,8 +332,7 @@ public class RbacView {
} }
boolean isAssumed() { boolean isAssumed() {
// TODO: not implemented yet return assumed;
return true;
} }
boolean isToCreate() { boolean isToCreate() {
@ -320,6 +356,10 @@ public class RbacView {
.orElse(false); .orElse(false);
} }
public void unassumed() {
this.assumed = false;
}
public enum GrantType { public enum GrantType {
ROLE_TO_USER, ROLE_TO_USER,
ROLE_TO_ROLE, ROLE_TO_ROLE,
@ -352,22 +392,25 @@ public class RbacView {
final EntityAlias entityAlias; final EntityAlias entityAlias;
final Permission permission; final Permission permission;
final String tableName;
final boolean toCreate; final boolean toCreate;
private RbacPermissionDefinition(final EntityAlias entityAlias, final Permission permission, final boolean toCreate) { private RbacPermissionDefinition(final EntityAlias entityAlias, final Permission permission, final String tableName, final boolean toCreate) {
this.entityAlias = entityAlias; this.entityAlias = entityAlias;
this.permission = permission; this.permission = permission;
this.tableName = tableName;
this.toCreate = toCreate; this.toCreate = toCreate;
permDefs.add(this);
} }
public RbacView grantedTo(final String entityAlias, final Role role) { public RbacView grantedTo(final String entityAlias, final Role role) {
findOrCreateGrantDef(this, findRbacRole(entityAlias, role) ).toCreate(); findOrCreateGrantDef(this, findRbacRole(entityAlias, role)).toCreate();
return RbacView.this; return RbacView.this;
} }
@Override @Override
public String toString() { public String toString() {
return "perm:" + entityAlias.aliasName + permission; return "perm:" + entityAlias.aliasName + permission + ofNullable(tableName).map(tn -> ":" + tn).orElse("");
} }
} }
@ -390,26 +433,22 @@ public class RbacView {
return this; return this;
} }
public RbacRoleDefinition owningUser(final RbacUserReference.UserRole userRole) { public RbacGrantDefinition owningUser(final RbacUserReference.UserRole userRole) {
grantRoleToUser(this, findUserRef(userRole)); return grantRoleToUser(this, findUserRef(userRole));
return this;
} }
public RbacRoleDefinition permission(final Permission permission) { public RbacGrantDefinition permission(final Permission permission) {
grantPermissionToRole( createPermission(entityAlias, permission) , this); return grantPermissionToRole(createPermission(entityAlias, permission), this);
return this;
} }
public RbacRoleDefinition incomingSuperRole(final String entityAlias, final Role role) { public RbacGrantDefinition incomingSuperRole(final String entityAlias, final Role role) {
final var incomingSuperRole = findRbacRole(entityAlias, role); final var incomingSuperRole = findRbacRole(entityAlias, role);
grantSubRoleToSuperRole(this, incomingSuperRole); return grantSubRoleToSuperRole(this, incomingSuperRole);
return this;
} }
public RbacRoleDefinition outgoingSubRole(final String entityAlias, final Role role) { public RbacGrantDefinition outgoingSubRole(final String entityAlias, final Role role) {
final var outgoingSubRole = findRbacRole(entityAlias, role); final var outgoingSubRole = findRbacRole(entityAlias, role);
grantSubRoleToSuperRole(outgoingSubRole, this); return grantSubRoleToSuperRole(outgoingSubRole, this);
return this;
} }
@Override @Override
@ -430,6 +469,7 @@ public class RbacView {
} }
final UserRole role; final UserRole role;
public RbacUserReference(final UserRole creator) { public RbacUserReference(final UserRole creator) {
this.role = creator; this.role = creator;
userDefs.add(this); userDefs.add(this);
@ -443,7 +483,7 @@ public class RbacView {
EntityAlias findEntityAlias(final String aliasName) { EntityAlias findEntityAlias(final String aliasName) {
final var found = entityAliases.get(aliasName); final var found = entityAliases.get(aliasName);
if ( found == null ) { if (found == null) {
throw new IllegalArgumentException("entityAlias not found: " + aliasName); throw new IllegalArgumentException("entityAlias not found: " + aliasName);
} }
return found; return found;
@ -461,6 +501,26 @@ public class RbacView {
} }
RbacPermissionDefinition findRbacPerm(final EntityAlias entityAlias, final Permission perm, String tableName) {
return permDefs.stream()
.filter(p -> p.getEntityAlias() == entityAlias && p.getPermission() == perm)
.findFirst()
.orElseGet(() -> new RbacPermissionDefinition(entityAlias, perm, tableName, true)); // TODO: true => toCreate
}
RbacPermissionDefinition findRbacPerm(final EntityAlias entityAlias, final Permission perm) {
return findRbacPerm(entityAlias, perm, null);
}
public RbacPermissionDefinition findRbacPerm(final String entityAliasName, final Permission perm, String tableName) {
return findRbacPerm(findEntityAlias(entityAliasName), perm, tableName);
}
public RbacPermissionDefinition findRbacPerm(final String entityAliasName, final Permission perm) {
return findRbacPerm(findEntityAlias(entityAliasName), perm);
}
private RbacGrantDefinition findOrCreateGrantDef(final RbacRoleDefinition roleDefinition, final RbacUserReference user) { private RbacGrantDefinition findOrCreateGrantDef(final RbacRoleDefinition roleDefinition, final RbacUserReference user) {
return grantDefs.stream() return grantDefs.stream()
.filter(g -> g.subRoleDef == roleDefinition && g.userDef == user) .filter(g -> g.subRoleDef == roleDefinition && g.userDef == user)
@ -475,7 +535,9 @@ public class RbacView {
.orElseGet(() -> new RbacGrantDefinition(permDef, roleDef)); .orElseGet(() -> new RbacGrantDefinition(permDef, roleDef));
} }
private RbacGrantDefinition findOrCreateGrantDef(final RbacRoleDefinition subRoleDefinition, final RbacRoleDefinition superRoleDefinition) { private RbacGrantDefinition findOrCreateGrantDef(
final RbacRoleDefinition subRoleDefinition,
final RbacRoleDefinition superRoleDefinition) {
return grantDefs.stream() return grantDefs.stream()
.filter(g -> g.subRoleDef == subRoleDefinition && g.superRoleDef == superRoleDefinition) .filter(g -> g.subRoleDef == subRoleDefinition && g.superRoleDef == superRoleDefinition)
.findFirst() .findFirst()
@ -503,12 +565,13 @@ public class RbacView {
@NotNull @NotNull
@Override @Override
public SQL fetchSql() { public SQL fetchSql() {
if ( fetchSql == null ) { if (fetchSql == null) {
return SQL.noop(); return SQL.noop();
} }
return switch (fetchSql.part) { return switch (fetchSql.part) {
case SQL_QUERY -> fetchSql; case SQL_QUERY -> fetchSql;
case AUTO_FETCH -> SQL.query("SELECT * FROM " + getRawTableName() + " WHERE uuid = ${ref}." + dependsOnColum.column); case AUTO_FETCH ->
SQL.query("SELECT * FROM " + getRawTableName() + " WHERE uuid = ${ref}." + dependsOnColum.column);
default -> throw new IllegalStateException("unexpected SQL definition: " + fetchSql); default -> throw new IllegalStateException("unexpected SQL definition: " + fetchSql);
}; };
} }
@ -518,7 +581,7 @@ public class RbacView {
} }
private String withoutEntitySuffix(final String simpleEntityName) { private String withoutEntitySuffix(final String simpleEntityName) {
return simpleEntityName.substring(0, simpleEntityName.length()-"Entity".length()); return simpleEntityName.substring(0, simpleEntityName.length() - "Entity".length());
} }
String simpleName() { String simpleName() {
@ -530,12 +593,22 @@ public class RbacView {
String getRawTableName() { String getRawTableName() {
return withoutRvSuffix(entityClass.getAnnotation(Table.class).name()); return withoutRvSuffix(entityClass.getAnnotation(Table.class).name());
} }
String dependsOnColumName() {
if (dependsOnColum == null) {
throw new IllegalStateException(
"Entity " + aliasName + "(" + entityClass.getSimpleName() + ")" + ": please add dependsOnColum");
} }
return dependsOnColum.column;
}
}
public static String withoutRvSuffix(final String tableName) { public static String withoutRvSuffix(final String tableName) {
return tableName.substring(0, tableName.length()-"_rv".length()); return tableName.substring(0, tableName.length() - "_rv".length());
} }
public record Role(String roleName) { public record Role(String roleName) {
public static final Role OWNER = new Role("owner"); public static final Role OWNER = new Role("owner");
public static final Role ADMIN = new Role("admin"); public static final Role ADMIN = new Role("admin");
public static final Role AGENT = new Role("agent"); public static final Role AGENT = new Role("agent");
@ -549,11 +622,13 @@ public class RbacView {
@Override @Override
public boolean equals(final Object obj) { public boolean equals(final Object obj) {
return ((obj instanceof Role) && ((Role)obj).roleName.equals(this.roleName)); return ((obj instanceof Role) && ((Role) obj).roleName.equals(this.roleName));
} }
} }
public record Permission(String permission) { public record Permission(String permission) {
public static final Permission INSERT = new Permission("insert");
public static final Permission ALL = new Permission("*"); public static final Permission ALL = new Permission("*");
public static final Permission EDIT = new Permission("edit"); public static final Permission EDIT = new Permission("edit");
public static final Permission VIEW = new Permission("view"); public static final Permission VIEW = new Permission("view");
@ -604,7 +679,8 @@ public class RbacView {
return new SQL(null, Part.NOOP); return new SQL(null, Part.NOOP);
} }
/** Generic DSL method to specify an SQL SELECT expression. /**
* Generic DSL method to specify an SQL SELECT expression.
* *
* @param sql an SQL SELECT expression (not ending with ';) * @param sql an SQL SELECT expression (not ending with ';)
* @return the wrapped SQL expression * @return the wrapped SQL expression
@ -614,7 +690,8 @@ public class RbacView {
return new SQL(sql, Part.SQL_QUERY); return new SQL(sql, Part.SQL_QUERY);
} }
/** Generic DSL method to specify an SQL SELECT expression by just the projection part. /**
* Generic DSL method to specify an SQL SELECT expression by just the projection part.
* *
* @param projection an SQL SELECT expression, the list of columns after 'SELECT' * @param projection an SQL SELECT expression, the list of columns after 'SELECT'
* @return the wrapped SQL projection * @return the wrapped SQL projection
@ -691,7 +768,7 @@ public class RbacView {
if (outerAliasNames.contains(originalAliasName) || originalAliasName.equals("global")) { if (outerAliasNames.contains(originalAliasName) || originalAliasName.equals("global")) {
return originalAliasName; return originalAliasName;
} }
if (originalAliasName.equals(importedRbacView.rootEntityAlias.aliasName) ) { if (originalAliasName.equals(importedRbacView.rootEntityAlias.aliasName)) {
return outerAliasName; return outerAliasName;
} }
return outerAliasName + "." + originalAliasName; return outerAliasName + "." + originalAliasName;
@ -700,17 +777,19 @@ public class RbacView {
public static void main(String[] args) { public static void main(String[] args) {
Stream.of( Stream.of(
net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity.class, TestCustomerEntity.class,
net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity.class, TestPackageEntity.class,
net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerDetailsEntity.class, HsOfficePersonEntity.class,
net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity.class, HsOfficePartnerEntity.class,
net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity.class, HsOfficePartnerDetailsEntity.class,
net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity.class, HsOfficeBankAccountEntity.class,
net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionEntity.class, HsOfficeDebitorEntity.class,
net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity.class, HsOfficeRelationshipEntity.class,
net.hostsharing.hsadminng.hs.office.sepamandate.HsOfficeSepaMandateEntity.class, HsOfficeCoopAssetsTransactionEntity.class,
net.hostsharing.hsadminng.hs.office.coopshares.HsOfficeCoopSharesTransactionEntity.class, HsOfficeContactEntity.class,
net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity.class HsOfficeSepaMandateEntity.class,
HsOfficeCoopSharesTransactionEntity.class,
HsOfficeMembershipEntity.class
).forEach(c -> { ).forEach(c -> {
final Method mainMethod = Arrays.stream(c.getMethods()).filter( final Method mainMethod = Arrays.stream(c.getMethods()).filter(
m -> isStatic(m.getModifiers()) && m.getName().equals("main") m -> isStatic(m.getModifiers()) && m.getName().equals("main")
@ -719,7 +798,7 @@ public class RbacView {
.orElse(null); .orElse(null);
if (mainMethod != null) { if (mainMethod != null) {
try { try {
mainMethod.invoke(null, new Object[]{null}); mainMethod.invoke(null, new Object[] { null });
} catch (IllegalAccessException | InvocationTargetException e) { } catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }

View File

@ -97,21 +97,21 @@ public class RbacViewMermaidFlowchart {
renderGrants(PERM_TO_ROLE, "%% granting permissions to roles"); renderGrants(PERM_TO_ROLE, "%% granting permissions to roles");
} }
private void renderGrants(final RbacView.RbacGrantDefinition.GrantType f, final String t) { private void renderGrants(final RbacView.RbacGrantDefinition.GrantType grantType, final String comment) {
final var userGrants = rbacDef.getGrantDefs().stream() final var grantsOfRequestedType = rbacDef.getGrantDefs().stream()
.filter(g -> g.grantType() == f) .filter(g -> g.grantType() == grantType)
.toList(); .toList();
if ( !userGrants.isEmpty()) { if ( !grantsOfRequestedType.isEmpty()) {
flowchart.ensureSingleEmptyLine(); flowchart.ensureSingleEmptyLine();
flowchart.writeLn(t); flowchart.writeLn(comment);
userGrants.forEach(g -> flowchart.writeLn(grantDef(g))); grantsOfRequestedType.forEach(g -> flowchart.writeLn(grantDef(g)));
} }
} }
private String grantDef(final RbacView.RbacGrantDefinition grant) { private String grantDef(final RbacView.RbacGrantDefinition grant) {
final var arrow = grant.isToCreate() final var arrow = grant.isToCreate()
? grant.isAssumed() ? " ==> " : " == // ==> " ? grant.isAssumed() ? " ==> " : " == /// ==> "
: grant.isAssumed() ? " -.-> " : " -.- // -.-> "; : grant.isAssumed() ? " -.-> " : " -.- /// -.-> ";
return switch (grant.grantType()) { return switch (grant.grantType()) {
case ROLE_TO_USER -> case ROLE_TO_USER ->
// TODO: other user types not implemented yet // TODO: other user types not implemented yet

View File

@ -30,6 +30,7 @@ public class RbacViewPostgresGenerator {
new RbacObjectGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql); new RbacObjectGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql);
new RbacRoleDescriptorsGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql); new RbacRoleDescriptorsGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql);
new RolesGrantsAndPermissionsGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql); new RolesGrantsAndPermissionsGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql);
new InsertTriggerGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql);
new RbacIdentityViewGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql); new RbacIdentityViewGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql);
new RbacRestrictedViewGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql); new RbacRestrictedViewGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql);
} }

View File

@ -68,10 +68,7 @@ public class StringWriter {
return string.toString(); return string.toString();
} }
private String indented(final String text) { public static String indented(final String text, final int indentLevel) {
if ( indentLevel == 0) {
return text;
}
final var indentation = StringUtils.repeat(" ", indentLevel); final var indentation = StringUtils.repeat(" ", indentLevel);
final var indented = stream(text.split("\n")) final var indented = stream(text.split("\n"))
.map(line -> line.trim().isBlank() ? "" : indentation + line) .map(line -> line.trim().isBlank() ? "" : indentation + line)
@ -79,6 +76,13 @@ public class StringWriter {
return indented; return indented;
} }
private String indented(final String text) {
if ( indentLevel == 0) {
return text;
}
return indented(text, indentLevel);
}
record VarDef(String name, String value){} record VarDef(String name, String value){}
private static final class VarReplacer { private static final class VarReplacer {

View File

@ -0,0 +1,5 @@
package net.hostsharing.hsadminng.rbac.rbacdef;
// TODO: The whole code in this package is more like a quick hack to solve an urgent problem.
// It should be re-written in PostgreSQL pl/pgsql,
// so that no Java is needed to use this RBAC system in it's full extend.

View File

@ -4,16 +4,16 @@ 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.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.io.IOException;
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.GLOBAL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.ALL; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.VIEW;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR; 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.Role.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
@ -24,7 +24,7 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class TestCustomerEntity implements RbacObject { public class TestCustomerEntity implements HasUuid {
@Id @Id
@GeneratedValue @GeneratedValue
@ -36,22 +36,24 @@ public class TestCustomerEntity implements RbacObject {
@Column(name = "adminusername") @Column(name = "adminusername")
private String adminUserName; private String adminUserName;
public static RbacView rbac() { public static RbacView rbac() {
return rbacViewFor("customer", TestCustomerEntity.class) return rbacViewFor("customer", TestCustomerEntity.class)
.withIdentityView(SQL.projection("prefix")) .withIdentityView(SQL.projection("prefix"))
.withRestrictedViewOrderBy(SQL.expression("reference"))
.withUpdatableColumns("reference", "prefix", "adminUserName") .withUpdatableColumns("reference", "prefix", "adminUserName")
.createRole(OWNER, (with) -> { .createRole(OWNER, (with) -> {
with.owningUser(CREATOR); with.owningUser(CREATOR);
with.incomingSuperRole(GLOBAL, ADMIN); with.incomingSuperRole(GLOBAL, ADMIN);
with.permission(ALL); with.permission(ALL);
}) })
.createSubRole(ADMIN, (with) -> { .createSubRole(ADMIN)
with.permission(RbacView.Permission.custom("add-package"));
})
.createSubRole(TENANT, (with) -> { .createSubRole(TENANT, (with) -> {
with.permission(VIEW); with.permission(VIEW);
}); });
} }
public static void main(String[] args) throws IOException {
rbac().generateWithBaseFileName("113-test-customer-rbac");
}
} }

View File

@ -4,18 +4,29 @@ 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.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
import net.hostsharing.hsadminng.test.cust.TestCustomerEntity; import net.hostsharing.hsadminng.test.cust.TestCustomerEntity;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.io.IOException;
import java.util.UUID; 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;
@Entity @Entity
@Table(name = "test_package_rv") @Table(name = "test_package_rv")
@Getter @Getter
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class TestPackageEntity { public class TestPackageEntity implements HasUuid {
@Id @Id
@GeneratedValue @GeneratedValue
@ -31,4 +42,35 @@ public class TestPackageEntity {
private String name; private String name;
private String description; private String description;
public static RbacView rbac() {
return rbacViewFor("package", TestPackageEntity.class)
.withIdentityView(SQL.projection("name"))
.withUpdatableColumns("customerUuid", "description")
.importEntityAlias("customer", TestCustomerEntity.class,
dependsOnColumn("customerUuid"),
fetchedBySql("""
SELECT * FROM test_customer c
WHERE c.uuid= ${ref}.customerUuid
"""))
.toRole("customer", ADMIN).grantPermission("package", INSERT)
.createRole(OWNER, (with) -> {
with.owningUser(CREATOR);
with.incomingSuperRole("customer", ADMIN).unassumed();
with.permission(ALL);
with.permission(EDIT);
})
.createSubRole(ADMIN)
.createSubRole(TENANT, (with) -> {
with.outgoingSubRole("customer", TENANT);
with.permission(VIEW);
});
}
public static void main(String[] args) throws IOException {
rbac().generateWithBaseFileName("123-test-package-rbac");
}
} }

View File

@ -381,6 +381,7 @@ create table RbacPermission
uuid uuid primary key references RbacReference (uuid) on delete cascade, uuid uuid primary key references RbacReference (uuid) on delete cascade,
objectUuid uuid not null references RbacObject, objectUuid uuid not null references RbacObject,
op RbacOp not null, op RbacOp not null,
opTableName RbacOp,
unique (objectUuid, op) unique (objectUuid, op)
); );
@ -397,6 +398,37 @@ select exists(
); );
$$; $$;
create or replace function createPermissions(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null)
returns uuid[]
language plpgsql as $$
declare
permissionId uuid;
begin
if (forObjectUuid is null) then
raise exception 'forObjectUuid must not be null';
end if;
if (forOp = 'INSERT' && forOpTableName is null) then
raise exception 'INSERT permissions needs forOpTableName';
end if;
if (forOp <> 'INSERT' && forOpTableName is not null) then
raise exception 'forOpTableName must only be specified for ops: [INSERT]'; -- currently no other
end if;
permissionId = (select uuid from RbacPermission where objectUuid = forObjectUuid and op = forOp and opTableName = forOpTableName);
if (permissionId is null) then
insert
into RbacReference ("type")
values ('RbacPermission')
returning uuid into permissionId;
insert
into RbacPermission (uuid, objectUuid, op, opTableName)
values (permissionId, forObjectUuid, forOp, opTableName);
end if;
return permissionId;
end;
$$;
-- TODO: deprecated, remove and amend all usages to createPermission
create or replace function createPermissions(forObjectUuid uuid, permitOps RbacOp[]) create or replace function createPermissions(forObjectUuid uuid, permitOps RbacOp[])
returns uuid[] returns uuid[]
language plpgsql as $$ language plpgsql as $$
@ -430,7 +462,7 @@ begin
end; end;
$$; $$;
create or replace function findPermissionId(forObjectUuid uuid, forOp RbacOp) create or replace function findPermissionId(forObjectUuid uuid, forOp RbacOp, opTableName text = null )
returns uuid returns uuid
returns null on null input returns null on null input
stable -- leakproof stable -- leakproof
@ -439,6 +471,7 @@ select uuid
from RbacPermission p from RbacPermission p
where p.objectUuid = forObjectUuid where p.objectUuid = forObjectUuid
and p.op = forOp and p.op = forOp
and p.opTableName = opTableName
$$; $$;
create or replace function findEffectivePermissionId(forObjectUuid uuid, forOp RbacOp) create or replace function findEffectivePermissionId(forObjectUuid uuid, forOp RbacOp)
@ -552,6 +585,18 @@ select exists(
); );
$$; $$;
create or replace function hasInsertPermission(objectUuid uuid, forOp RbacOp, tableName text )
returns BOOL
stable -- leakproof
language plpgsql as $$
declare
permissionUuid uuid;
begin
permissionUuid = findPermissionId(objectUuid, forOp);
end;
$$;
create or replace function hasGlobalRoleGranted(userUuid uuid) create or replace function hasGlobalRoleGranted(userUuid uuid)
returns bool returns bool
stable -- leakproof stable -- leakproof

View File

@ -16,10 +16,10 @@ class TestCustomerEntityTest {
subgraph customer["`**customer**`"] subgraph customer["`**customer**`"]
direction TB direction TB
style customer fill:#dd4901,stroke:darkblue,stroke-width:8px style customer fill:#dd4901,stroke:#274d6e,stroke-width:8px
subgraph customer:roles[ ] subgraph customer:roles[ ]
style customer:roles fill: #dd4901 style customer:roles fill:#dd4901,stroke:white
role:customer:owner[[customer:owner]] role:customer:owner[[customer:owner]]
role:customer:admin[[customer:admin]] role:customer:admin[[customer:admin]]
@ -27,10 +27,9 @@ class TestCustomerEntityTest {
end end
subgraph customer:permissions[ ] subgraph customer:permissions[ ]
style customer:permissions fill: #dd4901 style customer:permissions fill:#dd4901,stroke:white
perm:customer:*{{customer:*}} perm:customer:*{{customer:*}}
perm:customer:add-package{{customer:add-package}}
perm:customer:view{{customer:view}} perm:customer:view{{customer:view}}
end end
end end

View File

@ -0,0 +1,71 @@
package net.hostsharing.hsadminng.test.pac;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacViewMermaidFlowchart;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class TestPackageEntityTest {
@Test
void definesRbac() {
final var rbacFlowchart = new RbacViewMermaidFlowchart(TestPackageEntity.rbac()).toString();
assertThat(rbacFlowchart).isEqualTo("""
%%{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:*{{package:*}}
perm:package:edit{{package:edit}}
perm:package:view{{package:view}}
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 users
user:creator ==> role:package:owner
%% granting roles to roles
role:global:admin -.-> 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:*
role:package:owner ==> perm:package:edit
role:package:tenant ==> perm:package:view
""");
}
}