RBAC Diagram+PostgreSQL Generator #21

Merged
hsh-michaelhoennig merged 54 commits from experimental-rbacview-generator into master 2024-03-11 12:30:44 +01:00
4 changed files with 305 additions and 11 deletions
Showing only changes of commit 74071c15db - Show all commits

View File

@ -0,0 +1,5 @@
package net.hostsharing.hsadminng.rbac.rbacdef;
public enum PostgresTriggerReference {
NEW, OLD
}

View File

@ -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<RbacUserDefinition> userDefs = new HashSet<>();
private final Set<RbacUserReference> userDefs = new HashSet<>();
private final Set<RbacRoleDefinition> roleDefs = new HashSet<>();
private final Set<RbacPermissionDefinition> permDefs = new HashSet<>();
private final Map<String, EntityAlias> 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<? extends HasUuid> entityClass) {
this(aliasName, entityClass, null, null);
}
boolean isGlobal() {
return aliasName().equals("global");
}
}
public record Role(String roleName) {

View File

@ -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);
}
}

View File

@ -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<RbacPermissionDefinition> 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<RbacView.RbacUserReference> 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<RbacView.RbacGrantDefinition> 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<RbacView.RbacGrantDefinition> 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());
}
}
}