new case for insert permission trigger generator: indirect role check (via relation)

This commit is contained in:
Michael Hoennig 2024-03-15 06:18:02 +01:00
parent 386bea0e51
commit 266cd16b52
6 changed files with 225 additions and 67 deletions

View File

@ -103,8 +103,19 @@ public class HsOfficeSepaMandateEntity implements Stringifyable, HasUuid {
.withRestrictedViewOrderBy(expression("validity"))
.withUpdatableColumns("reference", "agreement", "validity")
.importEntityAlias("debitorRel", HsOfficeRelationshipEntity.class, dependsOnColumn("debitorRelUuid"))
.importEntityAlias("bankAccount", HsOfficeBankAccountEntity.class, dependsOnColumn("bankAccountUuid"))
.importEntityAlias("debitorRel", HsOfficeRelationshipEntity.class,
dependsOnColumn("debitorUuid"),
fetchedBySql("""
SELECT debitorRel.*
FROM hs_office_relationship debitorRel
JOIN hs_office_debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid
WHERE debitor.uuid = ${REF}.debitorUuid
""")
)
.importEntityAlias("bankAccount", HsOfficeBankAccountEntity.class,
dependsOnColumn("bankAccountUuid"),
autoFetched()
)
.createRole(OWNER, (with) -> {
with.owningUser(CREATOR);
@ -116,14 +127,16 @@ public class HsOfficeSepaMandateEntity implements Stringifyable, HasUuid {
})
.createSubRole(AGENT, (with) -> {
with.outgoingSubRole("bankAccount", REFERRER);
with.outgoingSubRole("debitor", AGENT);
with.outgoingSubRole("debitorRel", AGENT);
})
.createSubRole(REFERRER, (with) -> {
with.incomingSuperRole("bankAccount", ADMIN);
with.incomingSuperRole("debitor", AGENT);
with.incomingSuperRole("debitorRel", AGENT);
with.outgoingSubRole("debitorRel", TENANT);
with.permission(SELECT);
});
})
.toRole("debitorRel", ADMIN).grantPermission("sepaMandate", INSERT);
}
public static void main(String[] args) throws IOException {

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;
@ -91,49 +92,23 @@ public class InsertTriggerGenerator {
""",
with("rawSubTableName", rbacDef.getRootEntityAlias().getRawTableName()),
with("rawSuperTableName", superRoleDef.getEntityAlias().getRawTableName()),
with("rawSuperRoleDescriptor", toRoleDescriptor(superRoleDef, PostgresTriggerReference.NEW.name()))
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 -> {
if (!g.getSuperRoleDef().getEntityAlias().isGlobal()) {
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 (rbacDef.isRootEntityAlias(g.getSuperRoleDef().getEntityAlias())) {
generateInsertPermissionTriggerAllowByDirectRole(plPgSql, g);
} else {
generateInsertPermissionTriggerAllowByIndirectRole(plPgSql, g);
}
} else {
switch (g.getSuperRoleDef().getRole()) {
case ADMIN -> {
plPgSql.writeLn(
"""
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()));
generateInsertPermissionTriggerAllowOnlyGlobalAdmin(plPgSql);
}
case GUEST -> {
// no permission check trigger generated, as anybody can insert rows into this table
@ -146,7 +121,8 @@ public class InsertTriggerGenerator {
}
},
() -> {
plPgSql.writeLn("""
plPgSql.writeLn("""
-- FIXME: Where is this case necessary?
create trigger ${rawSubTable}_insert_permission_check_tg
before insert on ${rawSubTable}
for each row
@ -159,6 +135,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)

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());

View File

@ -42,7 +42,9 @@ public class RbacGrantsDiagramService {
PERMISSIONS,
NOT_ASSUMED,
TEST_ENTITIES,
NON_TEST_ENTITIES
NON_TEST_ENTITIES;
public static final EnumSet<Include> ALL = EnumSet.allOf(Include.class);
}
@Autowired
@ -65,6 +67,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;
}
@ -116,7 +122,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,9 +133,9 @@ 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"));
@ -151,7 +157,7 @@ public class RbacGrantsDiagramService {
// }
// return "[" + table + "\n" + entity + "]";
// }
return "[" + entityId + "]";
return "[" + cleanId(entityId) + "]";
}
private static String renderEntityIdName(final Node node) {
@ -170,7 +176,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,8 +202,9 @@ 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(",", "");
}
}

View File

@ -1,6 +1,6 @@
### rbac sepaMandate
This code generated was by RbacViewMermaidFlowchartGenerator at 2024-03-11T18:29:47.084556363.
This code generated was by RbacViewMermaidFlowchartGenerator at 2024-03-15T06:12:35.337470470.
```mermaid
%%{init:{'flowchart':{'htmlLabels':false}}}%%
@ -77,6 +77,7 @@ subgraph sepaMandate["`**sepaMandate**`"]
perm:sepaMandate:DELETE{{sepaMandate:DELETE}}
perm:sepaMandate:UPDATE{{sepaMandate:UPDATE}}
perm:sepaMandate:SELECT{{sepaMandate:SELECT}}
perm:sepaMandate:INSERT{{sepaMandate:INSERT}}
end
end
@ -174,5 +175,6 @@ role:sepaMandate:referrer ==> role:debitorRel:tenant
role:sepaMandate:owner ==> perm:sepaMandate:DELETE
role:sepaMandate:admin ==> perm:sepaMandate:UPDATE
role:sepaMandate:referrer ==> perm:sepaMandate:SELECT
role:debitorRel:admin ==> perm:sepaMandate:INSERT
```

View File

@ -1,5 +1,6 @@
--liquibase formatted sql
-- This code generated was by RbacViewPostgresGenerator at 2024-03-11T18:29:47.095199204.
-- This code generated was by RbacViewPostgresGenerator at 2024-03-15T06:12:35.345630060.
-- ============================================================================
--changeset hs-office-sepamandate-rbac-OBJECT:1 endDelimiter:--//
@ -34,14 +35,23 @@ declare
begin
call enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM hs_office_bankaccount WHERE uuid = NEW.bankAccountUuid into newBankAccount;
SELECT * FROM hs_office_relationship WHERE uuid = NEW.debitorUuid into newDebitorRel;
SELECT * FROM hs_office_bankaccount WHERE uuid = NEW.bankAccountUuid INTO newBankAccount;
assert newBankAccount.uuid is not null, format('newBankAccount must not be null for NEW.bankAccountUuid = %s', NEW.bankAccountUuid);
SELECT debitorRel.*
FROM hs_office_relationship debitorRel
JOIN hs_office_debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid
WHERE debitor.uuid = NEW.debitorUuid
INTO newDebitorRel;
assert newDebitorRel.uuid is not null, format('newDebitorRel must not be null for NEW.debitorUuid = %s', NEW.debitorUuid);
perform createRoleWithGrants(
hsOfficeSepaMandateOwner(NEW),
permissions => array['DELETE'],
userUuids => array[currentUserUuid()],
incomingSuperRoles => array[globalAdmin()]
incomingSuperRoles => array[globalAdmin()],
userUuids => array[currentUserUuid()]
);
perform createRoleWithGrants(
@ -54,16 +64,16 @@ begin
hsOfficeSepaMandateAgent(NEW),
incomingSuperRoles => array[hsOfficeSepaMandateAdmin(NEW)],
outgoingSubRoles => array[
hsOfficeBankAccountReferrer(newBankAccount),
hsOfficeRelationshipAgent(newDebitorRel)]
hsOfficeRelationshipAgent(newDebitorRel),
hsOfficeBankAccountReferrer(newBankAccount)]
);
perform createRoleWithGrants(
hsOfficeSepaMandateReferrer(NEW),
permissions => array['SELECT'],
incomingSuperRoles => array[
hsOfficeRelationshipAgent(newDebitorRel),
hsOfficeBankAccountAdmin(newBankAccount),
hsOfficeRelationshipAgent(newDebitorRel),
hsOfficeSepaMandateAgent(NEW)],
outgoingSubRoles => array[hsOfficeRelationshipTenant(newDebitorRel)]
);
@ -88,13 +98,52 @@ create trigger insertTriggerForHsOfficeSepaMandate_tg
after insert on hs_office_sepamandate
for each row
execute procedure insertTriggerForHsOfficeSepaMandate_tf();
--//
-- ============================================================================
--changeset hs-office-sepamandate-rbac-INSERT:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Creates INSERT INTO hs_office_sepamandate permissions for the related hs_office_relationship rows.
*/
do language plpgsql $$
declare
row hs_office_relationship;
permissionUuid uuid;
roleUuid uuid;
begin
call defineContext('create INSERT INTO hs_office_sepamandate permissions for the related hs_office_relationship rows');
FOR row IN SELECT * FROM hs_office_relationship
LOOP
roleUuid := findRoleId(hsOfficeRelationshipAdmin(row));
permissionUuid := createPermission(row.uuid, 'INSERT', 'hs_office_sepamandate');
call grantPermissionToRole(permissionUuid, roleUuid);
END LOOP;
END;
$$;
/**
Adds hs_office_sepamandate INSERT permission to specified role of new hs_office_relationship rows.
*/
create or replace function hs_office_sepamandate_hs_office_relationship_insert_tf()
returns trigger
language plpgsql
strict as $$
begin
call grantPermissionToRole(
hsOfficeRelationshipAdmin(NEW),
createPermission(NEW.uuid, 'INSERT', 'hs_office_sepamandate'));
return NEW;
end; $$;
create trigger hs_office_sepamandate_hs_office_relationship_insert_tg
after insert on hs_office_relationship
for each row
execute procedure hs_office_sepamandate_hs_office_relationship_insert_tf();
/**
Checks if the user or assumed roles are allowed to insert a row to hs_office_sepamandate.
*/
@ -102,19 +151,29 @@ create or replace function hs_office_sepamandate_insert_permission_missing_tf()
returns trigger
language plpgsql as $$
begin
raise exception '[403] insert into hs_office_sepamandate not allowed for current subjects % (%)',
currentSubjects(), currentSubjectsUuids();
if ( not hasInsertPermission(
( SELECT debitorRel.uuid FROM
(SELECT debitorRel.*
FROM hs_office_relationship debitorRel
JOIN hs_office_debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid
WHERE debitor.uuid = NEW.debitorUuid
) AS debitorRel
), 'INSERT', 'hs_office_sepamandate') ) then
raise exception
'[403] insert into hs_office_sepamandate not allowed for current subjects % (%)',
currentSubjects(), currentSubjectsUuids();
end if;
return NEW;
end; $$;
create trigger hs_office_sepamandate_insert_permission_check_tg
before insert on hs_office_sepamandate
for each row
-- As there is no explicit INSERT grant specified for this table,
-- only global admins are allowed to insert any rows.
when ( not isGlobalAdmin() )
execute procedure hs_office_sepamandate_insert_permission_missing_tf();
--//
-- ============================================================================
--changeset hs-office-sepamandate-rbac-IDENTITY-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
@ -125,18 +184,19 @@ create trigger hs_office_sepamandate_insert_permission_check_tg
join hs_office_bankaccount ba on ba.uuid = sm.bankAccountUuid
$idName$);
--//
-- ============================================================================
--changeset hs-office-sepamandate-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
call generateRbacRestrictedView('hs_office_sepamandate',
'validity',
$orderBy$
validity
$orderBy$,
$updates$
reference = new.reference,
reference = new.reference,
agreement = new.agreement,
validity = new.validity
$updates$);
--//