RBAC generator with conditional grants used for REPRESENTATIVE-Relation #33

Merged
hsh-michaelhoennig merged 31 commits from rbac-generator-with-conditional-grants into master 2024-04-08 11:16:07 +02:00
10 changed files with 463 additions and 114 deletions
Showing only changes of commit 001ab652c7 - Show all commits

View File

@ -15,6 +15,8 @@ import jakarta.persistence.Column;
import java.io.IOException; import java.io.IOException;
import java.util.UUID; import java.util.UUID;
import static net.hostsharing.hsadminng.rbac.rbacdef.ConditionGenerator.CaseDef.inCaseOf;
import static net.hostsharing.hsadminng.rbac.rbacdef.ConditionGenerator.CaseDef.inOtherCases;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.*; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL;
@ -110,11 +112,11 @@ public class HsOfficeRelationEntity implements RbacObject, Stringifyable {
with.permission(DELETE); with.permission(DELETE);
}) })
.createSubRole(ADMIN, (with) -> { .createSubRole(ADMIN, (with) -> {
with.incomingSuperRole("anchorPerson", ADMIN);
with.outgoingSubRole("anchorPerson", OWNER); with.outgoingSubRole("anchorPerson", OWNER);
with.permission(UPDATE); with.permission(UPDATE);
}) })
.createSubRole(AGENT, (with) -> { .createSubRole(AGENT, (with) -> {
with.incomingSuperRole("anchorPerson", ADMIN);
}) })
.createSubRole(TENANT, (with) -> { .createSubRole(TENANT, (with) -> {
with.incomingSuperRole("holderPerson", ADMIN); with.incomingSuperRole("holderPerson", ADMIN);
@ -125,7 +127,7 @@ public class HsOfficeRelationEntity implements RbacObject, Stringifyable {
with.permission(SELECT); with.permission(SELECT);
}); });
}), }),
//FIXME: .inCaseOf("DEBITOR") inCaseOf("DEBITOR", then -> {}),
inOtherCases(then -> { inOtherCases(then -> {
then.createRole(OWNER, (with) -> { then.createRole(OWNER, (with) -> {
with.owningUser(CREATOR); with.owningUser(CREATOR);

View File

@ -0,0 +1,75 @@
package net.hostsharing.hsadminng.rbac.rbacdef;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static java.lang.String.join;
import static java.util.Optional.ofNullable;
public class ConditionGenerator {
private final String discriminatorColumName;
private final Set<CaseDef> allCases;
public ConditionGenerator(final String discriminatorColumName, final Set<CaseDef> allCases) {
this.discriminatorColumName = discriminatorColumName;
this.allCases = allCases;
}
public String generatePlPgSql(final Set<CaseDef> forCases) {
if (forCases.size() == 1) {
if (forCases.iterator().next().isDefaultCase()) {
final var nonDefaultCases = allCases.stream().filter(c -> !c.isDefaultCase()).map(c -> c.val).toList();
if (nonDefaultCases.size() > 1) {
return discriminatorColumName + " not in ('" + join("', '", nonDefaultCases) + "')";
}
return discriminatorColumName + " <> '" + nonDefaultCases.getFirst() + "'";
}
return discriminatorColumName + " = '" + forCases.iterator().next().val + "'";
}
return discriminatorColumName + " in ('" + forCases.stream().map(c -> c.val).collect(Collectors.joining("', '")) + "')";
}
public static class CaseDef {
final String val;
final Consumer<RbacView> def;
private CaseDef(final String discriminatorColumnValue, final Consumer<RbacView> def) {
this.val = discriminatorColumnValue;
this.def = def;
}
public static CaseDef inCaseOf(final String discriminatorColumnValue, final Consumer<RbacView> def) {
return new CaseDef(discriminatorColumnValue, def);
}
public static CaseDef inOtherCases(final Consumer<RbacView> def) {
return new CaseDef(null, def);
}
@Override
public int hashCode() {
return ofNullable(val).map(String::hashCode).orElse(0);
}
@Override
public boolean equals(final Object other) {
if (this == other)
return true;
if (other == null || getClass() != other.getClass())
return false;
final CaseDef caseDef = (CaseDef) other;
return Objects.equals(val, caseDef.val);
}
boolean isDefaultCase() {
return val == null;
}
}
}

View File

@ -13,6 +13,7 @@ import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity;
import net.hostsharing.hsadminng.hs.office.sepamandate.HsOfficeSepaMandateEntity; import net.hostsharing.hsadminng.hs.office.sepamandate.HsOfficeSepaMandateEntity;
import net.hostsharing.hsadminng.rbac.rbacdef.ConditionGenerator.CaseDef;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
import net.hostsharing.hsadminng.test.cust.TestCustomerEntity; import net.hostsharing.hsadminng.test.cust.TestCustomerEntity;
import net.hostsharing.hsadminng.test.dom.TestDomainEntity; import net.hostsharing.hsadminng.test.dom.TestDomainEntity;
@ -31,9 +32,12 @@ import java.util.stream.Stream;
import static java.lang.reflect.Modifier.isStatic; import static java.lang.reflect.Modifier.isStatic;
import static java.util.Arrays.stream; import static java.util.Arrays.stream;
import static java.util.Optional.ofNullable; 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.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.PERM_TO_ROLE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR; 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 net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.Part.AUTO_FETCH;
import static org.apache.commons.collections4.SetUtils.hashSet;
import static org.apache.commons.lang3.StringUtils.uncapitalize; import static org.apache.commons.lang3.StringUtils.uncapitalize;
@Getter @Getter
@ -60,14 +64,17 @@ public class RbacView {
}; };
private final Set<String> updatableColumns = new LinkedHashSet<>(); private final Set<String> updatableColumns = new LinkedHashSet<>();
private final Set<RbacGrantDefinition> grantDefs = new LinkedHashSet<>(); private final Set<RbacGrantDefinition> grantDefs = new LinkedHashSet<>();
private final Set<CaseDef> allCases = new LinkedHashSet<>();
private String discriminatorColumName;
private CaseDef processingCase;
private SQL identityViewSqlQuery; private SQL identityViewSqlQuery;
private SQL orderBySqlExpression; private SQL orderBySqlExpression;
private EntityAlias rootEntityAliasProxy; private EntityAlias rootEntityAliasProxy;
private RbacRoleDefinition previousRoleDef; private RbacRoleDefinition previousRoleDef;
private final Map<String, String> cases = new HashMap<>() { private final Map<String, CaseDef> cases = new LinkedHashMap<>() {
@Override @Override
public String put(final String key, final String value) { public CaseDef put(final String key, final CaseDef value) {
if (containsKey(key)) { if (containsKey(key)) {
throw new IllegalArgumentException("duplicate case: " + key); throw new IllegalArgumentException("duplicate case: " + key);
} }
@ -247,7 +254,11 @@ public class RbacView {
} }
private RbacPermissionDefinition createPermission(final EntityAlias entityAlias, final Permission permission) { private RbacPermissionDefinition createPermission(final EntityAlias entityAlias, final Permission permission) {
return new RbacPermissionDefinition(entityAlias, permission, null, true); return permDefs.stream()
.filter(p -> p.permission == permission && p.entityAlias == entityAlias)
.findFirst()
// .map(g -> g.forCase(processingCase)) TODO: not implemented case dependent
.orElseGet(() -> new RbacPermissionDefinition(entityAlias, permission, null, true));
} }
public <EC extends RbacObject> RbacView declarePlaceholderEntityAliases(final String... aliasNames) { public <EC extends RbacObject> RbacView declarePlaceholderEntityAliases(final String... aliasNames) {
@ -292,7 +303,7 @@ public class RbacView {
if (rootEntityAliasProxy != null) { if (rootEntityAliasProxy != null) {
throw new IllegalStateException("there is already an entityAliasProxy: " + rootEntityAliasProxy); throw new IllegalStateException("there is already an entityAliasProxy: " + rootEntityAliasProxy);
} }
rootEntityAliasProxy = importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, false, NOT_NULL); rootEntityAliasProxy = importEntityAliasImpl(aliasName, entityClass, forCase, fetchSql, dependsOnColum, false, NOT_NULL);
return this; return this;
} }
@ -311,7 +322,7 @@ public class RbacView {
public RbacView importSubEntityAlias( public RbacView importSubEntityAlias(
final String aliasName, final Class<? extends RbacObject> entityClass, final String aliasName, final Class<? extends RbacObject> entityClass,
final SQL fetchSql, final Column dependsOnColum) { final SQL fetchSql, final Column dependsOnColum) {
importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, true, NOT_NULL); importEntityAliasImpl(aliasName, entityClass, usingDefaultCase(), fetchSql, dependsOnColum, true, NOT_NULL);
return this; return this;
} }
@ -345,17 +356,17 @@ public class RbacView {
public RbacView importEntityAlias( public RbacView importEntityAlias(
final String aliasName, final Class<? extends RbacObject> entityClass, final String aliasName, final Class<? extends RbacObject> entityClass,
final Column dependsOnColum, final SQL fetchSql, final Nullable nullable) { final Column dependsOnColum, final SQL fetchSql, final Nullable nullable) {
importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, false, nullable); importEntityAliasImpl(aliasName, entityClass, usingDefaultCase(), fetchSql, dependsOnColum, false, nullable);
return this; return this;
} }
private EntityAlias importEntityAliasImpl( private EntityAlias importEntityAliasImpl(
final String aliasName, final Class<? extends RbacObject> entityClass, final String aliasName, final Class<? extends RbacObject> entityClass, final ColumnValue forCase,
final SQL fetchSql, final Column dependsOnColum, boolean asSubEntity, final Nullable nullable) { final SQL fetchSql, final Column dependsOnColum, boolean asSubEntity, final Nullable nullable) {
final var entityAlias = new EntityAlias(aliasName, entityClass, fetchSql, dependsOnColum, asSubEntity, nullable); final var entityAlias = new EntityAlias(aliasName, entityClass, fetchSql, dependsOnColum, asSubEntity, nullable);
entityAliases.put(aliasName, entityAlias); entityAliases.put(aliasName, entityAlias);
try { try {
importAsAlias(aliasName, rbacDefinition(entityClass), asSubEntity); importAsAlias(aliasName, rbacDefinition(entityClass), forCase, asSubEntity);
} catch (final ReflectiveOperationException exc) { } catch (final ReflectiveOperationException exc) {
throw new RuntimeException("cannot import entity: " + entityClass, exc); throw new RuntimeException("cannot import entity: " + entityClass, exc);
} }
@ -367,7 +378,7 @@ public class RbacView {
return (RbacView) entityClass.getMethod("rbac").invoke(null); return (RbacView) entityClass.getMethod("rbac").invoke(null);
} }
private RbacView importAsAlias(final String aliasName, final RbacView importedRbacView, final boolean asSubEntity) { private RbacView importAsAlias(final String aliasName, final RbacView importedRbacView, final ColumnValue forCase, final boolean asSubEntity) {
final var mapper = new AliasNameMapper(importedRbacView, aliasName, final var mapper = new AliasNameMapper(importedRbacView, aliasName,
asSubEntity ? entityAliases.keySet() : null); asSubEntity ? entityAliases.keySet() : null);
importedRbacView.getEntityAliases().values().stream() importedRbacView.getEntityAliases().values().stream()
@ -382,7 +393,8 @@ public class RbacView {
new RbacRoleDefinition(findEntityAlias(mapper.map(roleDef.entityAlias.aliasName)), roleDef.role); new RbacRoleDefinition(findEntityAlias(mapper.map(roleDef.entityAlias.aliasName)), roleDef.role);
}); });
importedRbacView.getGrantDefs().forEach(grantDef -> { importedRbacView.getGrantDefs().forEach(grantDef -> {
if (grantDef.grantType() == RbacGrantDefinition.GrantType.ROLE_TO_ROLE) { if ( grantDef.matchesCase(forCase) &&
grantDef.grantType() == RbacGrantDefinition.GrantType.ROLE_TO_ROLE) {
final var importedGrantDef = findOrCreateGrantDef( final var importedGrantDef = findOrCreateGrantDef(
findRbacRole( findRbacRole(
mapper.map(grantDef.getSubRoleDef().entityAlias.aliasName), mapper.map(grantDef.getSubRoleDef().entityAlias.aliasName),
@ -400,8 +412,15 @@ public class RbacView {
} }
public RbacView switchOnColumn(final String discriminatorColumName, final CaseDef... caseDefs) { public RbacView switchOnColumn(final String discriminatorColumName, final CaseDef... caseDefs) {
this.discriminatorColumName = discriminatorColumName;
allCases.addAll(stream(caseDefs).toList());
// FIXME: currently only the default case is executed // FIXME: currently only the default case is executed
stream(caseDefs).filter(caseDef -> caseDef.val == null).findAny().orElseThrow().def.accept(this); stream(caseDefs).forEach(caseDef -> {
this.processingCase = caseDef;
caseDef.def.accept(this);
this.processingCase = null;
});
return this; return this;
} }
@ -463,6 +482,12 @@ public class RbacView {
} }
public void generateWithBaseFileName(final String baseFileName) { public void generateWithBaseFileName(final String baseFileName) {
if (allCases.size() > 1) {
allCases.forEach(c -> {
final var fileName = baseFileName + (c.isDefaultCase() ? "" : "-" + c.val) + ".md";
new RbacViewMermaidFlowchartGenerator(this, c).generateToMarkdownFile(Path.of(OUTPUT_BASEDIR, fileName));
});
}
new RbacViewMermaidFlowchartGenerator(this).generateToMarkdownFile(Path.of(OUTPUT_BASEDIR, baseFileName + ".md")); new RbacViewMermaidFlowchartGenerator(this).generateToMarkdownFile(Path.of(OUTPUT_BASEDIR, baseFileName + ".md"));
new RbacViewPostgresGenerator(this).generateToChangeLog(Path.of(OUTPUT_BASEDIR, baseFileName + ".sql")); new RbacViewPostgresGenerator(this).generateToChangeLog(Path.of(OUTPUT_BASEDIR, baseFileName + ".sql"));
} }
@ -488,25 +513,6 @@ public class RbacView {
} }
public static class CaseDef {
private final String val;
private final Consumer<RbacView> def;
public CaseDef(final String discriminatorColumnValue, final Consumer<RbacView> def) {
this.val = discriminatorColumnValue;
this.def = def;
}
}
public static CaseDef inCaseOf(final String discriminatorColumnValue, final Consumer<RbacView> def) {
return new CaseDef(discriminatorColumnValue, def);
}
public static CaseDef inOtherCases(final Consumer<RbacView> def) {
return new CaseDef(null, def);
}
public enum Nullable { public enum Nullable {
NOT_NULL, // DEFAULT NOT_NULL, // DEFAULT
NULLABLE NULLABLE
@ -522,8 +528,7 @@ public class RbacView {
private final RbacPermissionDefinition permDef; private final RbacPermissionDefinition permDef;
private boolean assumed = true; private boolean assumed = true;
private boolean toCreate = false; private boolean toCreate = false;
private String onlyInCaseOf; private Set<CaseDef> forCases = new HashSet<>();
private String exceptInCaseOf;
@Override @Override
public String toString() { public String toString() {
@ -537,11 +542,12 @@ public class RbacView {
}; };
} }
RbacGrantDefinition(final RbacRoleDefinition subRoleDef, final RbacRoleDefinition superRoleDef) { RbacGrantDefinition(final RbacRoleDefinition subRoleDef, final RbacRoleDefinition superRoleDef, final CaseDef forCase) {
this.userDef = null; this.userDef = null;
this.subRoleDef = subRoleDef; this.subRoleDef = subRoleDef;
this.superRoleDef = superRoleDef; this.superRoleDef = superRoleDef;
this.permDef = null; this.permDef = null;
this.forCases = hashSet(forCase);
register(this); register(this);
} }
@ -567,7 +573,7 @@ public class RbacView {
@NotNull @NotNull
GrantType grantType() { GrantType grantType() {
return permDef != null ? GrantType.PERM_TO_ROLE return permDef != null ? PERM_TO_ROLE
: userDef != null ? GrantType.ROLE_TO_USER : userDef != null ? GrantType.ROLE_TO_USER
: GrantType.ROLE_TO_ROLE; : GrantType.ROLE_TO_ROLE;
} }
@ -576,8 +582,27 @@ public class RbacView {
return assumed; return assumed;
} }
RbacGrantDefinition forCase(final CaseDef processingCase) {
forCases.add(processingCase);
return this;
}
boolean isConditional() { boolean isConditional() {
return onlyInCaseOf != null; return !forCases.isEmpty() && forCases.size()<allCases.size();
}
String generateCondition() {
return new ConditionGenerator(discriminatorColumName, allCases).generatePlPgSql(forCases);
}
boolean matchesCase(final ColumnValue value) {
return forCases.isEmpty() || forCases.contains(value.value);
}
boolean matchesCase(final CaseDef forCase) {
return forCases.isEmpty() || forCase == null || (forCases.size() == 1 && forCases.iterator().next() == null)
|| forCases.contains(forCase);
} }
boolean isToCreate() { boolean isToCreate() {
@ -606,17 +631,6 @@ public class RbacView {
return this; return this;
} }
public RbacGrantDefinition onlyInCaseOf(final String caseName) {
this.onlyInCaseOf = caseName;
return this;
}
public RbacGrantDefinition exceptInCaseOf(final String caseName) {
this.exceptInCaseOf = caseName;
return this;
}
public enum GrantType { public enum GrantType {
ROLE_TO_USER, ROLE_TO_USER,
ROLE_TO_ROLE, ROLE_TO_ROLE,
@ -840,7 +854,7 @@ public class RbacView {
private RbacGrantDefinition findOrCreateGrantDef(final RbacPermissionDefinition permDef, final RbacRoleDefinition roleDef) { private RbacGrantDefinition findOrCreateGrantDef(final RbacPermissionDefinition permDef, final RbacRoleDefinition roleDef) {
return grantDefs.stream() return grantDefs.stream()
.filter(g -> g.permDef == permDef && g.subRoleDef == roleDef) .filter(g -> g.permDef == permDef && g.superRoleDef == roleDef)
.findFirst() .findFirst()
.orElseGet(() -> new RbacGrantDefinition(permDef, roleDef)); .orElseGet(() -> new RbacGrantDefinition(permDef, roleDef));
} }
@ -851,7 +865,8 @@ public class RbacView {
return grantDefs.stream() return grantDefs.stream()
.filter(g -> g.subRoleDef == subRoleDefinition && g.superRoleDef == superRoleDefinition) .filter(g -> g.subRoleDef == subRoleDefinition && g.superRoleDef == superRoleDefinition)
.findFirst() .findFirst()
.orElseGet(() -> new RbacGrantDefinition(subRoleDefinition, superRoleDefinition)); .map(g -> g.forCase(processingCase))
.orElseGet(() -> new RbacGrantDefinition(subRoleDefinition, superRoleDefinition, processingCase));
} }
record EntityAlias(String aliasName, Class<? extends RbacObject> entityClass, SQL fetchSql, Column dependsOnColum, boolean isSubEntity, Nullable nullable) { record EntityAlias(String aliasName, Class<? extends RbacObject> entityClass, SQL fetchSql, Column dependsOnColum, boolean isSubEntity, Nullable nullable) {

View File

@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.rbac.rbacdef; package net.hostsharing.hsadminng.rbac.rbacdef;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import net.hostsharing.hsadminng.rbac.rbacdef.ConditionGenerator.CaseDef;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import java.nio.file.*; import java.nio.file.*;
@ -15,10 +16,13 @@ public class RbacViewMermaidFlowchartGenerator {
public static final String HOSTSHARING_DARK_BLUE = "#274d6e"; public static final String HOSTSHARING_DARK_BLUE = "#274d6e";
public static final String HOSTSHARING_LIGHT_BLUE = "#99bcdb"; public static final String HOSTSHARING_LIGHT_BLUE = "#99bcdb";
private final RbacView rbacDef; private final RbacView rbacDef;
private final CaseDef forCase;
private final StringWriter flowchart = new StringWriter(); private final StringWriter flowchart = new StringWriter();
public RbacViewMermaidFlowchartGenerator(final RbacView rbacDef) { public RbacViewMermaidFlowchartGenerator(final RbacView rbacDef, final CaseDef forCase) {
this.rbacDef = rbacDef; this.rbacDef = rbacDef;
this.forCase = forCase;
flowchart.writeLn(""" flowchart.writeLn("""
%%{init:{'flowchart':{'htmlLabels':false}}}%% %%{init:{'flowchart':{'htmlLabels':false}}}%%
flowchart TB flowchart TB
@ -26,6 +30,10 @@ public class RbacViewMermaidFlowchartGenerator {
renderEntitySubgraphs(); renderEntitySubgraphs();
renderGrants(); renderGrants();
} }
public RbacViewMermaidFlowchartGenerator(final RbacView rbacDef) {
this(rbacDef, null);
}
private void renderEntitySubgraphs() { private void renderEntitySubgraphs() {
rbacDef.getEntityAliases().values().stream() rbacDef.getEntityAliases().values().stream()
.filter(entityAlias -> !rbacDef.isEntityAliasProxy(entityAlias)) .filter(entityAlias -> !rbacDef.isEntityAliasProxy(entityAlias))
@ -99,6 +107,7 @@ public class RbacViewMermaidFlowchartGenerator {
private void renderGrants(final RbacView.RbacGrantDefinition.GrantType grantType, final String comment) { private void renderGrants(final RbacView.RbacGrantDefinition.GrantType grantType, final String comment) {
final var grantsOfRequestedType = rbacDef.getGrantDefs().stream() final var grantsOfRequestedType = rbacDef.getGrantDefs().stream()
.filter(g -> g.grantType() == grantType) .filter(g -> g.grantType() == grantType)
.filter(g -> g.matchesCase(forCase))
.toList(); .toList();
if ( !grantsOfRequestedType.isEmpty()) { if ( !grantsOfRequestedType.isEmpty()) {
flowchart.ensureSingleEmptyLine(); flowchart.ensureSingleEmptyLine();
@ -109,9 +118,7 @@ public class RbacViewMermaidFlowchartGenerator {
private String grantDef(final RbacView.RbacGrantDefinition grant) { private String grantDef(final RbacView.RbacGrantDefinition grant) {
final var arrow = (grant.isToCreate() ? " ==>" : " -.->") final var arrow = (grant.isToCreate() ? " ==>" : " -.->")
+ (grant.isConditional() + (grant.isAssumed() ? " " : "|XX| ");
? (grant.isAssumed() ? " |??| " : "|?XX?| ")
: (grant.isAssumed() ? " " : "|XX| "));
return switch (grant.grantType()) { return switch (grant.grantType()) {
case ROLE_TO_USER -> case ROLE_TO_USER ->
// TODO: other user types not implemented yet // TODO: other user types not implemented yet

View File

@ -1,11 +1,13 @@
package net.hostsharing.hsadminng.rbac.rbacdef; package net.hostsharing.hsadminng.rbac.rbacdef;
import net.hostsharing.hsadminng.rbac.rbacdef.ConditionGenerator.CaseDef;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacPermissionDefinition; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacPermissionDefinition;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream; import java.util.stream.Stream;
import static java.util.Optional.ofNullable; import static java.util.Optional.ofNullable;
@ -189,7 +191,25 @@ class RolesGrantsAndPermissionsGenerator {
createRolesWithGrantsSql(plPgSql, REFERRER); createRolesWithGrantsSql(plPgSql, REFERRER);
generateGrants(plPgSql, ROLE_TO_USER); generateGrants(plPgSql, ROLE_TO_USER);
generateGrants(plPgSql, ROLE_TO_ROLE); generateGrants(plPgSql, ROLE_TO_ROLE);
if (!rbacDef.getAllCases().isEmpty()) {
plPgSql.writeLn();
final var ifOrElsIf = new AtomicReference<>("IF ");
rbacDef.getAllCases().forEach(caseDef -> {
if (caseDef.val != null) {
plPgSql.writeLn(ifOrElsIf + rbacDef.getDiscriminatorColumName() + " = '" + caseDef.val + "' THEN");
} else {
plPgSql.writeLn("ELSE");
}
plPgSql.indented(() -> {
generateGrants(plPgSql, ROLE_TO_ROLE, caseDef);
});
ifOrElsIf.set("ELSIF ");
});
plPgSql.writeLn("END IF;");
}
generateGrants(plPgSql, PERM_TO_ROLE); generateGrants(plPgSql, PERM_TO_ROLE);
} }
@ -267,9 +287,19 @@ class RolesGrantsAndPermissionsGenerator {
return isInsertPermissionGrant; return isInsertPermissionGrant;
} }
private void generateGrants(final StringWriter plPgSql, final RbacGrantDefinition.GrantType grantType, final CaseDef caseDef) {
rbacGrants.stream()
.filter(g -> g.matchesCase(caseDef))
.filter(g -> g.grantType() == grantType)
.map(this::generateGrant)
.sorted()
.forEach(text -> plPgSql.writeLn(text, with("ref", NEW.name())));
}
private void generateGrants(final StringWriter plPgSql, final RbacGrantDefinition.GrantType grantType) { private void generateGrants(final StringWriter plPgSql, final RbacGrantDefinition.GrantType grantType) {
plPgSql.ensureSingleEmptyLine(); plPgSql.ensureSingleEmptyLine();
rbacGrants.stream() rbacGrants.stream()
.filter(g -> !g.isConditional())
.filter(g -> g.grantType() == grantType) .filter(g -> g.grantType() == grantType)
.map(this::generateGrant) .map(this::generateGrant)
.sorted() .sorted()
@ -302,7 +332,7 @@ class RolesGrantsAndPermissionsGenerator {
.replace("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef())); .replace("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef()));
}; };
// if (grantDef.isConditional()) { // if (grantDef.isConditional()) {
// return "if " + grantDef.getOnlyInCaseOf() + " then\n" // return "if " + grantDef.generateCondition() + " then\n"
// + " " + grantSql + "\n" // + " " + grantSql + "\n"
// + "end if;"; // + "end if;";
// } // }

View File

@ -0,0 +1,91 @@
### rbac relation
This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually.
```mermaid
%%{init:{'flowchart':{'htmlLabels':false}}}%%
flowchart TB
subgraph holderPerson["`**holderPerson**`"]
direction TB
style holderPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px
subgraph holderPerson:roles[ ]
style holderPerson:roles fill:#99bcdb,stroke:white
role:holderPerson:OWNER[[holderPerson:OWNER]]
role:holderPerson:ADMIN[[holderPerson:ADMIN]]
role:holderPerson:REFERRER[[holderPerson:REFERRER]]
end
end
subgraph anchorPerson["`**anchorPerson**`"]
direction TB
style anchorPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px
subgraph anchorPerson:roles[ ]
style anchorPerson:roles fill:#99bcdb,stroke:white
role:anchorPerson:OWNER[[anchorPerson:OWNER]]
role:anchorPerson:ADMIN[[anchorPerson:ADMIN]]
role:anchorPerson:REFERRER[[anchorPerson:REFERRER]]
end
end
subgraph contact["`**contact**`"]
direction TB
style contact fill:#99bcdb,stroke:#274d6e,stroke-width:8px
subgraph contact:roles[ ]
style contact:roles fill:#99bcdb,stroke:white
role:contact:OWNER[[contact:OWNER]]
role:contact:ADMIN[[contact:ADMIN]]
role:contact:REFERRER[[contact:REFERRER]]
end
end
subgraph relation["`**relation**`"]
direction TB
style relation fill:#dd4901,stroke:#274d6e,stroke-width:8px
subgraph relation:roles[ ]
style relation:roles fill:#dd4901,stroke:white
role:relation:OWNER[[relation:OWNER]]
role:relation:ADMIN[[relation:ADMIN]]
role:relation:AGENT[[relation:AGENT]]
role:relation:TENANT[[relation:TENANT]]
end
subgraph relation:permissions[ ]
style relation:permissions fill:#dd4901,stroke:white
perm:relation:DELETE{{relation:DELETE}}
perm:relation:UPDATE{{relation:UPDATE}}
perm:relation:SELECT{{relation:SELECT}}
perm:relation:INSERT{{relation:INSERT}}
end
end
%% granting roles to users
user:creator ==> role:relation:OWNER
%% granting roles to roles
role:global:ADMIN -.-> role:anchorPerson:OWNER
role:anchorPerson:OWNER -.-> role:anchorPerson:ADMIN
role:anchorPerson:ADMIN -.-> role:anchorPerson:REFERRER
role:global:ADMIN -.-> role:holderPerson:OWNER
role:holderPerson:OWNER -.-> role:holderPerson:ADMIN
role:holderPerson:ADMIN -.-> role:holderPerson:REFERRER
role:global:ADMIN -.-> role:contact:OWNER
role:contact:OWNER -.-> role:contact:ADMIN
role:contact:ADMIN -.-> role:contact:REFERRER
%% granting permissions to roles
role:relation:OWNER ==> perm:relation:DELETE
role:relation:ADMIN ==> perm:relation:UPDATE
role:relation:TENANT ==> perm:relation:SELECT
role:anchorPerson:ADMIN ==> perm:relation:INSERT
```

View File

@ -0,0 +1,103 @@
### rbac relation
This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually.
```mermaid
%%{init:{'flowchart':{'htmlLabels':false}}}%%
flowchart TB
subgraph holderPerson["`**holderPerson**`"]
direction TB
style holderPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px
subgraph holderPerson:roles[ ]
style holderPerson:roles fill:#99bcdb,stroke:white
role:holderPerson:OWNER[[holderPerson:OWNER]]
role:holderPerson:ADMIN[[holderPerson:ADMIN]]
role:holderPerson:REFERRER[[holderPerson:REFERRER]]
end
end
subgraph anchorPerson["`**anchorPerson**`"]
direction TB
style anchorPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px
subgraph anchorPerson:roles[ ]
style anchorPerson:roles fill:#99bcdb,stroke:white
role:anchorPerson:OWNER[[anchorPerson:OWNER]]
role:anchorPerson:ADMIN[[anchorPerson:ADMIN]]
role:anchorPerson:REFERRER[[anchorPerson:REFERRER]]
end
end
subgraph contact["`**contact**`"]
direction TB
style contact fill:#99bcdb,stroke:#274d6e,stroke-width:8px
subgraph contact:roles[ ]
style contact:roles fill:#99bcdb,stroke:white
role:contact:OWNER[[contact:OWNER]]
role:contact:ADMIN[[contact:ADMIN]]
role:contact:REFERRER[[contact:REFERRER]]
end
end
subgraph relation["`**relation**`"]
direction TB
style relation fill:#dd4901,stroke:#274d6e,stroke-width:8px
subgraph relation:roles[ ]
style relation:roles fill:#dd4901,stroke:white
role:relation:OWNER[[relation:OWNER]]
role:relation:ADMIN[[relation:ADMIN]]
role:relation:AGENT[[relation:AGENT]]
role:relation:TENANT[[relation:TENANT]]
end
subgraph relation:permissions[ ]
style relation:permissions fill:#dd4901,stroke:white
perm:relation:DELETE{{relation:DELETE}}
perm:relation:UPDATE{{relation:UPDATE}}
perm:relation:SELECT{{relation:SELECT}}
perm:relation:INSERT{{relation:INSERT}}
end
end
%% granting roles to users
user:creator ==> role:relation:OWNER
%% granting roles to roles
role:global:ADMIN -.-> role:anchorPerson:OWNER
role:anchorPerson:OWNER -.-> role:anchorPerson:ADMIN
role:anchorPerson:ADMIN -.-> role:anchorPerson:REFERRER
role:global:ADMIN -.-> role:holderPerson:OWNER
role:holderPerson:OWNER -.-> role:holderPerson:ADMIN
role:holderPerson:ADMIN -.-> role:holderPerson:REFERRER
role:global:ADMIN -.-> role:contact:OWNER
role:contact:OWNER -.-> role:contact:ADMIN
role:contact:ADMIN -.-> role:contact:REFERRER
role:global:ADMIN ==> role:relation:OWNER
role:holderPerson:ADMIN ==> role:relation:OWNER
role:relation:OWNER ==> role:relation:ADMIN
role:relation:ADMIN ==> role:anchorPerson:OWNER
role:relation:ADMIN ==> role:relation:AGENT
role:anchorPerson:ADMIN ==> role:relation:AGENT
role:relation:AGENT ==> role:relation:TENANT
role:holderPerson:ADMIN ==> role:relation:TENANT
role:contact:ADMIN ==> role:relation:TENANT
role:relation:TENANT ==> role:anchorPerson:REFERRER
role:relation:TENANT ==> role:holderPerson:REFERRER
role:relation:TENANT ==> role:contact:REFERRER
%% granting permissions to roles
role:relation:OWNER ==> perm:relation:DELETE
role:relation:ADMIN ==> perm:relation:UPDATE
role:relation:TENANT ==> perm:relation:SELECT
role:anchorPerson:ADMIN ==> perm:relation:INSERT
```

View File

@ -82,16 +82,19 @@ role:global:ADMIN -.-> role:contact:OWNER
role:contact:OWNER -.-> role:contact:ADMIN role:contact:OWNER -.-> role:contact:ADMIN
role:contact:ADMIN -.-> role:contact:REFERRER role:contact:ADMIN -.-> role:contact:REFERRER
role:global:ADMIN ==> role:relation:OWNER role:global:ADMIN ==> role:relation:OWNER
role:holderPerson:ADMIN ==> role:relation:OWNER
role:relation:OWNER ==> role:relation:ADMIN role:relation:OWNER ==> role:relation:ADMIN
role:anchorPerson:ADMIN ==> role:relation:ADMIN role:relation:ADMIN ==> role:anchorPerson:OWNER
role:relation:ADMIN ==> role:relation:AGENT role:relation:ADMIN ==> role:relation:AGENT
role:holderPerson:ADMIN ==> role:relation:AGENT role:anchorPerson:ADMIN ==> role:relation:AGENT
role:relation:AGENT ==> role:relation:TENANT role:relation:AGENT ==> role:relation:TENANT
role:holderPerson:ADMIN ==> role:relation:TENANT role:holderPerson:ADMIN ==> role:relation:TENANT
role:contact:ADMIN ==> role:relation:TENANT role:contact:ADMIN ==> role:relation:TENANT
role:relation:TENANT ==> role:anchorPerson:REFERRER role:relation:TENANT ==> role:anchorPerson:REFERRER
role:relation:TENANT ==> role:holderPerson:REFERRER role:relation:TENANT ==> role:holderPerson:REFERRER
role:relation:TENANT ==> role:contact:REFERRER role:relation:TENANT ==> role:contact:REFERRER
role:anchorPerson:ADMIN ==> role:relation:ADMIN
role:holderPerson:ADMIN ==> role:relation:AGENT
%% granting permissions to roles %% granting permissions to roles
role:relation:OWNER ==> perm:relation:DELETE role:relation:OWNER ==> perm:relation:DELETE

View File

@ -50,38 +50,51 @@ begin
perform createRoleWithGrants( perform createRoleWithGrants(
hsOfficeRelationOWNER(NEW), hsOfficeRelationOWNER(NEW),
permissions => array['DELETE'], permissions => array['DELETE'],
incomingSuperRoles => array[globalADMIN()],
userUuids => array[currentUserUuid()] userUuids => array[currentUserUuid()]
); );
perform createRoleWithGrants( perform createRoleWithGrants(
hsOfficeRelationADMIN(NEW), hsOfficeRelationADMIN(NEW),
permissions => array['UPDATE'], permissions => array['UPDATE']
incomingSuperRoles => array[
hsOfficePersonADMIN(newAnchorPerson),
hsOfficeRelationOWNER(NEW)]
); );
perform createRoleWithGrants( perform createRoleWithGrants(
hsOfficeRelationAGENT(NEW), hsOfficeRelationAGENT(NEW)
incomingSuperRoles => array[
hsOfficePersonADMIN(newHolderPerson),
hsOfficeRelationADMIN(NEW)]
); );
perform createRoleWithGrants( perform createRoleWithGrants(
hsOfficeRelationTENANT(NEW), hsOfficeRelationTENANT(NEW),
permissions => array['SELECT'], permissions => array['SELECT']
incomingSuperRoles => array[
hsOfficeContactADMIN(newContact),
hsOfficePersonADMIN(newHolderPerson),
hsOfficeRelationAGENT(NEW)],
outgoingSubRoles => array[
hsOfficeContactREFERRER(newContact),
hsOfficePersonREFERRER(newAnchorPerson),
hsOfficePersonREFERRER(newHolderPerson)]
); );
IF type = 'REPRESENTATIVE' THEN
call grantRoleToRole(hsOfficeContactREFERRER(newContact), hsOfficeRelationTENANT(NEW));
call grantRoleToRole(hsOfficePersonOWNER(newAnchorPerson), hsOfficeRelationADMIN(NEW));
call grantRoleToRole(hsOfficePersonREFERRER(newAnchorPerson), hsOfficeRelationTENANT(NEW));
call grantRoleToRole(hsOfficePersonREFERRER(newHolderPerson), hsOfficeRelationTENANT(NEW));
call grantRoleToRole(hsOfficeRelationADMIN(NEW), hsOfficeRelationOWNER(NEW));
call grantRoleToRole(hsOfficeRelationAGENT(NEW), hsOfficePersonADMIN(newAnchorPerson));
call grantRoleToRole(hsOfficeRelationAGENT(NEW), hsOfficeRelationADMIN(NEW));
call grantRoleToRole(hsOfficeRelationOWNER(NEW), globalAdmin());
call grantRoleToRole(hsOfficeRelationOWNER(NEW), hsOfficePersonADMIN(newHolderPerson));
call grantRoleToRole(hsOfficeRelationTENANT(NEW), hsOfficeContactADMIN(newContact));
call grantRoleToRole(hsOfficeRelationTENANT(NEW), hsOfficePersonADMIN(newHolderPerson));
call grantRoleToRole(hsOfficeRelationTENANT(NEW), hsOfficeRelationAGENT(NEW));
ELSIF type = 'DEBITOR' THEN
ELSE
call grantRoleToRole(hsOfficeContactREFERRER(newContact), hsOfficeRelationTENANT(NEW));
call grantRoleToRole(hsOfficePersonREFERRER(newAnchorPerson), hsOfficeRelationTENANT(NEW));
call grantRoleToRole(hsOfficePersonREFERRER(newHolderPerson), hsOfficeRelationTENANT(NEW));
call grantRoleToRole(hsOfficeRelationADMIN(NEW), hsOfficePersonADMIN(newAnchorPerson));
call grantRoleToRole(hsOfficeRelationADMIN(NEW), hsOfficeRelationOWNER(NEW));
call grantRoleToRole(hsOfficeRelationAGENT(NEW), hsOfficePersonADMIN(newHolderPerson));
call grantRoleToRole(hsOfficeRelationAGENT(NEW), hsOfficeRelationADMIN(NEW));
call grantRoleToRole(hsOfficeRelationOWNER(NEW), globalAdmin());
call grantRoleToRole(hsOfficeRelationTENANT(NEW), hsOfficeContactADMIN(newContact));
call grantRoleToRole(hsOfficeRelationTENANT(NEW), hsOfficePersonADMIN(newHolderPerson));
call grantRoleToRole(hsOfficeRelationTENANT(NEW), hsOfficeRelationAGENT(NEW));
END IF;
call leaveTriggerForObjectUuid(NEW.uuid); call leaveTriggerForObjectUuid(NEW.uuid);
end; $$; end; $$;
@ -118,48 +131,12 @@ create or replace procedure updateRbacRulesForHsOfficeRelation(
NEW hs_office_relation NEW hs_office_relation
) )
language plpgsql as $$ language plpgsql as $$
declare
oldHolderPerson hs_office_person;
newHolderPerson hs_office_person;
oldAnchorPerson hs_office_person;
newAnchorPerson hs_office_person;
oldContact hs_office_contact;
newContact hs_office_contact;
begin begin
call enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM hs_office_person WHERE uuid = OLD.holderUuid INTO oldHolderPerson;
assert oldHolderPerson.uuid is not null, format('oldHolderPerson must not be null for OLD.holderUuid = %s', OLD.holderUuid);
SELECT * FROM hs_office_person WHERE uuid = NEW.holderUuid INTO newHolderPerson;
assert newHolderPerson.uuid is not null, format('newHolderPerson must not be null for NEW.holderUuid = %s', NEW.holderUuid);
SELECT * FROM hs_office_person WHERE uuid = OLD.anchorUuid INTO oldAnchorPerson;
assert oldAnchorPerson.uuid is not null, format('oldAnchorPerson must not be null for OLD.anchorUuid = %s', OLD.anchorUuid);
SELECT * FROM hs_office_person WHERE uuid = NEW.anchorUuid INTO newAnchorPerson;
assert newAnchorPerson.uuid is not null, format('newAnchorPerson must not be null for NEW.anchorUuid = %s', NEW.anchorUuid);
SELECT * FROM hs_office_contact WHERE uuid = OLD.contactUuid INTO oldContact;
assert oldContact.uuid is not null, format('oldContact must not be null for OLD.contactUuid = %s', OLD.contactUuid);
SELECT * FROM hs_office_contact WHERE uuid = NEW.contactUuid INTO newContact;
assert newContact.uuid is not null, format('newContact must not be null for NEW.contactUuid = %s', NEW.contactUuid);
if NEW.contactUuid <> OLD.contactUuid then
call revokeRoleFromRole(hsOfficeRelationTENANT(OLD), hsOfficeContactADMIN(oldContact));
call grantRoleToRole(hsOfficeRelationTENANT(NEW), hsOfficeContactADMIN(newContact));
call revokeRoleFromRole(hsOfficeContactREFERRER(oldContact), hsOfficeRelationTENANT(OLD));
call grantRoleToRole(hsOfficeContactREFERRER(newContact), hsOfficeRelationTENANT(NEW));
if NEW.contactUuid is distinct from OLD.contactUuid then
delete from rbacgrants g where g.grantedbytriggerof = OLD.uuid;
call buildRbacSystemForHsOfficeRelation(NEW);
end if; end if;
call leaveTriggerForObjectUuid(NEW.uuid);
end; $$; end; $$;
/* /*

View File

@ -0,0 +1,46 @@
package net.hostsharing.hsadminng.rbac.rbacdef;
import net.hostsharing.hsadminng.rbac.rbacdef.ConditionGenerator.CaseDef;
import org.junit.jupiter.api.Test;
import java.util.function.Consumer;
import static net.hostsharing.hsadminng.rbac.rbacdef.ConditionGenerator.CaseDef.inCaseOf;
import static net.hostsharing.hsadminng.rbac.rbacdef.ConditionGenerator.CaseDef.inOtherCases;
import static org.apache.commons.collections4.SetUtils.hashSet;
import static org.assertj.core.api.Assertions.assertThat;
class ConditionGeneratorUnitTest {
static final Consumer<RbacView> CONSUMER_FAKE = null;
static final CaseDef CASE_A = inCaseOf("A", CONSUMER_FAKE);
static final CaseDef CASE_B = inCaseOf("B", CONSUMER_FAKE);
static final CaseDef DEFAULT_CASE = inOtherCases(CONSUMER_FAKE);
static final ConditionGenerator GENERATOR_WITH_A_B_AND_DEFAULT =
new ConditionGenerator("someCol", hashSet(CASE_A, CASE_B, DEFAULT_CASE));
@Test
void onlyCase_A_from_A_B_and_DEFAULT() {
assertThat(GENERATOR_WITH_A_B_AND_DEFAULT.generatePlPgSql(hashSet(CASE_A)))
.isEqualTo("someCol = 'A'");
}
@Test
void case_A_and_B_from_A_B_and_DEFAULT() {
assertThat(GENERATOR_WITH_A_B_AND_DEFAULT.generatePlPgSql(hashSet(CASE_A, CASE_B)))
.isEqualTo("someCol in ('A', 'B')");
}
@Test
void case_DEFAULT_from_A_B_and_DEFAULT() {
assertThat(GENERATOR_WITH_A_B_AND_DEFAULT.generatePlPgSql(hashSet(DEFAULT_CASE)))
.isEqualTo("someCol not in ('A', 'B')");
}
@Test
void case_A_and_DEFAULT_from_A_B_and_DEFAULT() {
assertThat(GENERATOR_WITH_A_B_AND_DEFAULT.generatePlPgSql(hashSet(CASE_A, DEFAULT_CASE)))
.isEqualTo("someCol <> 'B'");
}
}