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.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 {
@ -26,16 +27,16 @@ public class RbacRestrictedViewGenerator {
call generateRbacRestrictedView('${rawTableName}',
'${orderBy}',
$updates$
${updates}
${updates}
$updates$);
--//
""",
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;
@ -36,7 +48,7 @@ public class RbacView {
@Override
public EntityAlias put(final String key, final EntityAlias value) {
if ( containsKey(key) ) {
if (containsKey(key)) {
throw new IllegalArgumentException("duplicate entityAlias: " + key);
}
return super.put(key, value);
@ -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,21 +119,22 @@ 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) {
for ( String alias: aliasNames ) {
for (String alias : aliasNames) {
entityAliases.put(alias, new EntityAlias(alias));
}
return this;
}
public <EC extends RbacObject> RbacView importRootEntityAliasProxy(
final String aliasName, final Class<? extends HasUuid> entityClass, final SQL fetchSql, final Column dependsOnColum) {
if ( rootEntityAliasProxy != null ) {
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);
}
rootEntityAliasProxy = importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, false);
@ -155,7 +169,7 @@ public class RbacView {
entityAliases.put(aliasName, entityAlias);
try {
importAsAlias(aliasName, rbacDefinition(entityClass), asSubEntity);
} catch ( final ReflectiveOperationException exc) {
} catch (final ReflectiveOperationException exc) {
throw new RuntimeException("cannot import entity: " + entityClass, exc);
}
return entityAlias;
@ -178,13 +192,17 @@ public class RbacView {
entityAliases.put(mappedAliasName, new EntityAlias(mappedAliasName, entityAlias.entityClass));
});
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 -> {
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) {
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() {
@ -310,7 +346,7 @@ public class RbacView {
boolean dependsOnColumn(final String columnName) {
return dependsRoleDefOnColumnName(this.superRoleDef, columnName)
|| dependsRoleDefOnColumnName(this.subRoleDef, columnName);
|| dependsRoleDefOnColumnName(this.subRoleDef, columnName);
}
private Boolean dependsRoleDefOnColumnName(final RbacRoleDefinition superRoleDef, final String columnName) {
@ -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,22 +392,25 @@ 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) {
findOrCreateGrantDef(this, findRbacRole(entityAlias, role) ).toCreate();
findOrCreateGrantDef(this, findRbacRole(entityAlias, role)).toCreate();
return RbacView.this;
}
@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) {
final var incomingSuperRole = findRbacRole(entityAlias, role);
grantSubRoleToSuperRole(this, incomingSuperRole);
return this;
public RbacGrantDefinition incomingSuperRole(final String entityAlias, final Role role) {
final var incomingSuperRole = findRbacRole(entityAlias, role);
return grantSubRoleToSuperRole(this, incomingSuperRole);
}
public RbacRoleDefinition outgoingSubRole(final String entityAlias, final Role role) {
final var outgoingSubRole = findRbacRole(entityAlias, role);
grantSubRoleToSuperRole(outgoingSubRole, this);
return this;
public RbacGrantDefinition outgoingSubRole(final String entityAlias, final Role role) {
final var outgoingSubRole = findRbacRole(entityAlias, role);
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);
@ -443,7 +483,7 @@ public class RbacView {
EntityAlias findEntityAlias(final String aliasName) {
final var found = entityAliases.get(aliasName);
if ( found == null ) {
if (found == null) {
throw new IllegalArgumentException("entityAlias not found: " + aliasName);
}
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) {
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()
@ -484,31 +546,32 @@ public class RbacView {
record EntityAlias(String aliasName, Class<? extends RbacObject> entityClass, SQL fetchSql, Column dependsOnColum, boolean isSubEntity) {
public EntityAlias(final String aliasName) {
this(aliasName, null, null, null, false);
}
public EntityAlias(final String aliasName) {
this(aliasName, null, null, null, false);
}
public EntityAlias(final String aliasName, final Class<? extends RbacObject> entityClass) {
this(aliasName, entityClass, null, null, false);
}
public EntityAlias(final String aliasName, final Class<? extends RbacObject> entityClass) {
this(aliasName, entityClass, null, null, false);
}
boolean isGlobal() {
return aliasName().equals("global");
}
boolean isPlaceholder() {
return entityClass == null;
return entityClass == null;
}
@NotNull
@Override
public SQL fetchSql() {
if ( fetchSql == null ) {
if (fetchSql == null) {
return SQL.noop();
}
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);
};
}
@ -518,7 +581,7 @@ public class RbacView {
}
private String withoutEntitySuffix(final String simpleEntityName) {
return simpleEntityName.substring(0, simpleEntityName.length()-"Entity".length());
return simpleEntityName.substring(0, simpleEntityName.length() - "Entity".length());
}
String simpleName() {
@ -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());
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");
@ -549,11 +622,13 @@ public class RbacView {
@Override
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 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
@ -688,10 +765,10 @@ public class RbacView {
}
String map(final String originalAliasName) {
if (outerAliasNames.contains(originalAliasName) || originalAliasName.equals("global")) {
if (outerAliasNames.contains(originalAliasName) || originalAliasName.equals("global")) {
return originalAliasName;
}
if (originalAliasName.equals(importedRbacView.rootEntityAlias.aliasName) ) {
if (originalAliasName.equals(importedRbacView.rootEntityAlias.aliasName)) {
return outerAliasName;
}
return outerAliasName + "." + originalAliasName;
@ -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")
@ -719,7 +798,7 @@ public class RbacView {
.orElse(null);
if (mainMethod != null) {
try {
mainMethod.invoke(null, new Object[]{null});
mainMethod.invoke(null, new Object[] { null });
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}

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

@ -378,9 +378,10 @@ create domain RbacOp as varchar(67)
create table RbacPermission
(
uuid uuid primary key references RbacReference (uuid) on delete cascade,
objectUuid uuid not null references RbacObject,
op RbacOp not null,
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,21 +16,20 @@ 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]]
role:customer:tenant[[customer:tenant]]
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
""");
}
}