improved RBAC generators #26

Merged
hsh-michaelhoennig merged 17 commits from improved-rbac-generator into master 2024-03-26 11:25:18 +01:00
19 changed files with 405 additions and 153 deletions
Showing only changes of commit 0decfe1132 - Show all commits

View File

@ -131,7 +131,7 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable {
"vatBusiness",
"vatReverseCharge",
"defaultPrefix" /* TODO: do we want that updatable? */)
.createPermission(custom("new-debitor")).grantedTo("global", ADMIN)
.createPermission(INSERT).grantedTo("global", ADMIN)
.importRootEntityAliasProxy("debitorRel", HsOfficeRelationEntity.class,
fetchedBySql("""

View File

@ -84,7 +84,7 @@ public class HsOfficePartnerDetailsEntity implements HasUuid, Stringifyable {
"birthName",
"birthday",
"dateOfDeath")
.createPermission(custom("new-partner-details")).grantedTo("global", ADMIN)
.createPermission(INSERT).grantedTo("global", ADMIN)
.importRootEntityAliasProxy("partnerRel", HsOfficeRelationEntity.class,
fetchedBySql("""

View File

@ -90,7 +90,7 @@ public class HsOfficePartnerEntity implements Stringifyable, HasUuid {
"partnerRelUuid",
"personUuid",
"contactUuid")
.createPermission(custom("new-partner")).grantedTo("global", ADMIN)
.createPermission(INSERT).grantedTo("global", ADMIN)
.importRootEntityAliasProxy("partnerRel", HsOfficeRelationEntity.class,
fetchedBySql("SELECT * FROM hs_office_relation AS r WHERE r.uuid = ${ref}.partnerRelUuid"),

View File

@ -4,6 +4,7 @@ import java.util.Optional;
import java.util.function.BinaryOperator;
import java.util.stream.Stream;
import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.NEW;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.PERM_TO_ROLE;
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with;
@ -53,16 +54,16 @@ public class InsertTriggerGenerator {
FOR row IN SELECT * FROM ${rawSuperTableName}
LOOP
roleUuid := findRoleId(${rawSuperRoleDescriptor}(row));
roleUuid := findRoleId(${rawSuperRoleDescriptor});
permissionUuid := createPermission(row.uuid, 'INSERT', '${rawSubTableName}');
call grantPermissionToRole(roleUuid, permissionUuid);
call grantPermissionToRole(permissionUuid, roleUuid);
END LOOP;
END;
$$;
""",
with("rawSubTableName", rbacDef.getRootEntityAlias().getRawTableName()),
with("rawSuperTableName", superRoleDef.getEntityAlias().getRawTableName()),
with("rawSuperRoleDescriptor", toVar(superRoleDef))
with("rawSuperRoleDescriptor", toRoleDescriptor(superRoleDef, "row"))
);
});
}
@ -79,50 +80,50 @@ public class InsertTriggerGenerator {
strict as $$
begin
call grantPermissionToRole(
${rawSuperRoleDescriptor}(NEW),
createPermission(NEW.uuid, 'INSERT', '${rawSubTableName}'));
createPermission(NEW.uuid, 'INSERT', '${rawSubTableName}'),
${rawSuperRoleDescriptor});
return NEW;
end; $$;
create trigger ${rawSubTableName}_${rawSuperTableName}_insert_tg
-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
create trigger z_${rawSubTableName}_${rawSuperTableName}_insert_tg
after insert on ${rawSuperTableName}
for each row
execute procedure ${rawSubTableName}_${rawSuperTableName}_insert_tf();
""",
with("rawSubTableName", rbacDef.getRootEntityAlias().getRawTableName()),
with("rawSuperTableName", superRoleDef.getEntityAlias().getRawTableName()),
with("rawSuperRoleDescriptor", toVar(superRoleDef))
with("rawSuperRoleDescriptor", toRoleDescriptor(superRoleDef, NEW.name()))
);
});
}
private void generateInsertCheckTrigger(final StringWriter plPgSql) {
plPgSql.writeLn("""
/**
Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable}.
*/
create or replace function ${rawSubTable}_insert_permission_missing_tf()
returns trigger
language plpgsql as $$
begin
raise exception '[403] insert into ${rawSubTable} not allowed for current subjects % (%)',
currentSubjects(), currentSubjectsUuids();
end; $$;
""",
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()));
getOptionalInsertGrant().ifPresentOrElse(g -> {
plPgSql.writeLn("""
create trigger ${rawSubTable}_insert_permission_check_tg
before insert on ${rawSubTable}
for each row
when ( not hasInsertPermission(NEW.${referenceColumn}, 'INSERT', '${rawSubTable}') )
execute procedure ${rawSubTable}_insert_permission_missing_tf();
""",
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()),
with("referenceColumn", g.getSuperRoleDef().getEntityAlias().dependsOnColumName() ));
if (!g.getSuperRoleDef().getEntityAlias().isGlobal()) {
if (rbacDef.isRootEntityAlias(g.getSuperRoleDef().getEntityAlias())) {
generateInsertPermissionTriggerAllowByDirectRole(plPgSql, g);
} else {
generateInsertPermissionTriggerAllowByIndirectRole(plPgSql, g);
}
} else {
switch (g.getSuperRoleDef().getRole()) {
case ADMIN -> {
generateInsertPermissionTriggerAllowOnlyGlobalAdmin(plPgSql);
}
case GUEST -> {
// no permission check trigger generated, as anybody can insert rows into this table
}
default -> {
throw new IllegalArgumentException(
"invalid global role for INSERT permission: " + g.getSuperRoleDef().getRole());
}
}
}
},
() -> {
plPgSql.writeLn("""
plPgSql.writeLn("""
-- FIXME: Where is this case necessary?
create trigger ${rawSubTable}_insert_permission_check_tg
before insert on ${rawSubTable}
for each row
@ -135,6 +136,92 @@ public class InsertTriggerGenerator {
});
}
private void generateInsertPermissionTriggerAllowByDirectRole(final StringWriter plPgSql, final RbacView.RbacGrantDefinition g) {
plPgSql.writeLn("""
/**
Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable}.
*/
create or replace function ${rawSubTable}_insert_permission_missing_tf()
returns trigger
language plpgsql as $$
begin
raise exception '[403] insert into ${rawSubTable} not allowed for current subjects % (%)',
currentSubjects(), currentSubjectsUuids();
end; $$;
create trigger ${rawSubTable}_insert_permission_check_tg
before insert on ${rawSubTable}
for each row
when ( not hasInsertPermission(NEW.${referenceColumn}, 'INSERT', '${rawSubTable}') )
execute procedure ${rawSubTable}_insert_permission_missing_tf();
""",
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()),
with("referenceColumn", g.getSuperRoleDef().getEntityAlias().dependsOnColumName()));
}
private void generateInsertPermissionTriggerAllowByIndirectRole(
final StringWriter plPgSql,
final RbacView.RbacGrantDefinition g) {
plPgSql.writeLn("""
/**
Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable}.
*/
create or replace function ${rawSubTable}_insert_permission_missing_tf()
returns trigger
language plpgsql as $$
begin
if ( not hasInsertPermission(
( SELECT ${varName}.uuid FROM
""",
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()),
with("varName", g.getSuperRoleDef().getEntityAlias().aliasName()));
plPgSql.indented(3, () -> {
plPgSql.writeLn(
"(" + g.getSuperRoleDef().getEntityAlias().fetchSql().sql + ") AS ${varName}",
with("varName", g.getSuperRoleDef().getEntityAlias().aliasName()),
with("ref", NEW.name()));
});
plPgSql.writeLn("""
), 'INSERT', '${rawSubTable}') ) then
raise exception
'[403] insert into ${rawSubTable} not allowed for current subjects % (%)',
currentSubjects(), currentSubjectsUuids();
end if;
return NEW;
end; $$;
create trigger ${rawSubTable}_insert_permission_check_tg
before insert on ${rawSubTable}
for each row
execute procedure ${rawSubTable}_insert_permission_missing_tf();
""",
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()));
}
private void generateInsertPermissionTriggerAllowOnlyGlobalAdmin(final StringWriter plPgSql) {
plPgSql.writeLn("""
/**
Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable}.
*/
create or replace function ${rawSubTable}_insert_permission_missing_tf()
returns trigger
language plpgsql as $$
begin
raise exception '[403] insert into ${rawSubTable} not allowed for current subjects % (%)',
currentSubjects(), currentSubjectsUuids();
end; $$;
create trigger ${rawSubTable}_insert_permission_check_tg
before insert on ${rawSubTable}
for each row
when ( not isGlobalAdmin() )
execute procedure ${rawSubTable}_insert_permission_missing_tf();
""",
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()));
}
private Stream<RbacView.RbacGrantDefinition> getInsertGrants() {
return rbacDef.getGrantDefs().stream()
.filter(g -> g.grantType() == PERM_TO_ROLE)
@ -162,4 +249,12 @@ public class InsertTriggerGenerator {
return uncapitalize(roleDef.getEntityAlias().simpleName()) + capitalize(roleDef.getRole().roleName());
}
private String toRoleDescriptor(final RbacView.RbacRoleDefinition roleDef, final String ref) {
final var functionName = toVar(roleDef);
if (roleDef.getEntityAlias().isGlobal()) {
return functionName + "()";
}
return functionName + "(" + ref + ")";
}
}

View File

@ -31,7 +31,7 @@ public class RbacIdentityViewGenerator {
$idName$);
""";
case SQL_QUERY -> """
call generateRbacIdentityViewFromProjection('${rawTableName}', $idName$
call generateRbacIdentityViewFromQuery('${rawTableName}', $idName$
${identityViewSqlPart}
$idName$);
""";

View File

@ -24,9 +24,11 @@ public class RbacRestrictedViewGenerator {
--changeset ${liquibaseTagPrefix}-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
call generateRbacRestrictedView('${rawTableName}',
'${orderBy}',
$orderBy$
${orderBy}
$orderBy$,
$updates$
${updates}
${updates}
$updates$);
--//

View File

@ -32,6 +32,7 @@ import java.util.stream.Stream;
import static java.lang.reflect.Modifier.isStatic;
import static java.util.Arrays.stream;
import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.autoFetched;
import static org.apache.commons.lang3.StringUtils.uncapitalize;
@ -141,35 +142,42 @@ public class RbacView {
if (rootEntityAliasProxy != null) {
throw new IllegalStateException("there is already an entityAliasProxy: " + rootEntityAliasProxy);
}
rootEntityAliasProxy = importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, false);
rootEntityAliasProxy = importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, false, NOT_NULL);
return this;
}
public RbacView importSubEntityAlias(
final String aliasName, final Class<? extends HasUuid> entityClass,
final SQL fetchSql, final Column dependsOnColum) {
importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, true);
importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, true, NOT_NULL);
return this;
}
public RbacView importEntityAlias(
final String aliasName, final Class<? extends HasUuid> entityClass,
final Column dependsOnColum, final SQL fetchSql, final Nullable nullable) {
importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, false, nullable);
return this;
}
public RbacView importEntityAlias(
final String aliasName, final Class<? extends HasUuid> entityClass,
final Column dependsOnColum, final SQL fetchSql) {
importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, false);
importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, false, NOT_NULL);
return this;
}
public RbacView importEntityAlias(
final String aliasName, final Class<? extends HasUuid> entityClass,
final Column dependsOnColum) {
importEntityAliasImpl(aliasName, entityClass, autoFetched(), dependsOnColum, false);
importEntityAliasImpl(aliasName, entityClass, autoFetched(), dependsOnColum, false, null);
return this;
}
private EntityAlias importEntityAliasImpl(
final String aliasName, final Class<? extends HasUuid> entityClass,
final SQL fetchSql, final Column dependsOnColum, boolean asSubEntity) {
final var entityAlias = new EntityAlias(aliasName, entityClass, fetchSql, dependsOnColum, asSubEntity);
final SQL fetchSql, final Column dependsOnColum, boolean asSubEntity, final Nullable nullable) {
final var entityAlias = new EntityAlias(aliasName, entityClass, fetchSql, dependsOnColum, asSubEntity, nullable);
entityAliases.put(aliasName, entityAlias);
try {
importAsAlias(aliasName, rbacDefinition(entityClass), asSubEntity);
@ -281,15 +289,19 @@ 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();
public RbacView grantPermission(final Permission perm) {
final var forTable = rootEntityAlias.getRawTableName();
findOrCreateGrantDef(findRbacPerm(rootEntityAlias, perm, forTable), superRoleDef).toCreate();
return RbacView.this;
}
}
public enum Nullable {
NOT_NULL, // DEFAULT
NULLABLE
}
@Getter
@EqualsAndHashCode
public class RbacGrantDefinition {
@ -560,14 +572,14 @@ public class RbacView {
.orElseGet(() -> new RbacGrantDefinition(subRoleDefinition, superRoleDefinition));
}
record EntityAlias(String aliasName, Class<? extends RbacObject> entityClass, SQL fetchSql, Column dependsOnColum, boolean isSubEntity) {
record EntityAlias(String aliasName, Class<? extends RbacObject> entityClass, SQL fetchSql, Column dependsOnColum, boolean isSubEntity, Nullable nullable) {
public EntityAlias(final String aliasName) {
this(aliasName, null, null, null, false);
this(aliasName, null, null, null, false, null);
}
public EntityAlias(final String aliasName, final Class<? extends RbacObject> entityClass) {
this(aliasName, entityClass, null, null, false);
this(aliasName, entityClass, null, null, false, null);
}
boolean isGlobal() {
@ -626,39 +638,35 @@ public class RbacView {
return tableName.substring(0, tableName.length() - "_rv".length());
}
public record Role(String roleName) {
public enum Role {
public static final Role OWNER = new Role("owner");
public static final Role ADMIN = new Role("admin");
public static final Role AGENT = new Role("agent");
public static final Role TENANT = new Role("tenant");
public static final Role REFERRER = new Role("referrer");
OWNER,
ADMIN,
AGENT,
TENANT,
REFERRER,
GUEST;
@Override
public String toString() {
return ":" + roleName;
return ":" + roleName();
}
@Override
public boolean equals(final Object obj) {
return ((obj instanceof Role) && ((Role) obj).roleName.equals(this.roleName));
String roleName() {
return name().toLowerCase();
}
}
public record Permission(String permission) {
public static final Permission INSERT = new Permission("INSERT");
public static final Permission DELETE = new Permission("DELETE");
public static final Permission UPDATE = new Permission("UPDATE");
public static final Permission SELECT = new Permission("SELECT");
public static Permission custom(final String permission) {
return new Permission(permission);
}
public enum Permission {
INSERT,
DELETE,
UPDATE,
SELECT;
@Override
public String toString() {
return ":" + permission;
return ":" + name();
}
}

View File

@ -37,8 +37,11 @@ public class RbacViewPostgresGenerator {
@Override
public String toString() {
return plPgSql.toString();
}
return plPgSql.toString()
.replace("\n\n\n", "\n\n")
.replace("-- ====", "\n-- ====")
.replace("\n\n--//", "\n--//");
}
@SneakyThrows
public void generateToChangeLog(final Path outputPath) {

View File

@ -82,6 +82,7 @@ class RolesGrantsAndPermissionsGenerator {
plPgSql.writeLn("begin");
plPgSql.indented(() -> {
plPgSql.writeLn("call enterTriggerForObjectUuid(NEW.uuid);");
plPgSql.writeLn();
generateCreateRolesAndGrantsAfterInsert(plPgSql);
plPgSql.ensureSingleEmptyLine();
plPgSql.writeLn("call leaveTriggerForObjectUuid(NEW.uuid);");
@ -90,6 +91,37 @@ class RolesGrantsAndPermissionsGenerator {
plPgSql.writeLn();
}
private void generateSimplifiedUpdateTriggerFunction(final StringWriter plPgSql) {
final var updateConditions = updatableEntityAliases()
.map(RbacView.EntityAlias::dependsOnColumName)
.distinct()
.map(columnName -> "NEW." + columnName + " is distinct from OLD." + columnName)
.collect(joining( "\n or "));
plPgSql.writeLn("""
/*
Called from the AFTER UPDATE TRIGGER to re-wire the grants.
*/
create or replace procedure updateRbacRulesFor${simpleEntityName}(
OLD ${rawTableName},
NEW ${rawTableName}
)
language plpgsql as $$
begin
if ${updateConditions} then
delete from rbacgrants g where g.grantedbytriggerof = OLD.uuid;
call buildRbacSystemFor${simpleEntityName}(NEW);
end if;
end; $$;
""",
with("simpleEntityName", simpleEntityName),
with("rawTableName", rawTableName),
with("updateConditions", updateConditions));
}
private void generateUpdateTriggerFunction(final StringWriter plPgSql) {
plPgSql.writeLn("""
/*
@ -109,7 +141,7 @@ class RolesGrantsAndPermissionsGenerator {
plPgSql.chopEmptyLines();
plPgSql.indented(() -> {
updatableEntityAliases()
referencedEntityAliases()
.forEach((ea) -> {
plPgSql.writeLn(entityRefVar(OLD, ea) + " " + ea.getRawTableName() + ";");
plPgSql.writeLn(entityRefVar(NEW, ea) + " " + ea.getRawTableName() + ";");
@ -120,6 +152,7 @@ class RolesGrantsAndPermissionsGenerator {
plPgSql.writeLn("begin");
plPgSql.indented(() -> {
plPgSql.writeLn("call enterTriggerForObjectUuid(NEW.uuid);");
plPgSql.writeLn();
generateUpdateRolesAndGrantsAfterUpdate(plPgSql);
plPgSql.ensureSingleEmptyLine();
plPgSql.writeLn("call leaveTriggerForObjectUuid(NEW.uuid);");
@ -132,11 +165,18 @@ class RolesGrantsAndPermissionsGenerator {
return updatableEntityAliases().anyMatch(e -> true);
}
private boolean hasAnyUpdatableAndNullableEntityAliases() {
return updatableEntityAliases()
.filter(ea -> ea.nullable() == RbacView.Nullable.NULLABLE)
.anyMatch(e -> true);
}
private void generateCreateRolesAndGrantsAfterInsert(final StringWriter plPgSql) {
referencedEntityAliases()
.forEach((ea) -> plPgSql.writeLn(
ea.fetchSql().sql + " into " + entityRefVar(NEW, ea) + ";",
with("ref", NEW.name())));
.forEach((ea) -> {
generateFetchedVars(plPgSql, ea, NEW);
plPgSql.writeLn();
});
createRolesWithGrantsSql(plPgSql, OWNER);
createRolesWithGrantsSql(plPgSql, ADMIN);
@ -165,14 +205,11 @@ class RolesGrantsAndPermissionsGenerator {
private void generateUpdateRolesAndGrantsAfterUpdate(final StringWriter plPgSql) {
plPgSql.ensureSingleEmptyLine();
updatableEntityAliases()
referencedEntityAliases()
.forEach((ea) -> {
plPgSql.writeLn(
ea.fetchSql().sql + " into " + entityRefVar(OLD, ea) + ";",
with("ref", OLD.name()));
plPgSql.writeLn(
ea.fetchSql().sql + " into " + entityRefVar(NEW, ea) + ";",
with("ref", NEW.name()));
generateFetchedVars(plPgSql, ea, OLD);
generateFetchedVars(plPgSql, ea, NEW);
plPgSql.writeLn();
});
updatableEntityAliases()
@ -190,6 +227,23 @@ class RolesGrantsAndPermissionsGenerator {
});
}
private void generateFetchedVars(
final StringWriter plPgSql,
final RbacView.EntityAlias ea,
final PostgresTriggerReference old) {
plPgSql.writeLn(
ea.fetchSql().sql + " INTO " + entityRefVar(old, ea) + ";",
with("ref", old.name()));
if (ea.nullable() == RbacView.Nullable.NOT_NULL) {
plPgSql.writeLn(
"assert ${entityRefVar}.uuid is not null, format('${entityRefVar} must not be null for ${REF}.${dependsOnColumn} = %s', ${REF}.${dependsOnColumn});",
with("entityRefVar", entityRefVar(old, ea)),
with("dependsOnColumn", ea.dependsOnColumName()),
with("ref", old.name()));
plPgSql.writeLn();
}
}
private boolean isUpdatable(final RbacView.Column c) {
return rbacDef.getUpdatableColumns().contains(c);
}
@ -222,7 +276,7 @@ class RolesGrantsAndPermissionsGenerator {
.replace("${subRoleRef}", roleRef(OLD, grantDef.getSubRoleDef()))
.replace("${superRoleRef}", roleRef(OLD, grantDef.getSuperRoleDef()));
case PERM_TO_ROLE -> "call revokePermissionFromRole(${permRef}, ${superRoleRef});"
.replace("${permRef}", findPerm(OLD, grantDef.getPermDef()))
.replace("${permRef}", getPerm(OLD, grantDef.getPermDef()))
.replace("${superRoleRef}", roleRef(OLD, grantDef.getSuperRoleDef()));
};
}
@ -246,6 +300,10 @@ class RolesGrantsAndPermissionsGenerator {
return permRef("findPermissionId", ref, permDef);
}
private String getPerm(final PostgresTriggerReference ref, final RbacPermissionDefinition permDef) {
return permRef("getPermissionId", ref, permDef);
}
private String createPerm(final PostgresTriggerReference ref, final RbacPermissionDefinition permDef) {
return permRef("createPermission", ref, permDef);
}
@ -256,7 +314,7 @@ class RolesGrantsAndPermissionsGenerator {
.replace("${entityRef}", rbacDef.isRootEntityAlias(permDef.entityAlias)
? ref.name()
: refVarName(ref, permDef.entityAlias))
.replace("${perm}", permDef.permission.permission());
.replace("${perm}", permDef.permission.name());
}
private String refVarName(final PostgresTriggerReference ref, final RbacView.EntityAlias entityAlias) {
@ -301,12 +359,12 @@ class RolesGrantsAndPermissionsGenerator {
generatePermissionsForRole(plPgSql, role);
generateUserGrantsForRole(plPgSql, role);
generateIncomingSuperRolesForRole(plPgSql, role);
generateOutgoingSubRolesForRole(plPgSql, role);
generateUserGrantsForRole(plPgSql, role);
plPgSql.chopTail(",\n");
plPgSql.writeLn();
});
@ -333,7 +391,7 @@ class RolesGrantsAndPermissionsGenerator {
final var arrayElements = permissionGrantsForRole.stream()
.map(RbacView.RbacGrantDefinition::getPermDef)
.map(RbacPermissionDefinition::getPermission)
.map(RbacView.Permission::permission)
.map(RbacView.Permission::name)
.map(p -> "'" + p + "'")
.sorted()
.toList();
@ -444,7 +502,11 @@ class RolesGrantsAndPermissionsGenerator {
private void generateUpdateTrigger(final StringWriter plPgSql) {
generateHeader(plPgSql, "update");
generateUpdateTriggerFunction(plPgSql);
if ( hasAnyUpdatableAndNullableEntityAliases() ) {
generateSimplifiedUpdateTriggerFunction(plPgSql);
} else {
generateUpdateTriggerFunction(plPgSql);
}
plPgSql.writeLn("""
/*

View File

@ -38,12 +38,26 @@ public class StringWriter {
--indentLevel;
}
void indent(int levels) {
indentLevel += levels;
}
void unindent(int levels) {
indentLevel -= levels;
}
void indented(final Runnable indented) {
indent();
indented.run();
unindent();
}
void indented(int levels, final Runnable indented) {
indent(levels);
indented.run();
unindent(levels);
}
boolean chopTail(final String tail) {
if (string.toString().endsWith(tail)) {
string.setLength(string.length() - tail.length());
@ -103,8 +117,8 @@ public class StringWriter {
text = matcher.replaceAll(varDef.value());
});
return text;
} catch (Exception exc) {
throw exc;
} catch (final RuntimeException exc) {
throw exc; // FIXME: just for debugging, remove try/catch before merging to master
}
}
}

View File

@ -21,6 +21,8 @@ import static net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService.
@Service
public class RbacGrantsDiagramService {
private static final int GRANT_LIMIT = 500;
public static void writeToFile(final String title, final String graph, final String fileName) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))) {
@ -42,7 +44,11 @@ public class RbacGrantsDiagramService {
PERMISSIONS,
NOT_ASSUMED,
TEST_ENTITIES,
NON_TEST_ENTITIES
NON_TEST_ENTITIES;
public static final EnumSet<Include> ALL = EnumSet.allOf(Include.class);
public static final EnumSet<Include> ALL_TEST_ENTITY_RELATED = EnumSet.of(USERS, DETAILS, NOT_ASSUMED, TEST_ENTITIES, PERMISSIONS);
public static final EnumSet<Include> ALL_NON_TEST_ENTITY_RELATED = EnumSet.of(USERS, DETAILS, NOT_ASSUMED, NON_TEST_ENTITIES, PERMISSIONS);
}
@Autowired
@ -55,7 +61,7 @@ public class RbacGrantsDiagramService {
private EntityManager em;
public String allGrantsToCurrentUser(final EnumSet<Include> includes) {
final var graph = new HashSet<RawRbacGrantEntity>();
final var graph = new LimitedHashSet<RawRbacGrantEntity>();
for ( UUID subjectUuid: context.currentSubjectsUuids() ) {
traverseGrantsTo(graph, subjectUuid, includes);
}
@ -65,6 +71,10 @@ public class RbacGrantsDiagramService {
private void traverseGrantsTo(final Set<RawRbacGrantEntity> graph, final UUID refUuid, final EnumSet<Include> includes) {
final var grants = rawGrantRepo.findByAscendingUuid(refUuid);
grants.forEach(g -> {
if ( g.getDescendantIdName() == null ) {
// FIXME: what's that?
return;
}
if (!includes.contains(PERMISSIONS) && g.getDescendantIdName().startsWith("perm ")) {
return;
}
@ -88,7 +98,7 @@ public class RbacGrantsDiagramService {
.setParameter("targetObject", targetObject)
.setParameter("op", op)
.getSingleResult();
final var graph = new HashSet<RawRbacGrantEntity>();
final var graph = new LimitedHashSet<RawRbacGrantEntity>();
traverseGrantsFrom(graph, refUuid, includes);
return toMermaidFlowchart(graph, includes);
}
@ -116,7 +126,7 @@ public class RbacGrantsDiagramService {
)
.collect(groupingBy(RbacGrantsDiagramService::renderEntityIdName))
.entrySet().stream()
.map(entity -> "subgraph " + quoted(entity.getKey()) + renderSubgraph(entity.getKey()) + "\n\n "
.map(entity -> "subgraph " + cleanId(entity.getKey()) + renderSubgraph(entity.getKey()) + "\n\n "
+ entity.getValue().stream()
.map(n -> renderNode(n.idName(), n.uuid()).replace("\n", "\n "))
.sorted()
@ -127,14 +137,15 @@ public class RbacGrantsDiagramService {
: "";
final var grants = graph.stream()
.map(g -> quoted(g.getAscendantIdName())
.map(g -> cleanId(g.getAscendantIdName())
+ " -->" + (g.isAssumed() ? " " : "|XX| ")
+ quoted(g.getDescendantIdName()))
+ cleanId(g.getDescendantIdName()))
.sorted()
.collect(joining("\n"));
final var avoidCroppedNodeLabels = "%%{init:{'flowchart':{'htmlLabels':false}}}%%\n\n";
return (includes.contains(DETAILS) ? avoidCroppedNodeLabels : "")
+ (grants.length() > GRANT_LIMIT ? "%% too many grants, graph is cropped\n" : "")
+ "flowchart TB\n\n"
+ entities
+ grants;
@ -151,7 +162,7 @@ public class RbacGrantsDiagramService {
// }
// return "[" + table + "\n" + entity + "]";
// }
return "[" + entityId + "]";
return "[" + cleanId(entityId) + "]";
}
private static String renderEntityIdName(final Node node) {
@ -170,7 +181,7 @@ public class RbacGrantsDiagramService {
}
private String renderNode(final String idName, final UUID uuid) {
return quoted(idName) + renderNodeContent(idName, uuid);
return cleanId(idName) + renderNodeContent(idName, uuid);
}
private String renderNodeContent(final String idName, final UUID uuid) {
@ -196,9 +207,24 @@ public class RbacGrantsDiagramService {
}
@NotNull
private static String quoted(final String idName) {
return idName.replace(" ", ":").replaceAll("@.*", "");
private static String cleanId(final String idName) {
return idName.replace(" ", ":").replaceAll("@.*", "")
.replace("[", "").replace("]", "").replace("(", "").replace(")", "").replace(",", "");
}
class LimitedHashSet<T> extends HashSet<T> {
@Override
public boolean add(final T t) {
if (size() < GRANT_LIMIT ) {
return super.add(t);
} else {
return false;
}
}
}
}
record Node(String idName, UUID uuid) {

View File

@ -53,7 +53,7 @@ public class TestDomainEntity implements HasUuid {
SELECT * FROM test_package p
WHERE p.uuid= ${ref}.packageUuid
"""))
.toRole("package", ADMIN).grantPermission("domain", INSERT)
.toRole("package", ADMIN).grantPermission(INSERT)
.createRole(OWNER, (with) -> {
with.incomingSuperRole("package", ADMIN);

View File

@ -54,7 +54,7 @@ public class TestPackageEntity implements HasUuid {
SELECT * FROM test_customer c
WHERE c.uuid= ${ref}.customerUuid
"""))
.toRole("customer", ADMIN).grantPermission("package", INSERT)
.toRole("customer", ADMIN).grantPermission(INSERT)
.createRole(OWNER, (with) -> {
with.incomingSuperRole("customer", ADMIN);

View File

@ -1,6 +1,6 @@
### rbac customer
This code generated was by RbacViewMermaidFlowchartGenerator at 2024-03-11T11:29:11.571772062.
This code generated was by RbacViewMermaidFlowchartGenerator at 2024-03-21T09:53:18.451453701.
```mermaid
%%{init:{'flowchart':{'htmlLabels':false}}}%%

View File

@ -1,5 +1,6 @@
--liquibase formatted sql
-- This code generated was by RbacViewPostgresGenerator at 2024-03-11T11:29:11.584886824.
-- This code generated was by RbacViewPostgresGenerator at 2024-03-21T09:53:18.467932975.
-- ============================================================================
--changeset test-customer-rbac-OBJECT:1 endDelimiter:--//
@ -36,8 +37,8 @@ begin
perform createRoleWithGrants(
testCustomerOwner(NEW),
permissions => array['DELETE'],
userUuids => array[currentUserUuid()],
incomingSuperRoles => array[globalAdmin(unassumed())]
incomingSuperRoles => array[globalAdmin(unassumed())],
userUuids => array[currentUserUuid()]
);
perform createRoleWithGrants(
@ -72,9 +73,9 @@ create trigger insertTriggerForTestCustomer_tg
after insert on test_customer
for each row
execute procedure insertTriggerForTestCustomer_tf();
--//
-- ============================================================================
--changeset test-customer-rbac-INSERT:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
@ -97,8 +98,8 @@ create trigger test_customer_insert_permission_check_tg
-- only global admins are allowed to insert any rows.
when ( not isGlobalAdmin() )
execute procedure test_customer_insert_permission_missing_tf();
--//
-- ============================================================================
--changeset test-customer-rbac-IDENTITY-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
@ -106,18 +107,19 @@ create trigger test_customer_insert_permission_check_tg
call generateRbacIdentityViewFromProjection('test_customer', $idName$
prefix
$idName$);
--//
-- ============================================================================
--changeset test-customer-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
call generateRbacRestrictedView('test_customer',
'reference',
$orderBy$
reference
$orderBy$,
$updates$
reference = new.reference,
reference = new.reference,
prefix = new.prefix,
adminUserName = new.adminUserName
$updates$);
--//

View File

@ -1,6 +1,6 @@
### rbac package
This code generated was by RbacViewMermaidFlowchartGenerator at 2024-03-11T11:29:11.624847792.
This code generated was by RbacViewMermaidFlowchartGenerator at 2024-03-21T09:53:51.758424330.
```mermaid
%%{init:{'flowchart':{'htmlLabels':false}}}%%

View File

@ -1,5 +1,6 @@
--liquibase formatted sql
-- This code generated was by RbacViewPostgresGenerator at 2024-03-11T11:29:11.625353859.
-- This code generated was by RbacViewPostgresGenerator at 2024-03-21T09:53:51.767062425.
-- ============================================================================
--changeset test-package-rbac-OBJECT:1 endDelimiter:--//
@ -33,9 +34,12 @@ declare
begin
call enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM test_customer c
WHERE c.uuid= NEW.customerUuid
into newCustomer;
INTO newCustomer;
assert newCustomer.uuid is not null, format('newCustomer must not be null for NEW.customerUuid = %s', NEW.customerUuid);
perform createRoleWithGrants(
testPackageOwner(NEW),
@ -75,9 +79,9 @@ create trigger insertTriggerForTestPackage_tg
after insert on test_package
for each row
execute procedure insertTriggerForTestPackage_tf();
--//
-- ============================================================================
--changeset test-package-rbac-update-trigger:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
@ -101,14 +105,18 @@ begin
SELECT * FROM test_customer c
WHERE c.uuid= OLD.customerUuid
into oldCustomer;
INTO oldCustomer;
assert oldCustomer.uuid is not null, format('oldCustomer must not be null for OLD.customerUuid = %s', OLD.customerUuid);
SELECT * FROM test_customer c
WHERE c.uuid= NEW.customerUuid
into newCustomer;
INTO newCustomer;
assert newCustomer.uuid is not null, format('newCustomer must not be null for NEW.customerUuid = %s', NEW.customerUuid);
if NEW.customerUuid <> OLD.customerUuid then
call revokePermissionFromRole(findPermissionId(OLD.uuid, 'INSERT'), testCustomerAdmin(oldCustomer));
call revokePermissionFromRole(getPermissionId(OLD.uuid, 'INSERT'), testCustomerAdmin(oldCustomer));
call revokeRoleFromRole(testPackageOwner(OLD), testCustomerAdmin(oldCustomer));
call grantRoleToRole(testPackageOwner(NEW), testCustomerAdmin(newCustomer));
@ -138,9 +146,9 @@ create trigger updateTriggerForTestPackage_tg
after update on test_package
for each row
execute procedure updateTriggerForTestPackage_tf();
--//
-- ============================================================================
--changeset test-package-rbac-INSERT:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
@ -160,7 +168,7 @@ do language plpgsql $$
LOOP
roleUuid := findRoleId(testCustomerAdmin(row));
permissionUuid := createPermission(row.uuid, 'INSERT', 'test_package');
call grantPermissionToRole(roleUuid, permissionUuid);
call grantPermissionToRole(permissionUuid, roleUuid);
END LOOP;
END;
$$;
@ -174,12 +182,13 @@ create or replace function test_package_test_customer_insert_tf()
strict as $$
begin
call grantPermissionToRole(
testCustomerAdmin(NEW),
createPermission(NEW.uuid, 'INSERT', 'test_package'));
createPermission(NEW.uuid, 'INSERT', 'test_package'),
testCustomerAdmin(NEW));
return NEW;
end; $$;
create trigger test_package_test_customer_insert_tg
-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
create trigger z_test_package_test_customer_insert_tg
after insert on test_customer
for each row
execute procedure test_package_test_customer_insert_tf();
@ -191,17 +200,27 @@ create or replace function test_package_insert_permission_missing_tf()
returns trigger
language plpgsql as $$
begin
raise exception '[403] insert into test_package not allowed for current subjects % (%)',
currentSubjects(), currentSubjectsUuids();
if ( not hasInsertPermission(
( SELECT customer.uuid FROM
(SELECT * FROM test_customer c
WHERE c.uuid= NEW.customerUuid
) AS customer
), 'INSERT', 'test_package') ) then
raise exception
'[403] insert into test_package not allowed for current subjects % (%)',
currentSubjects(), currentSubjectsUuids();
end if;
return NEW;
end; $$;
create trigger test_package_insert_permission_check_tg
before insert on test_package
for each row
when ( not hasInsertPermission(NEW.customerUuid, 'INSERT', 'test_package') )
execute procedure test_package_insert_permission_missing_tf();
--//
-- ============================================================================
--changeset test-package-rbac-IDENTITY-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
@ -209,18 +228,19 @@ create trigger test_package_insert_permission_check_tg
call generateRbacIdentityViewFromProjection('test_package', $idName$
name
$idName$);
--//
-- ============================================================================
--changeset test-package-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
call generateRbacRestrictedView('test_package',
'name',
$orderBy$
name
$orderBy$,
$updates$
version = new.version,
version = new.version,
customerUuid = new.customerUuid,
description = new.description
$updates$);
--//

View File

@ -1,6 +1,6 @@
### rbac domain
This code generated was by RbacViewMermaidFlowchartGenerator at 2024-03-11T11:29:11.644658132.
This code generated was by RbacViewMermaidFlowchartGenerator at 2024-03-21T09:53:31.860490657.
```mermaid
%%{init:{'flowchart':{'htmlLabels':false}}}%%

View File

@ -1,5 +1,6 @@
--liquibase formatted sql
-- This code generated was by RbacViewPostgresGenerator at 2024-03-11T11:29:11.645391647.
-- This code generated was by RbacViewPostgresGenerator at 2024-03-21T09:53:31.873124905.
-- ============================================================================
--changeset test-domain-rbac-OBJECT:1 endDelimiter:--//
@ -33,9 +34,12 @@ declare
begin
call enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM test_package p
WHERE p.uuid= NEW.packageUuid
into newPackage;
INTO newPackage;
assert newPackage.uuid is not null, format('newPackage must not be null for NEW.packageUuid = %s', NEW.packageUuid);
perform createRoleWithGrants(
testDomainOwner(NEW),
@ -71,9 +75,9 @@ create trigger insertTriggerForTestDomain_tg
after insert on test_domain
for each row
execute procedure insertTriggerForTestDomain_tf();
--//
-- ============================================================================
--changeset test-domain-rbac-update-trigger:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
@ -97,14 +101,18 @@ begin
SELECT * FROM test_package p
WHERE p.uuid= OLD.packageUuid
into oldPackage;
INTO oldPackage;
assert oldPackage.uuid is not null, format('oldPackage must not be null for OLD.packageUuid = %s', OLD.packageUuid);
SELECT * FROM test_package p
WHERE p.uuid= NEW.packageUuid
into newPackage;
INTO newPackage;
assert newPackage.uuid is not null, format('newPackage must not be null for NEW.packageUuid = %s', NEW.packageUuid);
if NEW.packageUuid <> OLD.packageUuid then
call revokePermissionFromRole(findPermissionId(OLD.uuid, 'INSERT'), testPackageAdmin(oldPackage));
call revokePermissionFromRole(getPermissionId(OLD.uuid, 'INSERT'), testPackageAdmin(oldPackage));
call revokeRoleFromRole(testDomainOwner(OLD), testPackageAdmin(oldPackage));
call grantRoleToRole(testDomainOwner(NEW), testPackageAdmin(newPackage));
@ -137,9 +145,9 @@ create trigger updateTriggerForTestDomain_tg
after update on test_domain
for each row
execute procedure updateTriggerForTestDomain_tf();
--//
-- ============================================================================
--changeset test-domain-rbac-INSERT:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
@ -159,7 +167,7 @@ do language plpgsql $$
LOOP
roleUuid := findRoleId(testPackageAdmin(row));
permissionUuid := createPermission(row.uuid, 'INSERT', 'test_domain');
call grantPermissionToRole(roleUuid, permissionUuid);
call grantPermissionToRole(permissionUuid, roleUuid);
END LOOP;
END;
$$;
@ -173,12 +181,13 @@ create or replace function test_domain_test_package_insert_tf()
strict as $$
begin
call grantPermissionToRole(
testPackageAdmin(NEW),
createPermission(NEW.uuid, 'INSERT', 'test_domain'));
createPermission(NEW.uuid, 'INSERT', 'test_domain'),
testPackageAdmin(NEW));
return NEW;
end; $$;
create trigger test_domain_test_package_insert_tg
-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
create trigger z_test_domain_test_package_insert_tg
after insert on test_package
for each row
execute procedure test_domain_test_package_insert_tf();
@ -190,17 +199,27 @@ create or replace function test_domain_insert_permission_missing_tf()
returns trigger
language plpgsql as $$
begin
raise exception '[403] insert into test_domain not allowed for current subjects % (%)',
currentSubjects(), currentSubjectsUuids();
if ( not hasInsertPermission(
( SELECT package.uuid FROM
(SELECT * FROM test_package p
WHERE p.uuid= NEW.packageUuid
) AS package
), 'INSERT', 'test_domain') ) then
raise exception
'[403] insert into test_domain not allowed for current subjects % (%)',
currentSubjects(), currentSubjectsUuids();
end if;
return NEW;
end; $$;
create trigger test_domain_insert_permission_check_tg
before insert on test_domain
for each row
when ( not hasInsertPermission(NEW.packageUuid, 'INSERT', 'test_domain') )
execute procedure test_domain_insert_permission_missing_tf();
--//
-- ============================================================================
--changeset test-domain-rbac-IDENTITY-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
@ -208,18 +227,19 @@ create trigger test_domain_insert_permission_check_tg
call generateRbacIdentityViewFromProjection('test_domain', $idName$
name
$idName$);
--//
-- ============================================================================
--changeset test-domain-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
call generateRbacRestrictedView('test_domain',
'name',
$orderBy$
name
$orderBy$,
$updates$
version = new.version,
version = new.version,
packageUuid = new.packageUuid,
description = new.description
$updates$);
--//