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
15 changed files with 464 additions and 111 deletions
Showing only changes of commit b2cea1e882 - Show all commits

View File

@ -5,6 +5,7 @@ import lombok.experimental.FieldNameConstants;
import net.hostsharing.hsadminng.errors.DisplayName;
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.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
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.RbacUserReference.UserRole.CREATOR;
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.stringify.Stringify.stringify;
@ -67,7 +67,7 @@ public class HsOfficePersonEntity implements HasUuid, Stringifyable {
public static RbacView rbac() {
return rbacViewFor("person", HsOfficePersonEntity.class)
.withIdentityView(projection("concat(tradeName, familyName, givenName)"))
.withIdentityView(SQL.projection("concat(tradeName, familyName, givenName)"))
.withUpdatableColumns("personType", "tradeName", "givenName", "familyName")
.createRole(OWNER, (with) -> {
with.permission(ALL);

View File

@ -85,7 +85,7 @@ public class HsOfficeRelationshipEntity implements HasUuid, Stringifyable {
|| '-with-' || target.relType || '-'
|| (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)"))
.withUpdatableColumns("contactUuid")
.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 net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.indented;
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with;
public class RbacRestrictedViewGenerator {
@ -33,9 +34,9 @@ public class RbacRestrictedViewGenerator {
""",
with("liquibaseTagPrefix", liquibaseTagPrefix),
with("orderBy", rbacDef.getOrderBySqlExpression().sql),
with("updates", rbacDef.getUpdatableColumns().stream()
with("updates", indented(rbacDef.getUpdatableColumns().stream()
.map(c -> c + " = new." + c)
.collect(joining("\n"))),
.collect(joining(",\n")), 2)),
with("rawTableName", rawTableName));
}
}

View File

@ -1,18 +1,30 @@
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.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.rbac.rbacobject.RbacObject;
import net.hostsharing.hsadminng.test.cust.TestCustomerEntity;
import net.hostsharing.hsadminng.test.pac.TestPackageEntity;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Stream;
import static java.lang.reflect.Modifier.isStatic;
@ -60,6 +72,7 @@ public class RbacView {
new RbacUserReference(CREATOR);
entityAliases.put("global", new EntityAlias("global"));
}
public RbacView withUpdatableColumns(final String... columnNames) {
Collections.addAll(updatableColumns, columnNames);
return this;
@ -70,7 +83,7 @@ public class RbacView {
return this;
}
public RbacView withRestrictedViewOrderedBy(final SQL orderBySqlExpression) {
public RbacView withRestrictedViewOrderBy(final SQL orderBySqlExpression) {
this.orderBySqlExpression = orderBySqlExpression;
return this;
}
@ -106,9 +119,7 @@ public class RbacView {
}
private RbacPermissionDefinition createPermission(final EntityAlias entityAlias, final Permission permission) {
final RbacPermissionDefinition permDef = new RbacPermissionDefinition(entityAlias, permission, true);
permDefs.add(permDef);
return permDef;
return new RbacPermissionDefinition(entityAlias, permission, null, true);
}
public <EC extends RbacObject> RbacView declarePlaceholderEntityAliases(final String... aliasNames) {
@ -119,7 +130,10 @@ public class RbacView {
}
public <EC extends RbacObject> RbacView importRootEntityAliasProxy(
final String aliasName, final Class<? extends HasUuid> entityClass, final SQL fetchSql, final Column dependsOnColum) {
final String aliasName,
final Class<? extends HasUuid> entityClass,
final SQL fetchSql,
final Column dependsOnColum) {
if (rootEntityAliasProxy != null) {
throw new IllegalStateException("there is already an entityAliasProxy: " + rootEntityAliasProxy);
}
@ -183,8 +197,12 @@ public class RbacView {
importedRbacView.getGrantDefs().forEach(grantDef -> {
if (grantDef.grantType() == RbacGrantDefinition.GrantType.ROLE_TO_ROLE) {
findOrCreateGrantDef(
findRbacRole(mapper.map(grantDef.getSubRoleDef().entityAlias.aliasName), grantDef.getSubRoleDef().getRole()),
findRbacRole(mapper.map(grantDef.getSuperRoleDef().entityAlias.aliasName), grantDef.getSuperRoleDef().getRole())
findRbacRole(
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);
}
private RbacGrantDefinition grantRoleToUser(final RbacRoleDefinition roleDefinition, final RbacUserReference user) {
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();
}
private RbacGrantDefinition grantSubRoleToSuperRole(final RbacRoleDefinition subRoleDefinition, final RbacRoleDefinition superRoleDefinition) {
private RbacGrantDefinition grantSubRoleToSuperRole(
final RbacRoleDefinition subRoleDefinition,
final RbacRoleDefinition superRoleDefinition) {
return findOrCreateGrantDef(subRoleDefinition, superRoleDefinition).toCreate();
}
@ -220,6 +241,13 @@ public class RbacView {
return entityAlias == rootEntityAliasProxy;
}
public SQL getOrderBySqlExpression() {
if (orderBySqlExpression == null) {
return identityViewSqlQuery;
}
return orderBySqlExpression;
}
public void generateWithBaseFileName(final String baseFileName) {
new RbacViewMermaidFlowchart(this).generateToMarkdownFile(Path.of(OUTPUT_BASEDIR, baseFileName + "-generated.md"));
new RbacViewPostgresGenerator(this).generateToChangeLog(Path.of(OUTPUT_BASEDIR, baseFileName + "-generated.sql"));
@ -238,16 +266,25 @@ public class RbacView {
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
@EqualsAndHashCode
public class RbacGrantDefinition {
private final RbacUserReference userDef;
private final RbacRoleDefinition superRoleDef;
private final RbacRoleDefinition subRoleDef;
private final RbacPermissionDefinition permDef;
private boolean toCreate;
private boolean assumed = true;
private boolean toCreate = false;
@Override
public String toString() {
@ -295,8 +332,7 @@ public class RbacView {
}
boolean isAssumed() {
// TODO: not implemented yet
return true;
return assumed;
}
boolean isToCreate() {
@ -320,6 +356,10 @@ public class RbacView {
.orElse(false);
}
public void unassumed() {
this.assumed = false;
}
public enum GrantType {
ROLE_TO_USER,
ROLE_TO_ROLE,
@ -352,12 +392,15 @@ public class RbacView {
final EntityAlias entityAlias;
final Permission permission;
final String tableName;
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.permission = permission;
this.tableName = tableName;
this.toCreate = toCreate;
permDefs.add(this);
}
public RbacView grantedTo(final String entityAlias, final Role role) {
@ -367,7 +410,7 @@ public class RbacView {
@Override
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;
}
public RbacRoleDefinition owningUser(final RbacUserReference.UserRole userRole) {
grantRoleToUser(this, findUserRef(userRole));
return this;
public RbacGrantDefinition owningUser(final RbacUserReference.UserRole userRole) {
return grantRoleToUser(this, findUserRef(userRole));
}
public RbacRoleDefinition permission(final Permission permission) {
grantPermissionToRole( createPermission(entityAlias, permission) , this);
return this;
public RbacGrantDefinition permission(final Permission permission) {
return grantPermissionToRole(createPermission(entityAlias, permission), 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);
grantSubRoleToSuperRole(this, incomingSuperRole);
return this;
return grantSubRoleToSuperRole(this, incomingSuperRole);
}
public RbacRoleDefinition outgoingSubRole(final String entityAlias, final Role role) {
public RbacGrantDefinition outgoingSubRole(final String entityAlias, final Role role) {
final var outgoingSubRole = findRbacRole(entityAlias, role);
grantSubRoleToSuperRole(outgoingSubRole, this);
return this;
return grantSubRoleToSuperRole(outgoingSubRole, this);
}
@Override
@ -430,6 +469,7 @@ public class RbacView {
}
final UserRole role;
public RbacUserReference(final UserRole creator) {
this.role = creator;
userDefs.add(this);
@ -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) {
return grantDefs.stream()
.filter(g -> g.subRoleDef == roleDefinition && g.userDef == user)
@ -475,7 +535,9 @@ public class RbacView {
.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()
.filter(g -> g.subRoleDef == subRoleDefinition && g.superRoleDef == superRoleDefinition)
.findFirst()
@ -508,7 +570,8 @@ public class RbacView {
}
return switch (fetchSql.part) {
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);
};
}
@ -530,12 +593,22 @@ public class RbacView {
String getRawTableName() {
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) {
return tableName.substring(0, tableName.length() - "_rv".length());
}
public record Role(String roleName) {
public static final Role OWNER = new Role("owner");
public static final Role ADMIN = new Role("admin");
public static final Role AGENT = new Role("agent");
@ -554,6 +627,8 @@ public class RbacView {
}
public record Permission(String permission) {
public static final Permission INSERT = new Permission("insert");
public static final Permission ALL = new Permission("*");
public static final Permission EDIT = new Permission("edit");
public static final Permission VIEW = new Permission("view");
@ -604,7 +679,8 @@ public class RbacView {
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 ';)
* @return the wrapped SQL expression
@ -614,7 +690,8 @@ public class RbacView {
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'
* @return the wrapped SQL projection
@ -700,17 +777,19 @@ public class RbacView {
public static void main(String[] args) {
Stream.of(
net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity.class,
net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity.class,
net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerDetailsEntity.class,
net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity.class,
net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity.class,
net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity.class,
net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionEntity.class,
net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity.class,
net.hostsharing.hsadminng.hs.office.sepamandate.HsOfficeSepaMandateEntity.class,
net.hostsharing.hsadminng.hs.office.coopshares.HsOfficeCoopSharesTransactionEntity.class,
net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity.class
TestCustomerEntity.class,
TestPackageEntity.class,
HsOfficePersonEntity.class,
HsOfficePartnerEntity.class,
HsOfficePartnerDetailsEntity.class,
HsOfficeBankAccountEntity.class,
HsOfficeDebitorEntity.class,
HsOfficeRelationshipEntity.class,
HsOfficeCoopAssetsTransactionEntity.class,
HsOfficeContactEntity.class,
HsOfficeSepaMandateEntity.class,
HsOfficeCoopSharesTransactionEntity.class,
HsOfficeMembershipEntity.class
).forEach(c -> {
final Method mainMethod = Arrays.stream(c.getMethods()).filter(
m -> isStatic(m.getModifiers()) && m.getName().equals("main")

View File

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

View File

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

View File

@ -68,10 +68,7 @@ public class StringWriter {
return string.toString();
}
private String indented(final String text) {
if ( indentLevel == 0) {
return text;
}
public static String indented(final String text, final int indentLevel) {
final var indentation = StringUtils.repeat(" ", indentLevel);
final var indented = stream(text.split("\n"))
.map(line -> line.trim().isBlank() ? "" : indentation + line)
@ -79,6 +76,13 @@ public class StringWriter {
return indented;
}
private String indented(final String text) {
if ( indentLevel == 0) {
return text;
}
return indented(text, indentLevel);
}
record VarDef(String name, String value){}
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.NoArgsConstructor;
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.rbac.rbacobject.RbacObject;
import jakarta.persistence.*;
import java.io.IOException;
import java.util.UUID;
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.VIEW;
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.rbacViewFor;
@ -24,7 +24,7 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class TestCustomerEntity implements RbacObject {
public class TestCustomerEntity implements HasUuid {
@Id
@GeneratedValue
@ -36,22 +36,24 @@ public class TestCustomerEntity implements RbacObject {
@Column(name = "adminusername")
private String adminUserName;
public static RbacView rbac() {
return rbacViewFor("customer", TestCustomerEntity.class)
.withIdentityView(SQL.projection("prefix"))
.withRestrictedViewOrderBy(SQL.expression("reference"))
.withUpdatableColumns("reference", "prefix", "adminUserName")
.createRole(OWNER, (with) -> {
with.owningUser(CREATOR);
with.incomingSuperRole(GLOBAL, ADMIN);
with.permission(ALL);
})
.createSubRole(ADMIN, (with) -> {
with.permission(RbacView.Permission.custom("add-package"));
})
.createSubRole(ADMIN)
.createSubRole(TENANT, (with) -> {
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.NoArgsConstructor;
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 jakarta.persistence.*;
import java.io.IOException;
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
@Table(name = "test_package_rv")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class TestPackageEntity {
public class TestPackageEntity implements HasUuid {
@Id
@GeneratedValue
@ -31,4 +42,35 @@ public class TestPackageEntity {
private String name;
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,
objectUuid uuid not null references RbacObject,
op RbacOp not null,
opTableName RbacOp,
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[])
returns uuid[]
language plpgsql as $$
@ -430,7 +462,7 @@ begin
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 null on null input
stable -- leakproof
@ -439,6 +471,7 @@ select uuid
from RbacPermission p
where p.objectUuid = forObjectUuid
and p.op = forOp
and p.opTableName = opTableName
$$;
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)
returns bool
stable -- leakproof

View File

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