From 74071c15db0173ab159a872403233cee3e4068ee Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 23 Feb 2024 12:17:41 +0100 Subject: [PATCH] generate postgres trigger function + trigger for RbacView for simple objects --- .../rbacdef/PostgresTriggerReference.java | 5 + .../hsadminng/rbac/rbacdef/RbacView.java | 26 +- .../rbacdef/RbacViewPostgresGenerator.java | 47 ++++ .../RolesGrantsAndPermissionsGenerator.java | 238 ++++++++++++++++++ 4 files changed, 305 insertions(+), 11 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/PostgresTriggerReference.java create mode 100644 src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/PostgresTriggerReference.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/PostgresTriggerReference.java new file mode 100644 index 00000000..4fb5cb61 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/PostgresTriggerReference.java @@ -0,0 +1,5 @@ +package net.hostsharing.hsadminng.rbac.rbacdef; + +public enum PostgresTriggerReference { + NEW, OLD +} diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java index 880b1577..1997696d 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -8,7 +8,7 @@ import net.hostsharing.hsadminng.persistence.HasUuid; import jakarta.validation.constraints.NotNull; import java.util.*; -import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserDefinition.UserRole.CREATOR; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR; @Getter public class RbacView { @@ -17,7 +17,7 @@ public class RbacView { private final EntityAlias entityAlias; - private final Set userDefs = new HashSet<>(); + private final Set userDefs = new HashSet<>(); private final Set roleDefs = new HashSet<>(); private final Set permDefs = new HashSet<>(); private final Map entityAliases = new HashMap<>() { @@ -43,7 +43,7 @@ public class RbacView { RbacView(final String alias, final Class entityClass) { entityAlias = new EntityAlias(alias, entityClass); entityAliases.put(alias, entityAlias); - new RbacUserDefinition(CREATOR); + new RbacUserReference(CREATOR); entityAliases.put("global", new EntityAlias("global")); } @@ -58,7 +58,7 @@ public class RbacView { } public RbacRoleDefinition createRole(final Role role) { - return findRbacRole(entityAlias, role); + return findRbacRole(entityAlias, role).toCreate(); } public RbacPermissionDefinition createPermission(final Permission permission) { @@ -157,7 +157,7 @@ public class RbacView { @Getter @EqualsAndHashCode public class RbacGrantDefinition { - private final RbacUserDefinition userDef; + private final RbacUserReference userDef; private final RbacRoleDefinition superRoleDef; private final RbacRoleDefinition subRoleDef; private final RbacPermissionDefinition permDef; @@ -187,7 +187,7 @@ public class RbacView { grantDefs.add(this); } - public RbacGrantDefinition(final RbacRoleDefinition roleDef, final RbacUserDefinition userDef) { + public RbacGrantDefinition(final RbacRoleDefinition roleDef, final RbacUserReference userDef) { this.userDef = userDef; this.subRoleDef = roleDef; this.superRoleDef = null; @@ -323,19 +323,19 @@ public class RbacView { } } - public RbacUserDefinition currentUser() { + public RbacUserReference currentUser() { return userDefs.stream().filter(u -> u.role == CREATOR).findFirst().orElseThrow(); } @EqualsAndHashCode - public class RbacUserDefinition { + public class RbacUserReference { public enum UserRole { CREATOR } final UserRole role; - public RbacUserDefinition(final UserRole creator) { + public RbacUserReference(final UserRole creator) { this.role = creator; userDefs.add(this); } @@ -354,14 +354,14 @@ public class RbacView { return found; } - private RbacRoleDefinition findRbacRole(final EntityAlias entityAlias, final Role role) { + public RbacRoleDefinition findRbacRole(final EntityAlias entityAlias, final Role role) { return roleDefs.stream() .filter(r -> r.getEntityAlias() == entityAlias && r.getRole().equals(role)) .findFirst() .orElseGet(() -> new RbacRoleDefinition(entityAlias, role)); } - private RbacRoleDefinition findRbacRole(final String entityAliasName, final Role role) { + public RbacRoleDefinition findRbacRole(final String entityAliasName, final Role role) { return findRbacRole(findEntityAlias(entityAliasName), role); } @@ -374,6 +374,10 @@ public class RbacView { public EntityAlias(final String aliasName, final Class entityClass) { this(aliasName, entityClass, null, null); } + + boolean isGlobal() { + return aliasName().equals("global"); + } } public record Role(String roleName) { diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java new file mode 100644 index 00000000..862c3458 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java @@ -0,0 +1,47 @@ +package net.hostsharing.hsadminng.rbac.rbacdef; + +import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.time.LocalDateTime; + + +public class RbacViewPostgresGenerator { + + + private final RbacView rbacDef; + private final String liqibaseTagPrefix; + private final StringBuilder plPgSql = new StringBuilder(); + + public RbacViewPostgresGenerator(final RbacView forRbacDef) { + rbacDef = forRbacDef; + liqibaseTagPrefix = rbacDef.getEntityAlias().entityClass().getSimpleName(); + plPgSql.append(""" + --liquibase formatted sql + -- generated at: %{timestamp} + + """ + .replace("%{timestamp}", LocalDateTime.now().toString())); + + // generateSqlForRelatedRbacObject(); + + new RolesGrantsAndPermissionsGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql); + } + + + @Override +public String toString() { + return plPgSql.toString(); +} + +public static void main(String[] args) throws IOException { + + Files.writeString( + Paths.get("doc", "hsOfficeBankAccount.sql"), + new RbacViewPostgresGenerator(HsOfficeBankAccountEntity.hsOfficeBankAccount()).toString(), + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java new file mode 100644 index 00000000..30953c7b --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java @@ -0,0 +1,238 @@ +package net.hostsharing.hsadminng.rbac.rbacdef; + +import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacPermissionDefinition; +import org.apache.commons.lang3.StringUtils; + +import jakarta.persistence.Table; +import java.util.Set; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.joining; +import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.NEW; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.*; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*; +import static org.apache.commons.lang3.StringUtils.capitalize; +import static org.apache.commons.lang3.StringUtils.uncapitalize; + +class RolesGrantsAndPermissionsGenerator { + + private final RbacView rbacDef; + private final String liquibaseTagPrefix; + private final Class entityClass; + private final String simpleEntityName; + private final String simpleEntityVarName; + private final String rawTableName; + + RolesGrantsAndPermissionsGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) { + this.rbacDef = rbacDef; + this.liquibaseTagPrefix = liquibaseTagPrefix; + + entityClass = rbacDef.getEntityAlias().entityClass(); + simpleEntityName = withoutEntitySuffix(entityClass.getSimpleName()); + simpleEntityVarName = uncapitalize(simpleEntityName); + rawTableName = withoutRvSuffix(entityClass.getAnnotation(Table.class).name()); + } + + void generateTo(final StringBuilder plPgSql) { + generateHeader(plPgSql); + generateTriggerFunction(plPgSql); + generageInsertTrigger(plPgSql); + generateFooter(plPgSql); + } + + private void generateHeader(final StringBuilder plPgSql) { + plPgSql.append(""" + -- ============================================================================ + --changeset %{liquibaseTagPrefix}-rbac-CREATE-ROLES-GRANTS-PERMISSIONS:1 endDelimiter:--// + -- ---------------------------------------------------------------------------- + + """ + .replace("%{liquibaseTagPrefix}", liquibaseTagPrefix)); + } + + private void generateTriggerFunction(final StringBuilder plPgSql) { + plPgSql.append(""" + /* + Creates the roles, grants and permission for the AFTER INSERT TRIGGER. + */ + + create or replace function createRbacRolesFor%{simpleEntityName}() + returns trigger + language plpgsql + strict as $$ + begin + if TG_OP <> 'INSERT' then + raise exception 'invalid usage of TRIGGER AFTER INSERT function'; + end if; + %{createRolesWithGrantsSql} + return NEW; + end; $$; + """ + .replace("%{simpleEntityName}", simpleEntityName) + .replace("%{createRolesWithGrantsSql}", createRolesWithGrantsSql()) + ); + + } + + private String createRolesWithGrantsSql() { + final var plPgSql = new StringBuilder(); + createRolesWithGrantsSql(plPgSql, OWNER); + createRolesWithGrantsSql(plPgSql, ADMIN); + createRolesWithGrantsSql(plPgSql, AGENT); + createRolesWithGrantsSql(plPgSql, TENANT); + createRolesWithGrantsSql(plPgSql, REFERRER); + return plPgSql.toString(); + } + + private void createRolesWithGrantsSql(final StringBuilder plPgSql, final RbacView.Role role) { + + final var isToCreate = rbacDef.getRoleDefs().stream() + .filter(roleDef -> rbacDef.isMainEntityAlias(roleDef.getEntityAlias()) && roleDef.getRole() == role ) + .findFirst().map(RbacView.RbacRoleDefinition::isToCreate).orElse(false); + if (!isToCreate) { + return; + } + + plPgSql.append(""" + + perform createRoleWithGrants( + %{simpleEntityVarName)%{roleSuffix}(NEW), + """ + .replace("%{simpleEntityVarName)", simpleEntityVarName) + .replace("%{roleSuffix}", capitalize(role.roleName()))); + + final var permissionsForRole = findPermissionsGrantsForRole(rbacDef.getEntityAlias(), role); + if (!permissionsForRole.isEmpty()) { + final var permissionsForRoleInPlPgSql = permissionsForRole.stream() + .map(RbacPermissionDefinition::getPermission) + .map(RbacView.Permission::permission) + .map(p -> "'" + p + "'") + .collect(joining(", ")); + plPgSql.append(indent(3) + "permissions => array[" + permissionsForRoleInPlPgSql + "],\n"); + } + + final var grantsToUsers = findGrantsToUserForRole(rbacDef.getEntityAlias(), role); + if (!grantsToUsers.isEmpty()) { + final var grantsToUsersPlPgSql = grantsToUsers.stream() + .map(u -> toPlPgSqlReference(u)) + .collect(joining(", ")); + plPgSql.append(indent(3) + "userUuids => array[" + grantsToUsersPlPgSql + "],\n"); + } + + final var incomingGrants = findIncomingSuperRolesForRole(rbacDef.getEntityAlias(), role); + if (!incomingGrants.isEmpty()) { + final var incomingGrantsInPlPgSql = incomingGrants.stream() + .map(RbacView.RbacGrantDefinition::getSuperRoleDef) + .map(r -> toPlPgSqlReference(NEW, r)) + .collect(joining(", ")); + plPgSql.append(indent(3) + "incomingSuperRoles => array[" + incomingGrantsInPlPgSql + "],\n"); + } + + final var outgoingGrants = findOutgoingSuperRolesForRole(rbacDef.getEntityAlias(), role); + if (!outgoingGrants.isEmpty()) { + final var outgoingGrantsInPlPgSql = outgoingGrants.stream() + .map(RbacView.RbacGrantDefinition::getSuperRoleDef) + .map(r -> toPlPgSqlReference(NEW, r)) + .collect(joining(", ")); + plPgSql.append(indent(3) + "outgoingSubRoles => array[" + outgoingGrantsInPlPgSql + "],\n"); + } + + chopTail(plPgSql, ",\n"); + plPgSql.append("\n" + indent(2) + ");\n"); + } + + private Set findPermissionsGrantsForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { + final var roleDef = rbacDef.findRbacRole(entityAlias, role); + return rbacDef.getGrantDefs().stream() + .filter(g -> g.grantType() == ROLE_TO_PERM && g.getSuperRoleDef()==roleDef ) + .map(RbacView.RbacGrantDefinition::getPermDef) + .collect(Collectors.toSet()); + } + + private Set findGrantsToUserForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { + final var roleDef = rbacDef.findRbacRole(entityAlias, role); + return rbacDef.getGrantDefs().stream() + .filter(g -> g.grantType() == USER_TO_ROLE && g.getSubRoleDef() == roleDef ) + .map(RbacView.RbacGrantDefinition::getUserDef) + .collect(Collectors.toSet()); + } + + private Set findIncomingSuperRolesForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { + final var roleDef = rbacDef.findRbacRole(entityAlias, role); + return rbacDef.getGrantDefs().stream() + .filter(g -> g.grantType() == ROLE_TO_ROLE && g.getSubRoleDef()==roleDef ) + .collect(Collectors.toSet()); + } + + private Set findOutgoingSuperRolesForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { + final var roleDef = rbacDef.findRbacRole(entityAlias, role); + return rbacDef.getGrantDefs().stream() + .filter(g -> g.grantType() == ROLE_TO_ROLE && g.getSuperRoleDef()==roleDef ) + .filter(g -> g.getSubRoleDef().getEntityAlias() != entityAlias) + .collect(Collectors.toSet()); + } + + private void generageInsertTrigger(final StringBuilder plPgSql) { + plPgSql.append(""" + /* + An AFTER INSERT TRIGGER which creates the role structure for a new %{simpleEntityName} + */ + + create trigger createRbacRolesFor%{simpleEntityName}_Trigger + after insert + on %{rawTableName} + for each row + execute procedure createRbacRolesFor%{simpleEntityName}(); + --// + """ + .replace("%{simpleEntityName}", simpleEntityName) + .replace("%{rawTableName}", rawTableName) + ); + } + + private static void generateFooter(final StringBuilder plPgSql) { + plPgSql.append("\n\n"); + } + + private String withoutRvSuffix(final String tableName) { + return tableName.substring(0, tableName.length()-"_rv".length()); + } + + private String withoutEntitySuffix(final String simpleEntityName) { + return simpleEntityName.substring(0, simpleEntityName.length()-"Entity".length()); + } + + private String toPlPgSqlReference(final RbacView.RbacUserReference userRef) { + return switch (userRef.role) { + case CREATOR -> "currentUserUuid()"; + default -> throw new IllegalArgumentException("unknown user role: " + userRef); + }; + } + + private String toPlPgSqlReference(final PostgresTriggerReference triggerRef, final RbacView.RbacRoleDefinition roleDef) { + return toSimpleVarName(roleDef.getEntityAlias()) + StringUtils.capitalize(roleDef.getRole().roleName()) + + ( roleDef.getEntityAlias().isGlobal() ? "()" + : rbacDef.isMainEntityAlias(roleDef.getEntityAlias()) ? ("("+triggerRef.name()+")") + : "(" + toTriggerReference(triggerRef, roleDef.getEntityAlias()) + ")"); + } + + private static String toTriggerReference(final PostgresTriggerReference triggerRef, final RbacView.EntityAlias entityAlias) { + return triggerRef.name().toLowerCase() + StringUtils.capitalize(entityAlias.aliasName()); + } + + private String toSimpleVarName(final RbacView.EntityAlias entityAlias) { + return entityAlias.isGlobal() + ? entityAlias.aliasName() + : uncapitalize(withoutEntitySuffix(entityAlias.entityClass().getSimpleName())); + } + + private String indent(final int tabs) { + return " ".repeat(4*tabs); + } + + private void chopTail(final StringBuilder plPgSql, final String tail) { + if (plPgSql.toString().endsWith(tail)) { + plPgSql.setLength(plPgSql.length() - tail.length()); + } + } +}