conditional insert condition (so far just exactly 1 unique for each table)

This commit is contained in:
Michael Hoennig 2024-04-22 16:37:25 +02:00
parent 4eda99b95a
commit 17f8448bf1
12 changed files with 97 additions and 55 deletions

View File

@ -174,7 +174,7 @@ project.tasks.processResources.dependsOn processSpring
project.tasks.compileJava.dependsOn processSpring
// Rename javax to jakarta in OpenApi generated java files because
// io.openapiprocessor.openapi-processor 2022.2 does not yet support the openapiprocessor useSpringBoot3 config option.
// io.openapiprocessor.openapi-processor 2022.5 does not yet support the openapiprocessor useSpringBoot3 config option.
// TODO.impl: Upgrade to io.openapiprocessor.openapi-processor >= 2024.2
// and use either `bean-validation: true` in api-mapping.yaml or `useSpringBoot3 true` (not sure where exactly).
task openApiGenerate(type: Copy) {

View File

@ -35,10 +35,12 @@ import java.util.Map;
import java.util.UUID;
import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR;
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.lowerInclusiveFromPostgresDateRange;
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.upperInclusiveFromPostgresDateRange;
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.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.DELETE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT;
@ -148,12 +150,12 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject {
.withRestrictedViewOrderBy(SQL.expression("validity"))
.withUpdatableColumns("version", "caption", "validity", "resources")
.importEntityAlias("debitor", HsOfficeDebitorEntity.class,
.importEntityAlias("debitor", HsOfficeDebitorEntity.class, usingCase(DEBITOR),
dependsOnColumn("debitorUuid"),
directlyFetchedByDependsOnColumn(),
NOT_NULL)
.importEntityAlias("debitorRel", HsOfficeRelationEntity.class,
.importEntityAlias("debitorRel", HsOfficeRelationEntity.class, usingCase(DEBITOR),
dependsOnColumn("debitorUuid"),
fetchedBySql("""
SELECT ${columns}

View File

@ -18,8 +18,10 @@ import java.io.IOException;
import java.time.LocalDate;
import java.util.UUID;
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR;
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.*;
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.GLOBAL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*;
@ -107,7 +109,7 @@ public class HsOfficeSepaMandateEntity implements Stringifyable, RbacObject {
.withRestrictedViewOrderBy(expression("validity"))
.withUpdatableColumns("reference", "agreement", "validity")
.importEntityAlias("debitorRel", HsOfficeRelationEntity.class,
.importEntityAlias("debitorRel", HsOfficeRelationEntity.class, usingCase(DEBITOR),
dependsOnColumn("debitorUuid"),
fetchedBySql("""
SELECT ${columns}

View File

@ -50,7 +50,7 @@ public class InsertTriggerGenerator {
begin
call defineContext('create INSERT INTO ${rawSubTableName} permissions for the related ${rawSuperTableName} rows');
FOR row IN SELECT * FROM ${rawSuperTableName}
FOR row IN SELECT * FROM ${rawSuperTableName}${typeCondition}
LOOP
call grantPermissionToRole(
createPermission(row.uuid, 'INSERT', '${rawSubTableName}'),
@ -61,7 +61,10 @@ public class InsertTriggerGenerator {
""",
with("rawSubTableName", rbacDef.getRootEntityAlias().getRawTableName()),
with("rawSuperTableName", superRoleDef.getEntityAlias().getRawTableName()),
with("rawSuperRoleDescriptor", toRoleDescriptor(superRoleDef, "row"))
with("rawSuperRoleDescriptor", toRoleDescriptor(superRoleDef, "row")),
with("typeCondition", superRoleDef.getEntityAlias().isCaseDependent()
? "\n\t\t\tWHERE type = '${case}'".replace("${case}", superRoleDef.getEntityAlias().usingCase().value)
: "")
);
});
}
@ -77,9 +80,9 @@ public class InsertTriggerGenerator {
language plpgsql
strict as $$
begin
call grantPermissionToRole(
${typeConditionIf}call grantPermissionToRole(
createPermission(NEW.uuid, 'INSERT', '${rawSubTableName}'),
${rawSuperRoleDescriptor});
${rawSuperRoleDescriptor});${typeConditionEndIf}
return NEW;
end; $$;
@ -91,7 +94,14 @@ public class InsertTriggerGenerator {
""",
with("rawSubTableName", rbacDef.getRootEntityAlias().getRawTableName()),
with("rawSuperTableName", superRoleDef.getEntityAlias().getRawTableName()),
with("rawSuperRoleDescriptor", toRoleDescriptor(superRoleDef, NEW.name()))
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;"
: "")
);
});
}
@ -241,7 +251,10 @@ public class InsertTriggerGenerator {
private static <T> BinaryOperator<T> singleton() {
return (x, y) -> {
throw new IllegalStateException("only a single INSERT permission grant allowed");
if ( !x.equals(y) ) {
throw new IllegalStateException("only a single INSERT permission grant allowed");
}
return x;
};
}

View File

@ -18,7 +18,9 @@ import java.util.function.Consumer;
import java.util.stream.Collectors;
import static java.lang.reflect.Modifier.isStatic;
import static java.util.Arrays.asList;
import static java.util.Arrays.stream;
import static java.util.Collections.max;
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;
@ -314,6 +316,15 @@ public class RbacView {
return this;
}
// TODO.impl: use importEntityAlias with all parameters
@Deprecated
public RbacView importEntityAlias(
final String aliasName, final Class<? extends RbacObject> entityClass,
final Column dependsOnColum, final SQL fetchSql, final Nullable nullable) {
importEntityAliasImpl(aliasName, entityClass, usingDefaultCase(), fetchSql, dependsOnColum, false, nullable);
return this;
}
/**
* Imports the RBAC template from the given entity class and defines an anlias name for it.
*
@ -325,6 +336,9 @@ public class RbacView {
* A JPA entity class extending RbacObject which also implements an `rbac` method returning
* its RBAC specification.
*
* @param usingCase
* Only use this case value for a switch within the rbac rules.
*
* @param fetchSql
* An SQL SELECT statement which fetches the referenced row. Use `${REF}` to speficiy the
* newly created or updated row (will be replaced by NEW/OLD from the trigger method).
@ -342,19 +356,29 @@ public class RbacView {
* a JPA entity class extending RbacObject
*/
public RbacView importEntityAlias(
final String aliasName, final Class<? extends RbacObject> entityClass,
final String aliasName, final Class<? extends RbacObject> entityClass, final ColumnValue usingCase,
final Column dependsOnColum, final SQL fetchSql, final Nullable nullable) {
importEntityAliasImpl(aliasName, entityClass, usingDefaultCase(), fetchSql, dependsOnColum, false, nullable);
importEntityAliasImpl(aliasName, entityClass, usingCase, fetchSql, dependsOnColum, false, nullable);
return this;
}
private EntityAlias importEntityAliasImpl(
final String aliasName, final Class<? extends RbacObject> entityClass, final ColumnValue forCase,
final String aliasName, final Class<? extends RbacObject> entityClass, final ColumnValue usingCase,
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);
final var entityAlias = ofNullable(entityAliases.get(aliasName))
.orElseGet(() -> {
final var ea = new EntityAlias(aliasName, entityClass, usingCase, fetchSql, dependsOnColum, asSubEntity, nullable);
entityAliases.put(aliasName, ea);
return ea;
});
try {
importAsAlias(aliasName, rbacDefinition(entityClass), forCase, asSubEntity);
// TODO.impl: this only works for directly recursive RBAC definitions, not for indirect recursion
final var rbacDef = entityClass == rootEntityAlias.entityClass
? this
: rbacDefinition(entityClass);
importAsAlias(aliasName, rbacDef, usingCase, asSubEntity);
} catch (final ReflectiveOperationException exc) {
throw new RuntimeException("cannot import entity: " + entityClass, exc);
}
@ -369,7 +393,7 @@ public class RbacView {
private RbacView importAsAlias(final String aliasName, final RbacView importedRbacView, final ColumnValue forCase, final boolean asSubEntity) {
final var mapper = new AliasNameMapper(importedRbacView, aliasName,
asSubEntity ? entityAliases.keySet() : null);
importedRbacView.getEntityAliases().values().stream()
copyOf(importedRbacView.getEntityAliases().values()).stream()
.filter(entityAlias -> !importedRbacView.isRootEntityAlias(entityAlias))
.filter(entityAlias -> !entityAlias.isGlobal())
.filter(entityAlias -> !asSubEntity || !entityAliases.containsKey(entityAlias.aliasName))
@ -377,10 +401,10 @@ public class RbacView {
final String mappedAliasName = mapper.map(entityAlias.aliasName);
entityAliases.put(mappedAliasName, new EntityAlias(mappedAliasName, entityAlias.entityClass));
});
importedRbacView.getRoleDefs().forEach(roleDef -> {
copyOf(importedRbacView.getRoleDefs()).forEach(roleDef -> {
new RbacRoleDefinition(findEntityAlias(mapper.map(roleDef.entityAlias.aliasName)), roleDef.role);
});
importedRbacView.getGrantDefs().forEach(grantDef -> {
copyOf(importedRbacView.getGrantDefs()).forEach(grantDef -> {
if ( grantDef.grantType() == RbacGrantDefinition.GrantType.ROLE_TO_ROLE &&
(grantDef.forCases == null || grantDef.matchesCase(forCase)) ) {
final var importedGrantDef = findOrCreateGrantDef(
@ -411,6 +435,10 @@ public class RbacView {
return this;
}
private static <T> List<T> copyOf(final Collection<T> eas) {
return eas.stream().toList();
}
private void verifyVersionColumnExists() {
if (stream(rootEntityAlias.entityClass.getDeclaredFields())
.noneMatch(f -> f.getAnnotation(Version.class) != null)) {
@ -615,6 +643,13 @@ public class RbacView {
return this;
}
public long level() {
return max(asList(
superRoleDef != null ? superRoleDef.entityAlias.level() : 0,
subRoleDef != null ? subRoleDef.entityAlias.level() : 0,
permDef != null ? permDef.entityAlias.level() : 0));
}
public enum GrantType {
ROLE_TO_USER,
ROLE_TO_ROLE,
@ -854,14 +889,14 @@ public class RbacView {
return distinctGrantDef;
}
record EntityAlias(String aliasName, Class<? extends RbacObject> entityClass, SQL fetchSql, Column dependsOnColum, boolean isSubEntity, Nullable nullable) {
record EntityAlias(String aliasName, Class<? extends RbacObject> entityClass, ColumnValue usingCase, SQL fetchSql, Column dependsOnColum, boolean isSubEntity, Nullable nullable) {
public EntityAlias(final String aliasName) {
this(aliasName, null, null, null, false, null);
this(aliasName, null, null, null, null, false, null);
}
public EntityAlias(final String aliasName, final Class<? extends RbacObject> entityClass) {
this(aliasName, entityClass, null, null, false, null);
this(aliasName, entityClass, null, null, null, false, null);
}
boolean isGlobal() {
@ -872,7 +907,6 @@ public class RbacView {
return entityClass == null;
}
@NotNull
@Override
public SQL fetchSql() {
if (fetchSql == null) {
@ -914,6 +948,14 @@ public class RbacView {
}
return dependsOnColum.column;
}
long level() {
return aliasName.chars().filter(ch -> ch == '.').count() + 1;
}
boolean isCaseDependent() {
return usingCase != null && usingCase.value != null;
}
}
public static String withoutRvSuffix(final String tableName) {
@ -1074,10 +1116,9 @@ public class RbacView {
return new ColumnValue(null);
}
public static ColumnValue usingCase(final String value) {
return new ColumnValue(value);
public static <E extends Enum<E>> ColumnValue usingCase(final E value) {
return new ColumnValue(value.name());
}
public final String value;
private ColumnValue(final String value) {

View File

@ -15,6 +15,9 @@ 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 CaseDef forCase;
@ -56,6 +59,7 @@ public class RbacViewMermaidFlowchartGenerator {
flowchart.indented( () -> {
rbacDef.getEntityAliases().values().stream()
.filter(e -> e.level() <= MAX_LEVEL_TO_RENDER)
.filter(e -> e.aliasName().startsWith(entity.aliasName() + ":"))
.forEach(this::renderEntitySubgraph);
@ -106,6 +110,7 @@ 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)
.toList();

View File

@ -108,16 +108,6 @@ role:debitorRel.holderPerson:ADMIN -.-> role:debitorRel.holderPerson:REFERRER
role:global:ADMIN -.-> role:debitorRel.contact:OWNER
role:debitorRel.contact:OWNER -.-> role:debitorRel.contact:ADMIN
role:debitorRel.contact:ADMIN -.-> role:debitorRel.contact:REFERRER
role:global:ADMIN -.-> role:debitorRel:OWNER
role:debitorRel:OWNER -.-> role:debitorRel:ADMIN
role:debitorRel:ADMIN -.-> role:debitorRel:AGENT
role:debitorRel:AGENT -.-> role:debitorRel:TENANT
role:debitorRel.contact:ADMIN -.-> role:debitorRel:TENANT
role:debitorRel:TENANT -.-> role:debitorRel.anchorPerson:REFERRER
role:debitorRel:TENANT -.-> role:debitorRel.holderPerson:REFERRER
role:debitorRel:TENANT -.-> role:debitorRel.contact:REFERRER
role:debitorRel.anchorPerson:ADMIN -.-> role:debitorRel:OWNER
role:debitorRel.holderPerson:ADMIN -.-> role:debitorRel:AGENT
role:global:ADMIN -.-> role:bankAccount:OWNER
role:bankAccount:OWNER -.-> role:bankAccount:ADMIN
role:bankAccount:ADMIN -.-> role:bankAccount:REFERRER

View File

@ -115,6 +115,7 @@ do language plpgsql $$
call defineContext('create INSERT INTO hs_office_sepamandate permissions for the related hs_office_relation rows');
FOR row IN SELECT * FROM hs_office_relation
WHERE type = 'DEBITOR'
LOOP
call grantPermissionToRole(
createPermission(row.uuid, 'INSERT', 'hs_office_sepamandate'),
@ -131,9 +132,11 @@ create or replace function hs_office_sepamandate_hs_office_relation_insert_tf()
language plpgsql
strict as $$
begin
call grantPermissionToRole(
if NEW.type = 'DEBITOR' then
call grantPermissionToRole(
createPermission(NEW.uuid, 'INSERT', 'hs_office_sepamandate'),
hsOfficeRelationADMIN(NEW));
end if;
return NEW;
end; $$;

View File

@ -262,16 +262,6 @@ role:debitorRel.holderPerson:ADMIN -.-> role:debitorRel.holderPerson:REFERRER
role:global:ADMIN -.-> role:debitorRel.contact:OWNER
role:debitorRel.contact:OWNER -.-> role:debitorRel.contact:ADMIN
role:debitorRel.contact:ADMIN -.-> role:debitorRel.contact:REFERRER
role:global:ADMIN -.-> role:debitorRel:OWNER
role:debitorRel:OWNER -.-> role:debitorRel:ADMIN
role:debitorRel:ADMIN -.-> role:debitorRel:AGENT
role:debitorRel:AGENT -.-> role:debitorRel:TENANT
role:debitorRel.contact:ADMIN -.-> role:debitorRel:TENANT
role:debitorRel:TENANT -.-> role:debitorRel.anchorPerson:REFERRER
role:debitorRel:TENANT -.-> role:debitorRel.holderPerson:REFERRER
role:debitorRel:TENANT -.-> role:debitorRel.contact:REFERRER
role:debitorRel.anchorPerson:ADMIN -.-> role:debitorRel:OWNER
role:debitorRel.holderPerson:ADMIN -.-> role:debitorRel:AGENT
role:debitorRel:AGENT ==> role:bookingItem:OWNER
role:bookingItem:OWNER ==> role:bookingItem:ADMIN
role:debitorRel:AGENT ==> role:bookingItem:ADMIN

View File

@ -111,7 +111,7 @@ do language plpgsql $$
call defineContext('create INSERT INTO hs_booking_item permissions for the related hs_office_relation rows');
FOR row IN SELECT * FROM hs_office_relation
WHERE type in ('DEBITOR') -- TODO.rbac: currently manually patched, needs to be generated
WHERE type = 'DEBITOR'
LOOP
call grantPermissionToRole(
createPermission(row.uuid, 'INSERT', 'hs_booking_item'),
@ -128,11 +128,11 @@ create or replace function hs_booking_item_hs_office_relation_insert_tf()
language plpgsql
strict as $$
begin
if NEW.type = 'DEBITOR' then -- TODO.rbac: currently manually patched, needs to be generated
call grantPermissionToRole(
if NEW.type = 'DEBITOR' then
call grantPermissionToRole(
createPermission(NEW.uuid, 'INSERT', 'hs_booking_item'),
hsOfficeRelationADMIN(NEW));
end if;
end if;
return NEW;
end; $$;

View File

@ -141,8 +141,6 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean
.map(s -> s.replace("hs_office_", ""))
.containsExactlyInAnyOrder(distinct(from(
initialGrantNames,
// TODO.rbac: this grant should only be created for DEBITOR-Relationships, thus the RBAC DSL needs to support conditional grants
"{ grant perm:relation#HostsharingeG-with-PARTNER-EBess:INSERT>sepamandate to role:relation#HostsharingeG-with-PARTNER-EBess:ADMIN by system and assume }",
// permissions on partner
"{ grant perm:partner#P-20032:DELETE to role:relation#HostsharingeG-with-PARTNER-EBess:OWNER by system and assume }",

View File

@ -131,8 +131,6 @@ class HsOfficeRelationRepositoryIntegrationTest extends ContextBasedTestWithClea
"hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:TENANT"));
assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(Array.fromFormatted(
initialGrantNames,
// TODO.rbac: this grant should only be created for DEBITOR-Relationships, thus the RBAC DSL needs to support conditional grants
"{ grant perm:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:INSERT>hs_office_sepamandate to role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:ADMIN by system and assume }",
"{ grant perm:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:DELETE to role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:OWNER by system and assume }",
"{ grant role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:OWNER to role:global#global:ADMIN by system and assume }",