RBAC Diagram+PostgreSQL Generator #21
@ -0,0 +1,5 @@
|
||||
package net.hostsharing.hsadminng.rbac.rbacdef;
|
||||
|
||||
public enum PostgresTriggerReference {
|
||||
NEW, OLD
|
||||
}
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user