allow-multiple-insert-permission-grants (#49)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #49
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
Michael Hoennig 2024-04-29 11:43:49 +02:00
parent 66332b6de2
commit dbe695c214
45 changed files with 1387 additions and 2332 deletions

View File

@ -179,7 +179,9 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject {
.createSubRole(TENANT, (with) -> {
with.outgoingSubRole("debitorRel", TENANT);
with.permission(SELECT);
});
})
.limitDiagramTo("bookingItem", "debitorRel", "global");
}
public static void main(String[] args) throws IOException {

View File

@ -37,6 +37,7 @@ import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOU
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.CaseDef.inCaseOf;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.CaseDef.inOtherCases;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase;
@ -156,9 +157,10 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject {
dependsOnColumn("parentAssetUuid"),
directlyFetchedByDependsOnColumn(),
NULLABLE)
// TODO.rbac: implement multiple INSERT-rules, e.g. for Asset.bookingItem + Asset.parentAsset
//.toRole("parentServer", AGENT).grantPermission(INSERT)
)
.toRole("parentServer", ADMIN).grantPermission(INSERT)
.toRole("bookingItem", AGENT).grantPermission(INSERT)
),
inOtherCases(then -> {})
)
.createRole(OWNER, (with) -> {
@ -171,7 +173,9 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject {
.createSubRole(TENANT, (with) -> {
with.outgoingSubRole("bookingItem", TENANT);
with.permission(SELECT);
});
})
.limitDiagramTo("asset", "bookingItem", "bookingItem.debitorRel", "parentServer", "global");
}
public static void main(String[] args) throws IOException {

View File

@ -19,7 +19,6 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.CaseDef.inCaseOf;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.CaseDef.inOtherCases;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*;

View File

@ -1,12 +1,16 @@
package net.hostsharing.hsadminng.rbac.rbacdef;
import java.util.Optional;
import java.util.Set;
import java.util.function.BinaryOperator;
import java.util.stream.Stream;
import static java.util.stream.Collectors.joining;
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.RbacView.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.GUEST;
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with;
import static org.apache.commons.lang3.StringUtils.capitalize;
import static org.apache.commons.lang3.StringUtils.uncapitalize;
@ -22,194 +26,121 @@ public class InsertTriggerGenerator {
}
void generateTo(final StringWriter plPgSql) {
generateLiquibaseChangesetHeader(plPgSql);
generateGrantInsertRoleToExistingObjects(plPgSql);
generateInsertPermissionGrantTrigger(plPgSql);
generateInsertCheckTrigger(plPgSql);
plPgSql.writeLn("--//");
if (isInsertPermissionGrantedToGlobalGuest()) {
// any user is allowed to insert new rows => no insert check needed
return;
}
generateInsertGrants(plPgSql);
generateInsertPermissionChecks(plPgSql);
}
private void generateLiquibaseChangesetHeader(final StringWriter plPgSql) {
private void generateInsertGrants(final StringWriter plPgSql) {
if (isInsertPermissionIsNotGrantedAtAll()) {
generateInsertPermissionTriggerAlwaysDisallow(plPgSql);
} else {
generateInsertPermissionGrants(plPgSql);
}
}
private void generateInsertPermissionGrants(final StringWriter plPgSql) {
plPgSql.writeLn("""
-- ============================================================================
--changeset ${liquibaseTagPrefix}-rbac-INSERT:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
""",
-- ============================================================================
--changeset ${liquibaseTagPrefix}-rbac-GRANTING-INSERT-PERMISSION:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
""",
with("liquibaseTagPrefix", liquibaseTagPrefix));
}
private void generateGrantInsertRoleToExistingObjects(final StringWriter plPgSql) {
getOptionalInsertSuperRole().ifPresent( superRoleDef -> {
getInsertGrants().forEach( g -> {
plPgSql.writeLn("""
/*
Creates INSERT INTO ${rawSubTableName} permissions for the related ${rawSuperTableName} rows.
*/
do language plpgsql $$
declare
row ${rawSuperTableName};
begin
call defineContext('create INSERT INTO ${rawSubTableName} permissions for the related ${rawSuperTableName} rows');
FOR row IN SELECT * FROM ${rawSuperTableName}${typeCondition}
LOOP
call grantPermissionToRole(
createPermission(row.uuid, 'INSERT', '${rawSubTableName}'),
${rawSuperRoleDescriptor});
END LOOP;
END;
$$;
""",
with("rawSubTableName", rbacDef.getRootEntityAlias().getRawTableName()),
with("rawSuperTableName", superRoleDef.getEntityAlias().getRawTableName()),
with("rawSuperRoleDescriptor", toRoleDescriptor(superRoleDef, "row")),
with("typeCondition", superRoleDef.getEntityAlias().isCaseDependent()
? "\n\t\t\tWHERE type = '${case}'".replace("${case}", superRoleDef.getEntityAlias().usingCase().value)
: "")
);
});
}
-- granting INSERT permission to ${rawSubTable} ----------------------------
""",
with("rawSubTable", g.getSuperRoleDef().getEntityAlias().getRawTableName()));
private void generateInsertPermissionGrantTrigger(final StringWriter plPgSql) {
getOptionalInsertSuperRole().ifPresent( superRoleDef -> {
plPgSql.writeLn("""
if (isGrantToADifferentTable(g)) {
plPgSql.writeLn(
"""
/*
Grants INSERT INTO ${rawSubTable} permissions to specified role of pre-existing ${rawSuperTable} rows.
*/
do language plpgsql $$
declare
row ${rawSuperTable};
begin
call defineContext('create INSERT INTO ${rawSubTable} permissions for pre-exising ${rawSuperTable} rows');
FOR row IN SELECT * FROM ${rawSuperTable}
${whenCondition}
LOOP
call grantPermissionToRole(
createPermission(row.uuid, 'INSERT', '${rawSubTable}'),
${superRoleRef});
END LOOP;
end;
$$;
""",
with("whenCondition", g.getSuperRoleDef().getEntityAlias().isCaseDependent()
// TODO.impl: 'type' needs to be dynamically generated
? "WHERE type = '${value}'"
.replace("${value}", g.getSuperRoleDef().getEntityAlias().usingCase().value)
: "-- unconditional for all rows in that table"),
with("rawSuperTable", g.getSuperRoleDef().getEntityAlias().getRawTableName()),
with("rawSubTable", g.getPermDef().getEntityAlias().getRawTableName()),
with("superRoleRef", toRoleDescriptor(g.getSuperRoleDef(), "row")));
} else {
plPgSql.writeLn("""
-- Granting INSERT INTO hs_hosting_asset permissions to specified role of pre-existing hs_hosting_asset rows slipped,
-- because there cannot yet be any pre-existing rows in the same table yet.
""",
with("rawSuperTable", g.getSuperRoleDef().getEntityAlias().getRawTableName()),
with("rawSubTable", g.getPermDef().getEntityAlias().getRawTableName()));
}
plPgSql.writeLn("""
/**
Adds ${rawSubTableName} INSERT permission to specified role of new ${rawSuperTableName} rows.
Grants ${rawSubTable} INSERT permission to specified role of new ${rawSuperTable} rows.
*/
create or replace function ${rawSubTableName}_${rawSuperTableName}_insert_tf()
create or replace function new_${rawSubTable}_grants_insert_to_${rawSuperTable}_tf()
returns trigger
language plpgsql
strict as $$
begin
${typeConditionIf}call grantPermissionToRole(
createPermission(NEW.uuid, 'INSERT', '${rawSubTableName}'),
${rawSuperRoleDescriptor});${typeConditionEndIf}
${ifConditionThen}
call grantPermissionToRole(
createPermission(NEW.uuid, 'INSERT', '${rawSubTable}'),
${superRoleRef});
${ifConditionEnd}
return NEW;
end; $$;
-- 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}
-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
create trigger z_new_${rawSubTable}_grants_insert_to_${rawSuperTable}_tg
after insert on ${rawSuperTable}
for each row
execute procedure ${rawSubTableName}_${rawSuperTableName}_insert_tf();
execute procedure new_${rawSubTable}_grants_insert_to_${rawSuperTable}_tf();
""",
with("rawSubTableName", rbacDef.getRootEntityAlias().getRawTableName()),
with("rawSuperTableName", superRoleDef.getEntityAlias().getRawTableName()),
with("rawSuperRoleDescriptor", toRoleDescriptor(superRoleDef, NEW.name())),
with("typeConditionIf",
superRoleDef.getEntityAlias().isCaseDependent()
? "if NEW.type = '${case}' then\n\t\t".replace("${case}", superRoleDef.getEntityAlias().usingCase().value)
: ""),
with("typeConditionEndIf", superRoleDef.getEntityAlias().isCaseDependent()
? "\n\tend if;"
: "")
);
with("ifConditionThen", g.getSuperRoleDef().getEntityAlias().isCaseDependent()
// TODO.impl: .type needs to be dynamically generated
? "if NEW.type = '" + g.getSuperRoleDef().getEntityAlias().usingCase().value + "' then"
: "-- unconditional for all rows in that table"),
with("ifConditionEnd", g.getSuperRoleDef().getEntityAlias().isCaseDependent()
? "end if;"
: "-- end."),
with("superRoleRef", toRoleDescriptor(g.getSuperRoleDef(), NEW.name())),
with("rawSuperTable", g.getSuperRoleDef().getEntityAlias().getRawTableName()),
with("rawSubTable", g.getPermDef().getEntityAlias().getRawTableName()));
});
}
private void generateInsertCheckTrigger(final StringWriter plPgSql) {
getOptionalInsertGrant().ifPresentOrElse(g -> {
if (g.getSuperRoleDef().getEntityAlias().isGlobal()) {
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());
}
}
} else {
if (g.getSuperRoleDef().getEntityAlias().isFetchedByDirectForeignKey()) {
generateInsertPermissionTriggerAllowByRoleOfDirectForeignKey(plPgSql, g);
} else {
generateInsertPermissionTriggerAllowByRoleOfIndirectForeignKey(plPgSql, g);
}
}
},
() -> {
System.err.println("WARNING: no explicit INSERT grant for " + rbacDef.getRootEntityAlias().simpleName() + " => implicitly grant INSERT to global:ADMIN");
generateInsertPermissionTriggerAllowOnlyGlobalAdmin(plPgSql);
});
}
private void generateInsertPermissionTriggerAllowByRoleOfDirectForeignKey(final StringWriter plPgSql, final RbacView.RbacGrantDefinition g) {
private void generateInsertPermissionTriggerAlwaysDisallow(final StringWriter plPgSql) {
plPgSql.writeLn("""
/**
Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable},
where the check is performed by a direct role.
A direct role is a role depending on a foreign key directly available in the NEW row.
*/
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; $$;
-- ============================================================================
--changeset ${liquibaseTagPrefix}-rbac-ALWAYS-DISALLOW-INSERT:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
""",
with("liquibaseTagPrefix", liquibaseTagPrefix));
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 generateInsertPermissionTriggerAllowByRoleOfIndirectForeignKey(
final StringWriter plPgSql,
final RbacView.RbacGrantDefinition g) {
plPgSql.writeLn("""
/**
Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable},
where the check is performed by an indirect role.
An indirect role is a role which depends on an object uuid which is not a direct foreign key
of the source entity, but needs to be fetched via joined tables.
*/
create or replace function ${rawSubTable}_insert_permission_check_tf()
returns trigger
language plpgsql as $$
declare
superRoleObjectUuid uuid;
begin
""",
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()));
plPgSql.chopEmptyLines();
plPgSql.indented(2, () -> {
plPgSql.writeLn(
"superRoleObjectUuid := (" + g.getSuperRoleDef().getEntityAlias().fetchSql().sql + ");\n" +
"assert superRoleObjectUuid is not null, 'superRoleObjectUuid must not be null';",
with("columns", g.getSuperRoleDef().getEntityAlias().aliasName() + ".uuid"),
with("ref", NEW.name()));
});
plPgSql.writeLn();
plPgSql.writeLn("""
if ( not hasInsertPermission(superRoleObjectUuid, '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_check_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},
@ -219,17 +150,129 @@ public class InsertTriggerGenerator {
returns trigger
language plpgsql as $$
begin
raise exception '[403] insert into ${rawSubTable} not allowed for current subjects % (%)',
currentSubjects(), currentSubjectsUuids();
raise exception '[403] insert into ${rawSubTable} not allowed regardless of current subject, no insert permissions grated at all';
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()));
plPgSql.writeLn("--//");
}
private void generateInsertPermissionChecks(final StringWriter plPgSql) {
generateInsertPermissionsCheckHeader(plPgSql);
plPgSql.indented(1, () -> {
getInsertGrants().forEach(g -> {
generateInsertPermissionChecksForSingleGrant(plPgSql, g);
});
plPgSql.chopTail(" or\n");
});
generateInsertPermissionsChecksFooter(plPgSql);
}
private void generateInsertPermissionsCheckHeader(final StringWriter plPgSql) {
plPgSql.writeLn("""
-- ============================================================================
--changeset ${rawSubTable}-rbac-CHECKING-INSERT-PERMISSION:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/**
Checks if the user respectively the assumed roles are allowed to insert a row to ${rawSubTable}.
*/
create or replace function ${rawSubTable}_insert_permission_check_tf()
returns trigger
language plpgsql as $$
declare
superObjectUuid uuid;
begin
""",
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()));
plPgSql.chopEmptyLines();
}
private void generateInsertPermissionChecksForSingleGrant(final StringWriter plPgSql, final RbacView.RbacGrantDefinition g) {
final RbacView.EntityAlias superRoleEntityAlias = g.getSuperRoleDef().getEntityAlias();
final var caseCondition = g.isConditional()
? ("NEW.type in (" + toStringList(g.getForCases()) + ") and ")
: "";
if (g.getSuperRoleDef().isGlobal(GUEST)) {
plPgSql.writeLn(
"""
-- check INSERT INSERT permission for global anyone
if ${caseCondition}true then
return NEW;
end if;
""",
with("caseCondition", caseCondition));
} else if (g.getSuperRoleDef().isGlobal(ADMIN)) {
plPgSql.writeLn(
"""
-- check INSERT INSERT if global ADMIN
if ${caseCondition}isGlobalAdmin() then
return NEW;
end if;
""",
with("caseCondition", caseCondition));
} else if (g.getSuperRoleDef().getEntityAlias().isFetchedByDirectForeignKey()) {
plPgSql.writeLn(
"""
-- check INSERT permission via direct foreign key: NEW.${refColumn}
if ${caseCondition}hasInsertPermission(NEW.${refColumn}, '${rawSubTable}') then
return NEW;
end if;
""",
with("caseCondition", caseCondition),
with("refColumn", superRoleEntityAlias.dependsOnColumName()),
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()));
} else {
plPgSql.writeLn(
"""
-- check INSERT permission via indirect foreign key: NEW.${refColumn}
superObjectUuid := (${fetchSql});
assert superObjectUuid is not null, 'object uuid fetched depending on ${rawSubTable}.${refColumn} must not be null, also check fetchSql in RBAC DSL';
if ${caseCondition}hasInsertPermission(superObjectUuid, '${rawSubTable}') then
return NEW;
end if;
""",
with("caseCondition", caseCondition),
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()),
with("refColumn", superRoleEntityAlias.dependsOnColumName()),
with("fetchSql", g.getSuperRoleDef().getEntityAlias().fetchSql().sql),
with("columns", g.getSuperRoleDef().getEntityAlias().aliasName() + ".uuid"),
with("ref", NEW.name()));
}
}
private void generateInsertPermissionsChecksFooter(final StringWriter plPgSql) {
plPgSql.writeLn();
plPgSql.writeLn("""
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
execute procedure ${rawSubTable}_insert_permission_check_tf();
--//
""",
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()));
}
private String toStringList(final Set<RbacView.CaseDef> cases) {
return cases.stream().map(c -> "'" + c.value + "'").collect(joining(", "));
}
private boolean isGrantToADifferentTable(final RbacView.RbacGrantDefinition g) {
return !rbacDef.getRootEntityAlias().getRawTableName().equals(g.getSuperRoleDef().getEntityAlias().getRawTableName());
}
private Stream<RbacView.RbacGrantDefinition> getInsertGrants() {
@ -238,6 +281,15 @@ public class InsertTriggerGenerator {
.filter(g -> g.getPermDef().toCreate && g.getPermDef().getPermission() == INSERT);
}
private boolean isInsertPermissionIsNotGrantedAtAll() {
return getInsertGrants().findAny().isEmpty();
}
private boolean isInsertPermissionGrantedToGlobalGuest() {
return getInsertGrants().anyMatch(g ->
g.getSuperRoleDef().getEntityAlias().isGlobal() && g.getSuperRoleDef().getRole() == GUEST);
}
private Optional<RbacView.RbacGrantDefinition> getOptionalInsertGrant() {
return getInsertGrants()
.reduce(singleton());
@ -252,7 +304,8 @@ public class InsertTriggerGenerator {
private static <T> BinaryOperator<T> singleton() {
return (x, y) -> {
if ( !x.equals(y) ) {
throw new IllegalStateException("only a single INSERT permission grant allowed");
return x;
// throw new IllegalStateException("only a single INSERT permission grant allowed");
}
return x;
};

View File

@ -32,10 +32,10 @@ public class RbacIdentityViewGenerator {
$idName$);
""";
case SQL_QUERY -> """
call generateRbacIdentityViewFromQuery('${rawTableName}',
$idName$
${identityViewSqlPart}
$idName$);
call generateRbacIdentityViewFromQuery('${rawTableName}',
$idName$
${identityViewSqlPart}
$idName$);
""";
default -> throw new IllegalStateException("illegal SQL part given");
},
@ -43,5 +43,6 @@ public class RbacIdentityViewGenerator {
with("rawTableName", rawTableName));
plPgSql.writeLn("--//");
plPgSql.writeLn();
}
}

View File

@ -25,6 +25,7 @@ import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.PERM_TO_ROLE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.ROLE_TO_ROLE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.Part.AUTO_FETCH;
import static org.apache.commons.collections4.SetUtils.hashSet;
@ -62,6 +63,7 @@ public class RbacView {
private SQL orderBySqlExpression;
private EntityAlias rootEntityAliasProxy;
private RbacRoleDefinition previousRoleDef;
private Set<String> limitDiagramToAliasNames;
private final Map<String, CaseDef> cases = new LinkedHashMap<>() {
@Override
public CaseDef put(final String key, final CaseDef value) {
@ -396,8 +398,7 @@ public class RbacView {
new RbacRoleDefinition(findEntityAlias(mapper.map(roleDef.entityAlias.aliasName)), roleDef.role);
});
copyOf(importedRbacView.getGrantDefs()).forEach(grantDef -> {
if ( grantDef.grantType() == RbacGrantDefinition.GrantType.ROLE_TO_ROLE &&
(grantDef.forCases == null || grantDef.matchesCase(forCase)) ) {
if ( grantDef.grantType() == ROLE_TO_ROLE && grantDef.matchesCase(forCase) ) {
final var importedGrantDef = findOrCreateGrantDef(
findRbacRole(
mapper.map(grantDef.getSubRoleDef().entityAlias.aliasName),
@ -499,6 +500,29 @@ public class RbacView {
new RbacViewPostgresGenerator(this).generateToChangeLog(Path.of(OUTPUT_BASEDIR, baseFileName + ".sql"));
}
public RbacView limitDiagramTo(final String... aliasNames) {
this.limitDiagramToAliasNames = Set.of(aliasNames);
return this;
}
public boolean renderInDiagram(final EntityAlias ea) {
return limitDiagramToAliasNames == null || limitDiagramToAliasNames.contains(ea.aliasName());
}
public boolean renderInDiagram(final RbacGrantDefinition g) {
if ( limitDiagramToAliasNames == null ) {
return true;
}
return switch (g.grantType()) {
case ROLE_TO_USER ->
renderInDiagram(g.getSubRoleDef().getEntityAlias());
case ROLE_TO_ROLE ->
renderInDiagram(g.getSuperRoleDef().getEntityAlias()) && renderInDiagram(g.getSubRoleDef().getEntityAlias());
case PERM_TO_ROLE ->
renderInDiagram(g.getSuperRoleDef().getEntityAlias()) && renderInDiagram(g.getPermDef().getEntityAlias());
};
}
public class RbacGrantBuilder {
private final RbacRoleDefinition superRoleDef;
@ -535,7 +559,7 @@ public class RbacView {
private final RbacPermissionDefinition permDef;
private boolean assumed = true;
private boolean toCreate = false;
private Set<CaseDef> forCases = new HashSet<>();
private Set<CaseDef> forCases = new LinkedHashSet<>();
@Override
public String toString() {
@ -560,11 +584,13 @@ public class RbacView {
register(this);
}
public RbacGrantDefinition(final RbacPermissionDefinition permDef, final RbacRoleDefinition roleDef) {
public RbacGrantDefinition(final RbacPermissionDefinition permDef, final RbacRoleDefinition roleDef,
final CaseDef forCase) {
this.userDef = null;
this.subRoleDef = null;
this.superRoleDef = roleDef;
this.permDef = permDef;
this.forCases = forCase != null ? hashSet(forCase) : null;
register(this);
}
@ -584,7 +610,7 @@ public class RbacView {
GrantType grantType() {
return permDef != null ? PERM_TO_ROLE
: userDef != null ? GrantType.ROLE_TO_USER
: GrantType.ROLE_TO_ROLE;
: ROLE_TO_ROLE;
}
boolean isAssumed() {
@ -602,9 +628,10 @@ public class RbacView {
}
boolean matchesCase(final ColumnValue requestedCase) {
final var noCasesDefined = forCases.isEmpty();
final var noCasesDefined = forCases == null;
final var generateForAllCases = requestedCase == null;
final boolean isGrantedForRequestedCase = forCases.stream().anyMatch(c -> c.isCase(requestedCase));
final boolean isGrantedForRequestedCase = forCases == null || forCases.stream().anyMatch(c -> c.isCase(requestedCase))
|| forCases.stream().anyMatch(CaseDef::isDefaultCase) && !allCases.stream().anyMatch(c -> c.isCase(requestedCase));
return noCasesDefined || generateForAllCases || isGrantedForRequestedCase;
}
@ -676,7 +703,8 @@ public class RbacView {
final String tableName;
final boolean toCreate;
private RbacPermissionDefinition(final EntityAlias entityAlias, final Permission permission, final String tableName, 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;
@ -788,6 +816,10 @@ public class RbacView {
public String toString() {
return "role:" + entityAlias.aliasName + role;
}
public boolean isGlobal(final Role role) {
return entityAlias.isGlobal() && this.role == role;
}
}
public RbacUserReference findUserRef(final RbacUserReference.UserRole userRole) {
@ -842,19 +874,6 @@ public class RbacView {
.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)
@ -866,7 +885,8 @@ public class RbacView {
return grantDefs.stream()
.filter(g -> g.permDef == permDef && g.superRoleDef == roleDef)
.findFirst()
.orElseGet(() -> new RbacGrantDefinition(permDef, roleDef));
.map(g -> g.forCase(processingCase))
.orElseGet(() -> new RbacGrantDefinition(permDef, roleDef, processingCase));
}
private RbacGrantDefinition findOrCreateGrantDef(

View File

@ -5,7 +5,12 @@ import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.CaseDef;
import org.apache.commons.lang3.StringUtils;
import java.nio.file.*;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.joining;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.*;
@ -15,17 +20,28 @@ public class RbacViewMermaidFlowchartGenerator {
public static final String HOSTSHARING_LIGHT_ORANGE = "#feb28c";
public static final String HOSTSHARING_DARK_BLUE = "#274d6e";
public static final String HOSTSHARING_LIGHT_BLUE = "#99bcdb";
// TODO.rbac: implement level limit for all renderable items and remove items which not part of a grant
private static final long MAX_LEVEL_TO_RENDER = 3;
private final RbacView rbacDef;
private final List<RbacView.EntityAlias> usedEntityAliases;
private final CaseDef forCase;
private final StringWriter flowchart = new StringWriter();
public RbacViewMermaidFlowchartGenerator(final RbacView rbacDef, final CaseDef forCase) {
this.rbacDef = rbacDef;
this.forCase = forCase;
usedEntityAliases = rbacDef.getGrantDefs().stream()
.flatMap(g -> Stream.of(
g.getSuperRoleDef() != null ? g.getSuperRoleDef().getEntityAlias() : null,
g.getSubRoleDef() != null ? g.getSubRoleDef().getEntityAlias() : null,
g.getPermDef() != null ? g.getPermDef().getEntityAlias() : null))
.filter(Objects::nonNull)
.sorted(comparing(RbacView.EntityAlias::aliasName))
.distinct()
.filter(rbacDef::renderInDiagram)
.collect(Collectors.toList());
flowchart.writeLn("""
%%{init:{'flowchart':{'htmlLabels':false}}}%%
flowchart TB
@ -38,13 +54,18 @@ public class RbacViewMermaidFlowchartGenerator {
this(rbacDef, null);
}
private void renderEntitySubgraphs() {
rbacDef.getEntityAliases().values().stream()
usedEntityAliases.stream()
.filter(entityAlias -> !rbacDef.isEntityAliasProxy(entityAlias))
.filter(entityAlias -> !entityAlias.isPlaceholder())
.filter(rbacDef::renderInDiagram)
.forEach(this::renderEntitySubgraph);
}
private void renderEntitySubgraph(final RbacView.EntityAlias entity) {
if (!rbacDef.renderInDiagram(entity)) {
return;
}
final var color = rbacDef.isRootEntityAlias(entity) ? HOSTSHARING_DARK_ORANGE
: entity.isSubEntity() ? HOSTSHARING_LIGHT_ORANGE
: HOSTSHARING_LIGHT_BLUE;
@ -58,8 +79,7 @@ public class RbacViewMermaidFlowchartGenerator {
.replace("%{strokeColor}", HOSTSHARING_DARK_BLUE ));
flowchart.indented( () -> {
rbacDef.getEntityAliases().values().stream()
.filter(e -> e.level() <= MAX_LEVEL_TO_RENDER)
usedEntityAliases.stream()
.filter(e -> e.aliasName().startsWith(entity.aliasName() + ":"))
.forEach(this::renderEntitySubgraph);
@ -110,9 +130,9 @@ public class RbacViewMermaidFlowchartGenerator {
private void renderGrants(final RbacView.RbacGrantDefinition.GrantType grantType, final String comment) {
final var grantsOfRequestedType = rbacDef.getGrantDefs().stream()
.filter(g -> g.level() <= MAX_LEVEL_TO_RENDER)
.filter(g -> g.grantType() == grantType)
.filter(this::isToBeRenderedInThisGraph)
.filter(rbacDef::renderInDiagram)
.filter(this::isToBeRenderedForThisCase)
.toList();
if ( !grantsOfRequestedType.isEmpty()) {
flowchart.ensureSingleEmptyLine();
@ -121,8 +141,8 @@ public class RbacViewMermaidFlowchartGenerator {
}
}
private boolean isToBeRenderedInThisGraph(final RbacView.RbacGrantDefinition g) {
if ( g.grantType() != ROLE_TO_ROLE )
private boolean isToBeRenderedForThisCase(final RbacView.RbacGrantDefinition g) {
if ( g.grantType() == ROLE_TO_USER )
return true;
if ( forCase == null && !g.isConditional() )
return true;

View File

@ -2,8 +2,6 @@ package net.hostsharing.hsadminng.rbac.rbacdef;
import org.apache.commons.lang3.StringUtils;
import java.util.regex.Pattern;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.joining;
@ -111,9 +109,11 @@ public class StringWriter {
String apply(final String textToAppend) {
text = textToAppend;
stream(varDefs).forEach(varDef -> {
final var pattern = Pattern.compile("\\$\\{" + varDef.name() + "}", Pattern.CASE_INSENSITIVE);
final var matcher = pattern.matcher(text);
text = matcher.replaceAll(varDef.value());
// TODO.impl: I actually want a case-independent search+replace but ...
// for which the substitution String can contain sequences of "${...}" to be replaced by further varDefs.
text = text.replace("${" + varDef.name() + "}", varDef.value());
text = text.replace("${" + varDef.name().toUpperCase() + "}", varDef.value());
text = text.replace("${" + varDef.name().toLowerCase() + "}", varDef.value());
});
return text;
}

View File

@ -0,0 +1,16 @@
--liquibase formatted sql
-- ============================================================================
-- RAISE-FUNCTIONS
--changeset RAISE-FUNCTIONS:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Like RAISE EXCEPTION ... just as an expression instead of a statement.
*/
create or replace function raiseException(msg text)
returns varchar
language plpgsql as $$
begin
raise exception using message = msg;
end; $$;
--//

View File

@ -569,14 +569,14 @@ select exists(
);
$$;
create or replace function hasInsertPermission(objectUuid uuid, forOp RbacOp, tableName text )
create or replace function hasInsertPermission(objectUuid uuid, tableName text )
returns BOOL
stable -- leakproof
language plpgsql as $$
declare
permissionUuid uuid;
begin
permissionUuid = findPermissionId(objectUuid, forOp, tableName);
permissionUuid = findPermissionId(objectUuid, 'INSERT'::RbacOp, tableName);
return permissionUuid is not null;
end;
$$;

View File

@ -77,66 +77,82 @@ execute procedure insertTriggerForTestCustomer_tf();
-- ============================================================================
--changeset test-customer-rbac-INSERT:1 endDelimiter:--//
--changeset test-customer-rbac-GRANTING-INSERT-PERMISSION:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
-- granting INSERT permission to global ----------------------------
/*
Creates INSERT INTO test_customer permissions for the related global rows.
Grants INSERT INTO test_customer permissions to specified role of pre-existing global rows.
*/
do language plpgsql $$
declare
row global;
begin
call defineContext('create INSERT INTO test_customer permissions for the related global rows');
call defineContext('create INSERT INTO test_customer permissions for pre-exising global rows');
FOR row IN SELECT * FROM global
-- unconditional for all rows in that table
LOOP
call grantPermissionToRole(
createPermission(row.uuid, 'INSERT', 'test_customer'),
globalADMIN());
createPermission(row.uuid, 'INSERT', 'test_customer'),
globalADMIN());
END LOOP;
END;
end;
$$;
/**
Adds test_customer INSERT permission to specified role of new global rows.
Grants test_customer INSERT permission to specified role of new global rows.
*/
create or replace function test_customer_global_insert_tf()
create or replace function new_test_customer_grants_insert_to_global_tf()
returns trigger
language plpgsql
strict as $$
begin
call grantPermissionToRole(
-- unconditional for all rows in that table
call grantPermissionToRole(
createPermission(NEW.uuid, 'INSERT', 'test_customer'),
globalADMIN());
-- end.
return NEW;
end; $$;
-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
create trigger z_test_customer_global_insert_tg
create trigger z_new_test_customer_grants_insert_to_global_tg
after insert on global
for each row
execute procedure test_customer_global_insert_tf();
execute procedure new_test_customer_grants_insert_to_global_tf();
-- ============================================================================
--changeset test_customer-rbac-CHECKING-INSERT-PERMISSION:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/**
Checks if the user or assumed roles are allowed to insert a row to test_customer,
where only global-admin has that permission.
Checks if the user respectively the assumed roles are allowed to insert a row to test_customer.
*/
create or replace function test_customer_insert_permission_missing_tf()
create or replace function test_customer_insert_permission_check_tf()
returns trigger
language plpgsql as $$
declare
superObjectUuid uuid;
begin
-- check INSERT INSERT if global ADMIN
if isGlobalAdmin() then
return NEW;
end if;
raise exception '[403] insert into test_customer not allowed for current subjects % (%)',
currentSubjects(), currentSubjectsUuids();
currentSubjects(), currentSubjectsUuids();
end; $$;
create trigger test_customer_insert_permission_check_tg
before insert on test_customer
for each row
when ( not isGlobalAdmin() )
execute procedure test_customer_insert_permission_missing_tf();
execute procedure test_customer_insert_permission_check_tf();
--//
-- ============================================================================
--changeset test-customer-rbac-IDENTITY-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
@ -147,6 +163,7 @@ call generateRbacIdentityViewFromProjection('test_customer',
$idName$);
--//
-- ============================================================================
--changeset test-customer-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------

View File

@ -6,6 +6,19 @@ This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manua
%%{init:{'flowchart':{'htmlLabels':false}}}%%
flowchart TB
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
subgraph package["`**package**`"]
direction TB
style package fill:#dd4901,stroke:#274d6e,stroke-width:8px
@ -28,19 +41,6 @@ subgraph package["`**package**`"]
end
end
subgraph customer["`**customer**`"]
direction TB
style customer fill:#99bcdb,stroke:#274d6e,stroke-width:8px
subgraph customer:roles[ ]
style customer:roles fill:#99bcdb,stroke:white
role:customer:OWNER[[customer:OWNER]]
role:customer:ADMIN[[customer:ADMIN]]
role:customer:TENANT[[customer:TENANT]]
end
end
%% granting roles to roles
role:global:ADMIN -.->|XX| role:customer:OWNER
role:customer:OWNER -.-> role:customer:ADMIN

View File

@ -142,68 +142,82 @@ execute procedure updateTriggerForTestPackage_tf();
-- ============================================================================
--changeset test-package-rbac-INSERT:1 endDelimiter:--//
--changeset test-package-rbac-GRANTING-INSERT-PERMISSION:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
-- granting INSERT permission to test_customer ----------------------------
/*
Creates INSERT INTO test_package permissions for the related test_customer rows.
Grants INSERT INTO test_package permissions to specified role of pre-existing test_customer rows.
*/
do language plpgsql $$
declare
row test_customer;
begin
call defineContext('create INSERT INTO test_package permissions for the related test_customer rows');
call defineContext('create INSERT INTO test_package permissions for pre-exising test_customer rows');
FOR row IN SELECT * FROM test_customer
-- unconditional for all rows in that table
LOOP
call grantPermissionToRole(
createPermission(row.uuid, 'INSERT', 'test_package'),
testCustomerADMIN(row));
createPermission(row.uuid, 'INSERT', 'test_package'),
testCustomerADMIN(row));
END LOOP;
END;
end;
$$;
/**
Adds test_package INSERT permission to specified role of new test_customer rows.
Grants test_package INSERT permission to specified role of new test_customer rows.
*/
create or replace function test_package_test_customer_insert_tf()
create or replace function new_test_package_grants_insert_to_test_customer_tf()
returns trigger
language plpgsql
strict as $$
begin
call grantPermissionToRole(
-- unconditional for all rows in that table
call grantPermissionToRole(
createPermission(NEW.uuid, 'INSERT', 'test_package'),
testCustomerADMIN(NEW));
-- end.
return NEW;
end; $$;
-- 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
create trigger z_new_test_package_grants_insert_to_test_customer_tg
after insert on test_customer
for each row
execute procedure test_package_test_customer_insert_tf();
execute procedure new_test_package_grants_insert_to_test_customer_tf();
-- ============================================================================
--changeset test_package-rbac-CHECKING-INSERT-PERMISSION:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/**
Checks if the user or assumed roles are allowed to insert a row to test_package,
where the check is performed by a direct role.
A direct role is a role depending on a foreign key directly available in the NEW row.
Checks if the user respectively the assumed roles are allowed to insert a row to test_package.
*/
create or replace function test_package_insert_permission_missing_tf()
create or replace function test_package_insert_permission_check_tf()
returns trigger
language plpgsql as $$
declare
superObjectUuid uuid;
begin
-- check INSERT permission via direct foreign key: NEW.customerUuid
if hasInsertPermission(NEW.customerUuid, 'test_package') then
return NEW;
end if;
raise exception '[403] insert into test_package not allowed for current subjects % (%)',
currentSubjects(), currentSubjectsUuids();
currentSubjects(), currentSubjectsUuids();
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();
execute procedure test_package_insert_permission_check_tf();
--//
-- ============================================================================
--changeset test-package-rbac-IDENTITY-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
@ -214,6 +228,7 @@ call generateRbacIdentityViewFromProjection('test_package',
$idName$);
--//
-- ============================================================================
--changeset test-package-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------

View File

@ -6,32 +6,6 @@ This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manua
%%{init:{'flowchart':{'htmlLabels':false}}}%%
flowchart TB
subgraph package.customer["`**package.customer**`"]
direction TB
style package.customer fill:#99bcdb,stroke:#274d6e,stroke-width:8px
subgraph package.customer:roles[ ]
style package.customer:roles fill:#99bcdb,stroke:white
role:package.customer:OWNER[[package.customer:OWNER]]
role:package.customer:ADMIN[[package.customer:ADMIN]]
role:package.customer:TENANT[[package.customer:TENANT]]
end
end
subgraph package["`**package**`"]
direction TB
style package fill:#99bcdb,stroke:#274d6e,stroke-width:8px
subgraph package:roles[ ]
style package:roles fill:#99bcdb,stroke:white