diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java index 653702f7..4347549f 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java @@ -6,8 +6,10 @@ import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.stream.Stream; -import static java.util.stream.Collectors.*; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toSet; import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.NEW; import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.OLD; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.*; @@ -32,7 +34,7 @@ class RolesGrantsAndPermissionsGenerator { .filter(RbacView.RbacGrantDefinition::isToCreate) .collect(toSet())); this.liquibaseTagPrefix = liquibaseTagPrefix; - + entityClass = rbacDef.getRootEntityAlias().entityClass(); simpleEntityVarName = rbacDef.getRootEntityAlias().simpleName(); simpleEntityName = capitalize(simpleEntityVarName); @@ -43,6 +45,9 @@ class RolesGrantsAndPermissionsGenerator { generateHeader(plPgSql); generateTriggerFunction(plPgSql); generateInsertTrigger(plPgSql); + if (hasAnyUpdatableEntityAliases()) { + generateUpdateTrigger(plPgSql); + } generateFooter(plPgSql); } @@ -61,7 +66,7 @@ class RolesGrantsAndPermissionsGenerator { A Creates the roles, grants and permission for the AFTER INSERT TRIGGER. */ - create or replace procedure createRbacRolesFor${simpleEntityName}( + create or replace procedure buildRbacSystemFor${simpleEntityName}( TG_OP text, OLD ${rawTableName}, NEW ${rawTableName} @@ -73,36 +78,45 @@ class RolesGrantsAndPermissionsGenerator { .replace("${rawTableName}", rawTableName)); plPgSql.indented(() -> { - rbacDef.getEntityAliases().values().stream() - .filter((ea) -> !rbacDef.isRootEntityAlias(ea)) - .filter((ea) -> ea.fetchSql() != null) + updatableEntityAliases() .forEach((ea) -> { - plPgSql.writeLn( entityRefVar(NEW, ea) + " " + getRawTableName(ea.entityClass()) + ";"); + plPgSql.writeLn(entityRefVar(NEW, ea) + " " + getRawTableName(ea.entityClass()) + ";"); }); - }); + }); plPgSql.indented(() -> { plPgSql.writeLn("begin"); + generateCreateRolesAndGrantsAfterInsert(plPgSql); - generateUpdateRolesAndGrantsAfterUpdate(plPgSql); + if (hasAnyUpdatableEntityAliases()) { + generateUpdateRolesAndGrantsAfterUpdate(plPgSql); + } + plPgSql.writeLn(""" + else + raise exception 'invalid usage of TRIGGER'; + end if; + """); plPgSql.ensureEmptyLine(); - plPgSql.writeLn("end; $$;"); }); + plPgSql.writeLn("end; $$;"); plPgSql.writeLn(); } + private boolean hasAnyUpdatableEntityAliases() { + return updatableEntityAliases().anyMatch(e -> true); + } + private void generateCreateRolesAndGrantsAfterInsert(final StringWriter plPgSql) { plPgSql.ensureEmptyLine(); plPgSql.writeLn("if TG_OP = 'INSERT' then"); plPgSql.indented(() -> { - rbacDef.getEntityAliases().values().stream() - .filter((ea) -> !rbacDef.isRootEntityAlias(ea)) - .filter((ea) -> ea.fetchSql() != null) + updatableEntityAliases() .forEach((ea) -> { - plPgSql.writeLn( ea.fetchSql().sql.replace("${ref}", NEW.name()) + " into " + entityRefVar(NEW, ea) + ";"); - }); + plPgSql.writeLn( + ea.fetchSql().sql.replace("${ref}", NEW.name()) + " into " + entityRefVar(NEW, ea) + ";"); + }); createRolesWithGrantsSql(plPgSql, OWNER); createRolesWithGrantsSql(plPgSql, ADMIN); @@ -114,13 +128,22 @@ class RolesGrantsAndPermissionsGenerator { generateGrants(plPgSql, ROLE_TO_ROLE); generateGrants(plPgSql, PERM_TO_ROLE); }); + } - plPgSql.writeLn("end if;"); -} + private Stream referencedEntityAliases() { + return rbacDef.getEntityAliases().values().stream() + .filter((ea) -> !rbacDef.isRootEntityAlias(ea)) + .filter((ea) -> ea.fetchSql() != null); + } + + private Stream updatableEntityAliases() { + return referencedEntityAliases() + .filter(ea -> rbacDef.getUpdatableColumns().contains(ea.dependsOnColum().column) ); + } private void generateUpdateRolesAndGrantsAfterUpdate(final StringWriter plPgSql) { plPgSql.ensureEmptyLine(); - plPgSql.writeLn("if TG_OP = 'UPDATE' then"); + plPgSql.writeLn("elsif TG_OP = 'UPDATE' then"); plPgSql.indented(() -> { @@ -128,7 +151,8 @@ class RolesGrantsAndPermissionsGenerator { .filter(ea -> !rbacDef.isRootEntityAlias(ea)) .filter(ea -> ea.fetchSql() != null) .forEach(ea -> { - plPgSql.writeLn( ea.fetchSql().sql.replace("${ref}", OLD.name()) + " into " + entityRefVar(OLD, ea) + ";"); + plPgSql.writeLn( + ea.fetchSql().sql.replace("${ref}", OLD.name()) + " into " + entityRefVar(OLD, ea) + ";"); }); rbacDef.getEntityAliases().values().stream() @@ -147,8 +171,6 @@ class RolesGrantsAndPermissionsGenerator { plPgSql.writeLn("end if;"); }); }); - - plPgSql.writeLn("end if;"); } private boolean isUpdatable(final RbacView.Column c) { @@ -160,7 +182,8 @@ class RolesGrantsAndPermissionsGenerator { .filter(RbacView.RbacGrantDefinition::isToCreate) .filter(g -> g.dependsOnColumn(columnName)) .forEach(g -> { - plPgSql.writeLn("-- TODO: " + g); + plPgSql.writeLn("-- TODO: revoke " + g); + plPgSql.writeLn(generateGrant(g)); }); } @@ -177,11 +200,11 @@ class RolesGrantsAndPermissionsGenerator { return switch (grantDef.grantType()) { case ROLE_TO_USER -> throw new IllegalArgumentException("unexpected grant"); case ROLE_TO_ROLE -> "call grantRoleToRole(${subRoleRef}, ${superRoleRef}));" - .replace("${subRoleRef}", roleRef(NEW, grantDef.getSubRoleDef()) ) - .replace("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef()) ); + .replace("${subRoleRef}", roleRef(NEW, grantDef.getSubRoleDef())) + .replace("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef())); case PERM_TO_ROLE -> "call grantPermissionsToRole(${permRef}, ${superRoleRef}));" - .replace("${permRef}", permRef(NEW, grantDef.getPermDef()) ) - .replace("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef()) ); + .replace("${permRef}", permRef(NEW, grantDef.getPermDef())) + .replace("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef())); }; } @@ -198,10 +221,10 @@ class RolesGrantsAndPermissionsGenerator { } private String roleRef(final PostgresTriggerReference rootRefVar, final RbacView.RbacRoleDefinition roleDef) { - if ( roleDef == null ) { + if (roleDef == null) { System.out.println("null"); } - if ( roleDef.getEntityAlias().isGlobal()) { + if (roleDef.getEntityAlias().isGlobal()) { return "globalAdmin()"; } final String entityRefVar = entityRefVar(rootRefVar, roleDef.getEntityAlias()); @@ -217,16 +240,16 @@ class RolesGrantsAndPermissionsGenerator { private void createRolesWithGrantsSql(final StringWriter plPgSql, final RbacView.Role role) { - final var isToCreate = rbacDef.getRoleDefs().stream() - .filter(roleDef -> rbacDef.isRootEntityAlias(roleDef.getEntityAlias()) && roleDef.getRole() == role ) - .findFirst().map(RbacView.RbacRoleDefinition::isToCreate).orElse(false); + final var isToCreate = rbacDef.getRoleDefs().stream() + .filter(roleDef -> rbacDef.isRootEntityAlias(roleDef.getEntityAlias()) && roleDef.getRole() == role) + .findFirst().map(RbacView.RbacRoleDefinition::isToCreate).orElse(false); if (!isToCreate) { return; } plPgSql.writeLn(); plPgSql.writeLn("perform createRoleWithGrants("); - plPgSql.indented( () -> { + plPgSql.indented(() -> { plPgSql.writeLn("${simpleVarName)${roleSuffix}(NEW)," .replace("${simpleVarName)", simpleEntityVarName) .replace("${roleSuffix}", capitalize(role.roleName()))); @@ -268,7 +291,7 @@ class RolesGrantsAndPermissionsGenerator { .map(RbacView.Permission::permission) .map(p -> "'" + p + "'") .toList(); - plPgSql.indented( () -> + plPgSql.indented(() -> plPgSql.writeLn("permissions => array[" + joinArrayElements(arrayElements, 3) + "],\n")); rbacGrants.removeAll(permissionGrantsForRole); } @@ -306,31 +329,39 @@ class RolesGrantsAndPermissionsGenerator { : arrayElements.stream().collect(joining(",\n\t", "\n\t", "")); } - private Set findPermissionsGrantsForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { + private Set findPermissionsGrantsForRole( + final RbacView.EntityAlias entityAlias, + final RbacView.Role role) { final var roleDef = rbacDef.findRbacRole(entityAlias, role); return rbacGrants.stream() - .filter(g -> g.grantType() == PERM_TO_ROLE && g.getSuperRoleDef()==roleDef ) + .filter(g -> g.grantType() == PERM_TO_ROLE && g.getSuperRoleDef() == roleDef) .collect(toSet()); } - private Set findGrantsToUserForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { + private Set findGrantsToUserForRole( + final RbacView.EntityAlias entityAlias, + final RbacView.Role role) { final var roleDef = rbacDef.findRbacRole(entityAlias, role); return rbacGrants.stream() - .filter(g -> g.grantType() == ROLE_TO_USER && g.getSubRoleDef() == roleDef ) + .filter(g -> g.grantType() == ROLE_TO_USER && g.getSubRoleDef() == roleDef) .collect(toSet()); } - private Set findIncomingSuperRolesForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { + private Set findIncomingSuperRolesForRole( + final RbacView.EntityAlias entityAlias, + final RbacView.Role role) { final var roleDef = rbacDef.findRbacRole(entityAlias, role); return rbacGrants.stream() - .filter(g -> g.grantType() == ROLE_TO_ROLE && g.getSubRoleDef()==roleDef ) + .filter(g -> g.grantType() == ROLE_TO_ROLE && g.getSubRoleDef() == roleDef) .collect(toSet()); } - private Set findOutgoingSuperRolesForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { + private Set findOutgoingSuperRolesForRole( + final RbacView.EntityAlias entityAlias, + final RbacView.Role role) { final var roleDef = rbacDef.findRbacRole(entityAlias, role); return rbacGrants.stream() - .filter(g -> g.grantType() == ROLE_TO_ROLE && g.getSuperRoleDef()==roleDef ) + .filter(g -> g.grantType() == ROLE_TO_ROLE && g.getSuperRoleDef() == roleDef) .filter(g -> g.getSubRoleDef().getEntityAlias() != entityAlias) .collect(toSet()); } @@ -341,20 +372,47 @@ class RolesGrantsAndPermissionsGenerator { An AFTER INSERT TRIGGER which creates the role structure for a new ${simpleEntityName} */ - create or replace function createRbacRolesFor${simpleEntityName}_tf() + create or replace function insertTriggerFor${simpleEntityName}_tf() returns trigger language plpgsql strict as $$ begin - call createRbacRolesFor${simpleEntityName}(TG_OP, OLD, NEW); + call buildRbacSystemFor${simpleEntityName}(TG_OP, OLD, NEW); return NEW; end; $$; - create trigger createRbacRolesFor${simpleEntityName}_tg + create trigger insertTriggerFor${simpleEntityName}_tg after insert on ${rawTableName} for each row - execute procedure createRbacRolesFor${simpleEntityName}_tf(); + execute procedure insertTriggerFor${simpleEntityName}_tf(); + --// + """ + .replace("${simpleEntityName}", simpleEntityName) + .replace("${rawTableName}", rawTableName) + ); + } + + private void generateUpdateTrigger(final StringWriter plPgSql) { + plPgSql.writeLn(""" + /* + An AFTER UPDATE TRIGGER which re-wires the grant structure for an updated ${simpleEntityName} + */ + + create or replace function updateTriggerFor${simpleEntityName}_tf() + returns trigger + language plpgsql + strict as $$ + begin + call buildRbacSystemFor${simpleEntityName}(TG_OP, OLD, NEW); + return NEW; + end; $$; + + create trigger updateTriggerFor${simpleEntityName}_tg + after update + on ${rawTableName} + for each row + execute procedure updateTriggerFor${simpleEntityName}_tf(); --// """ .replace("${simpleEntityName}", simpleEntityName) @@ -385,7 +443,9 @@ class RolesGrantsAndPermissionsGenerator { return uncapitalize(roleDef.getEntityAlias().simpleName()) + capitalize(roleDef.getRole().roleName()); } - private static String toTriggerReference(final PostgresTriggerReference triggerRef, final RbacView.EntityAlias entityAlias) { + private static String toTriggerReference( + final PostgresTriggerReference triggerRef, + final RbacView.EntityAlias entityAlias) { return triggerRef.name().toLowerCase() + capitalize(entityAlias.aliasName()); } }