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
13 changed files with 208 additions and 71 deletions
Showing only changes of commit 17282c857f - Show all commits

View File

@ -12,6 +12,7 @@ import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.io.IOException;
import java.util.UUID; import java.util.UUID;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.*; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.*;
@ -72,4 +73,8 @@ public class HsOfficeBankAccountEntity implements HasUuid, Stringifyable {
with.permission(VIEW); with.permission(VIEW);
}); });
} }
public static void main(String[] args) throws IOException {
rbac().generateWithBaseFileName("243-hs-office-bankaccount-rbac");
}
} }

View File

@ -11,6 +11,7 @@ import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.GenericGenerator; import org.hibernate.annotations.GenericGenerator;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.io.IOException;
import java.util.UUID; import java.util.UUID;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL;
@ -76,4 +77,8 @@ public class HsOfficeContactEntity implements Stringifyable, HasUuid {
with.permission(VIEW); with.permission(VIEW);
}); });
} }
public static void main(String[] args) throws IOException {
rbac().generateWithBaseFileName("203-hs-office-contact-rbac");
}
} }

View File

@ -14,6 +14,7 @@ import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.GenericGenerator; import org.hibernate.annotations.GenericGenerator;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.io.IOException;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@ -123,7 +124,7 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable {
.withUpdatableColumns( .withUpdatableColumns(
"debitorRel", "debitorRel",
"billable", "billable",
"billingContactUuid", "debitorUuid",
"refundBankAccountUuid", "refundBankAccountUuid",
"vatId", "vatId",
"vatCountryCode", "vatCountryCode",
@ -144,7 +145,7 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable {
.createPermission(VIEW).grantedTo("debitorRel", TENANT) .createPermission(VIEW).grantedTo("debitorRel", TENANT)
.importEntityAlias("refundBankAccount", HsOfficeBankAccountEntity.class, .importEntityAlias("refundBankAccount", HsOfficeBankAccountEntity.class,
dependsOnColumn("bankAccountUuid"), fetchedBySql(""" dependsOnColumn("refundBankAccountUuid"), fetchedBySql("""
SELECT * SELECT *
FROM hs_office_relationship AS r FROM hs_office_relationship AS r
WHERE r.relType = 'ACCOUNTING' AND r.relHolderUuid = ${REF}.debitorRelUuid WHERE r.relType = 'ACCOUNTING' AND r.relHolderUuid = ${REF}.debitorRelUuid
@ -154,7 +155,7 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable {
.toRole("debitorRel", AGENT).grantRole("refundBankAccount", REFERRER) .toRole("debitorRel", AGENT).grantRole("refundBankAccount", REFERRER)
.importEntityAlias("partnerRel", HsOfficeRelationshipEntity.class, .importEntityAlias("partnerRel", HsOfficeRelationshipEntity.class,
dependsOnColumn("debitorRelUuid"), fetchedBySql(""" dependsOnColumn("partnerRelUuid"), fetchedBySql("""
SELECT * SELECT *
FROM hs_office_relationship AS partnerRel FROM hs_office_relationship AS partnerRel
WHERE ${debitorRel}.relAnchorUuid = partnerRel.relHolderUuid WHERE ${debitorRel}.relAnchorUuid = partnerRel.relHolderUuid
@ -168,4 +169,8 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable {
.forExampleRole("operationalPerson", ADMIN).wouldBeGrantedTo("partnerRel", ADMIN) .forExampleRole("operationalPerson", ADMIN).wouldBeGrantedTo("partnerRel", ADMIN)
.forExampleRole("partnerRel", TENANT).wouldBeGrantedTo("partnerPerson", REFERRER); .forExampleRole("partnerRel", TENANT).wouldBeGrantedTo("partnerPerson", REFERRER);
} }
public static void main(String[] args) throws IOException {
rbac().generateWithBaseFileName("273-hs-office-debitor-rbac");
}
} }

View File

@ -10,6 +10,7 @@ import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable; import net.hostsharing.hsadminng.stringify.Stringifyable;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.io.IOException;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.UUID; import java.util.UUID;
@ -100,4 +101,8 @@ public class HsOfficePartnerDetailsEntity implements HasUuid, Stringifyable {
// not when anything in partner details changes. // not when anything in partner details changes.
; ;
} }
public static void main(String[] args) throws IOException {
rbac().generateWithBaseFileName("234-hs-office-partner-details-rbac");
}
} }

View File

@ -108,6 +108,6 @@ public class HsOfficePartnerEntity implements Stringifyable, HasUuid {
} }
public static void main(String[] args) throws IOException { public static void main(String[] args) throws IOException {
HsOfficePartnerEntity.rbac().generateWithBaseFileName("233-hs-office-partner-rbac"); rbac().generateWithBaseFileName("233-hs-office-partner-rbac");
} }
} }

View File

@ -10,6 +10,7 @@ import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.io.IOException;
import java.util.UUID; import java.util.UUID;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL;
@ -80,4 +81,9 @@ public class HsOfficePersonEntity implements HasUuid, Stringifyable {
with.permission(VIEW); with.permission(VIEW);
}); });
} }
public static void main(String[] args) throws IOException {
rbac().generateWithBaseFileName("213-hs-office-person-rbac");
}
} }

View File

@ -11,6 +11,7 @@ import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable; import net.hostsharing.hsadminng.stringify.Stringifyable;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.io.IOException;
import java.util.UUID; import java.util.UUID;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
@ -86,13 +87,16 @@ public class HsOfficeRelationshipEntity implements HasUuid, Stringifyable {
""")) """))
.withUpdatableColumns("contactUuid") .withUpdatableColumns("contactUuid")
.importEntityAlias("anchorPerson", HsOfficePersonEntity.class, .importEntityAlias("anchorPerson", HsOfficePersonEntity.class,
dependsOnColumn("relAnchorUuid"), fetchedBySql("select * from hs_office_person as p where p.uuid = ${REF}.relAnchorUuid") dependsOnColumn("relAnchorUuid"),
fetchedBySql("select * from hs_office_person as p where p.uuid = ${REF}.relAnchorUuid")
) )
.importEntityAlias("holderPerson", HsOfficePersonEntity.class, .importEntityAlias("holderPerson", HsOfficePersonEntity.class,
dependsOnColumn("relHolderUuid"), fetchedBySql("select * from hs_office_person as p where p.uuid = ${REF}.relHolderUuid") dependsOnColumn("relHolderUuid"),
fetchedBySql("select * from hs_office_person as p where p.uuid = ${REF}.relHolderUuid")
) )
.importEntityAlias("contact", HsOfficeContactEntity.class, .importEntityAlias("contact", HsOfficeContactEntity.class,
dependsOnColumn("contactUuid"), fetchedBySql("select * from hs_office_contact as c where c.uuid = ${REF}.contactUuid") dependsOnColumn("contactUuid"),
fetchedBySql("select * from hs_office_contact as c where c.uuid = ${REF}.contactUuid")
) )
.createRole(OWNER, (with) -> { .createRole(OWNER, (with) -> {
with.owningUser(CREATOR); with.owningUser(CREATOR);
@ -115,4 +119,8 @@ public class HsOfficeRelationshipEntity implements HasUuid, Stringifyable {
with.permission(VIEW); with.permission(VIEW);
}); });
} }
public static void main(String[] args) throws IOException {
rbac().generateWithBaseFileName("223-hs-office-relationship-rbac");
}
} }

View File

@ -123,6 +123,6 @@ public class HsOfficeSepaMandateEntity implements Stringifyable, HasUuid {
} }
public static void main(String[] args) throws IOException { public static void main(String[] args) throws IOException {
HsOfficeSepaMandateEntity.rbac().generateWithBaseFileName("253-hs-office-sepamandate-rbac"); rbac().generateWithBaseFileName("253-hs-office-sepamandate-rbac");
} }
} }

View File

@ -1,5 +1,6 @@
package net.hostsharing.hsadminng.rbac.rbacdef; package net.hostsharing.hsadminng.rbac.rbacdef;
import java.lang.reflect.Method;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -12,7 +13,9 @@ import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.util.*; import java.util.*;
import java.util.stream.Stream;
import static java.lang.reflect.Modifier.isStatic;
import static java.util.Optional.ofNullable; import static java.util.Optional.ofNullable;
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.autoFetched; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.autoFetched;
@ -57,7 +60,6 @@ public class RbacView {
new RbacUserReference(CREATOR); new RbacUserReference(CREATOR);
entityAliases.put("global", new EntityAlias("global")); entityAliases.put("global", new EntityAlias("global"));
} }
public RbacView withUpdatableColumns(final String... columnNames) { public RbacView withUpdatableColumns(final String... columnNames) {
Collections.addAll(updatableColumns, columnNames); Collections.addAll(updatableColumns, columnNames);
return this; return this;
@ -493,10 +495,11 @@ public class RbacView {
return entityClass == null; return entityClass == null;
} }
@NotNull
@Override @Override
public SQL fetchSql() { public SQL fetchSql() {
if ( fetchSql == null ) { if ( fetchSql == null ) {
return null; return SQL.noop();
} }
return switch (fetchSql.part) { return switch (fetchSql.part) {
case SQL_QUERY -> fetchSql; case SQL_QUERY -> fetchSql;
@ -505,6 +508,10 @@ public class RbacView {
}; };
} }
public boolean hasFetchSql() {
return fetchSql != null;
}
private String withoutEntitySuffix(final String simpleEntityName) { private String withoutEntitySuffix(final String simpleEntityName) {
return simpleEntityName.substring(0, simpleEntityName.length()-"Entity".length()); return simpleEntityName.substring(0, simpleEntityName.length()-"Entity".length());
} }
@ -583,6 +590,15 @@ public class RbacView {
return new SQL(null, Part.AUTO_FETCH); return new SQL(null, Part.AUTO_FETCH);
} }
/**
* DSL method to specify there there is no SQL query specified.
*
* @return a wrapped SQL definition object representing a noop query
*/
public static SQL noop() {
return new SQL(null, Part.NOOP);
}
/** Generic DSL method to specify an SQL SELECT expression. /** Generic DSL method to specify an SQL SELECT expression.
* *
* @param sql an SQL SELECT expression (not ending with ';) * @param sql an SQL SELECT expression (not ending with ';)
@ -604,8 +620,10 @@ public class RbacView {
} }
enum Part { enum Part {
NOOP,
SQL_QUERY, SQL_QUERY,
AUTO_FETCH, SQL_PROJECTION AUTO_FETCH,
SQL_PROJECTION
} }
final String sql; final String sql;
@ -668,4 +686,35 @@ public class RbacView {
return outerAliasName + "." + originalAliasName; return outerAliasName + "." + originalAliasName;
} }
} }
public static void main(String[] args) {
Stream.of(
net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity.class,
net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity.class,
net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerDetailsEntity.class,
net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity.class,
net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity.class,
net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity.class,
net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionEntity.class,
net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity.class,
net.hostsharing.hsadminng.hs.office.sepamandate.HsOfficeSepaMandateEntity.class,
net.hostsharing.hsadminng.hs.office.coopshares.HsOfficeCoopSharesTransactionEntity.class,
net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity.class
).forEach(c -> {
final Method mainMethod = Arrays.stream(c.getMethods()).filter(
m -> isStatic(m.getModifiers()) && m.getName().equals("main")
)
.findFirst()
.orElse(null);
if (mainMethod != null) {
try {
mainMethod.invoke(null, new Object[]{null});
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
} else {
System.err.println("no main method in: " + c.getName());
}
});
}
} }

View File

@ -76,7 +76,7 @@ public class RbacViewMermaidFlowchart {
private void wrapOutputInSubgraph(final String name, final String color, final String content) { private void wrapOutputInSubgraph(final String name, final String color, final String content) {
if (!StringUtils.isEmpty(content)) { if (!StringUtils.isEmpty(content)) {
flowchart.ensureEmptyLine(); flowchart.ensureSingleEmptyLine();
flowchart.writeLn("subgraph " + name + "[ ]\n"); flowchart.writeLn("subgraph " + name + "[ ]\n");
flowchart.indented(() -> { flowchart.indented(() -> {
flowchart.writeLn("style %{aliasName} fill:%{fillColor},stroke:white" flowchart.writeLn("style %{aliasName} fill:%{fillColor},stroke:white"
@ -102,7 +102,7 @@ public class RbacViewMermaidFlowchart {
.filter(g -> g.grantType() == f) .filter(g -> g.grantType() == f)
.toList(); .toList();
if ( !userGrants.isEmpty()) { if ( !userGrants.isEmpty()) {
flowchart.ensureEmptyLine(); flowchart.ensureSingleEmptyLine();
flowchart.writeLn(t); flowchart.writeLn(t);
userGrants.forEach(g -> flowchart.writeLn(grantDef(g))); userGrants.forEach(g -> flowchart.writeLn(grantDef(g)));
} }

View File

@ -8,6 +8,8 @@ import java.nio.file.Paths;
import java.nio.file.StandardOpenOption; import java.nio.file.StandardOpenOption;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.NEW;
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with;
public class RbacViewPostgresGenerator { public class RbacViewPostgresGenerator {
@ -21,10 +23,11 @@ public class RbacViewPostgresGenerator {
liqibaseTagPrefix = rbacDef.getRootEntityAlias().entityClass().getSimpleName(); liqibaseTagPrefix = rbacDef.getRootEntityAlias().entityClass().getSimpleName();
plPgSql.writeLn(""" plPgSql.writeLn("""
--liquibase formatted sql --liquibase formatted sql
-- This code generated was by ${generator} at %{timestamp}. -- This code generated was by ${generator} at ${timestamp}.
""" """,
.replace("${generator}", getClass().getSimpleName()) with("generator", getClass().getSimpleName()),
.replace("%{timestamp}", LocalDateTime.now().toString())); with("timestamp", LocalDateTime.now().toString()),
with("ref", NEW.name()));
new RolesGrantsAndPermissionsGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql); new RolesGrantsAndPermissionsGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql);
} }

View File

@ -4,7 +4,6 @@ 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.Objects;
import java.util.Set; import java.util.Set;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -15,6 +14,7 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.OL
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.*; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.getRawTableName; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.getRawTableName;
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with;
import static org.apache.commons.lang3.StringUtils.capitalize; import static org.apache.commons.lang3.StringUtils.capitalize;
import static org.apache.commons.lang3.StringUtils.uncapitalize; import static org.apache.commons.lang3.StringUtils.uncapitalize;
@ -65,27 +65,32 @@ class RolesGrantsAndPermissionsGenerator {
/* /*
A Creates the roles, grants and permission for the AFTER INSERT TRIGGER. A Creates the roles, grants and permission for the AFTER INSERT TRIGGER.
*/ */
create or replace procedure buildRbacSystemFor${simpleEntityName}( create or replace procedure buildRbacSystemFor${simpleEntityName}(
TG_OP text, TG_OP text,
OLD ${rawTableName}, OLD ${rawTableName},
NEW ${rawTableName} NEW ${rawTableName}
) )
language plpgsql as $$ language plpgsql as $$
declare declare
""" """
.replace("${simpleEntityName}", simpleEntityName) .replace("${simpleEntityName}", simpleEntityName)
.replace("${rawTableName}", rawTableName)); .replace("${rawTableName}", rawTableName));
plPgSql.chopEmptyLines();
plPgSql.indented(() -> { plPgSql.indented(() -> {
referencedEntityAliases()
.forEach((ea) -> plPgSql.writeLn(entityRefVar(NEW, ea) + " " + getRawTableName(ea.entityClass()) + ";"));
updatableEntityAliases() updatableEntityAliases()
.forEach((ea) -> { .forEach((ea) -> plPgSql.writeLn(entityRefVar(OLD, ea) + " " + getRawTableName(ea.entityClass()) + ";"));
plPgSql.writeLn(entityRefVar(NEW, ea) + " " + getRawTableName(ea.entityClass()) + ";");
});
}); });
plPgSql.writeLn();
plPgSql.writeLn("begin");
plPgSql.indented(() -> { plPgSql.indented(() -> {
plPgSql.writeLn("begin");
generateCreateRolesAndGrantsAfterInsert(plPgSql); generateCreateRolesAndGrantsAfterInsert(plPgSql);
if (hasAnyUpdatableEntityAliases()) { if (hasAnyUpdatableEntityAliases()) {
@ -96,28 +101,28 @@ class RolesGrantsAndPermissionsGenerator {
raise exception 'invalid usage of TRIGGER'; raise exception 'invalid usage of TRIGGER';
end if; end if;
"""); """);
plPgSql.ensureEmptyLine(); plPgSql.ensureSingleEmptyLine();
}); });
plPgSql.writeLn("end; $$;"); plPgSql.writeLn("end; $$;");
plPgSql.writeLn(); plPgSql.writeLn();
} }
private boolean hasAnyUpdatableEntityAliases() { private boolean hasAnyUpdatableEntityAliases() {
return updatableEntityAliases().anyMatch(e -> true); return updatableEntityAliases().anyMatch(e -> true);
} }
private void generateCreateRolesAndGrantsAfterInsert(final StringWriter plPgSql) { private void generateCreateRolesAndGrantsAfterInsert(final StringWriter plPgSql) {
plPgSql.ensureEmptyLine(); referencedEntityAliases()
.forEach((ea) -> plPgSql.writeLn(
ea.fetchSql().sql + " into " + entityRefVar(NEW, ea) + ";",
with("ref", NEW.name())));
plPgSql.ensureSingleEmptyLine();
plPgSql.writeLn("if TG_OP = 'INSERT' then"); plPgSql.writeLn("if TG_OP = 'INSERT' then");
plPgSql.indented(() -> { plPgSql.indented(() -> {
updatableEntityAliases() plPgSql.chopEmptyLines();
.forEach((ea) -> {
plPgSql.writeLn(
ea.fetchSql().sql.replace("${ref}", NEW.name()) + " into " + entityRefVar(NEW, ea) + ";");
});
createRolesWithGrantsSql(plPgSql, OWNER); createRolesWithGrantsSql(plPgSql, OWNER);
createRolesWithGrantsSql(plPgSql, ADMIN); createRolesWithGrantsSql(plPgSql, ADMIN);
createRolesWithGrantsSql(plPgSql, AGENT); createRolesWithGrantsSql(plPgSql, AGENT);
@ -127,38 +132,36 @@ class RolesGrantsAndPermissionsGenerator {
generateGrants(plPgSql, ROLE_TO_USER); generateGrants(plPgSql, ROLE_TO_USER);
generateGrants(plPgSql, ROLE_TO_ROLE); generateGrants(plPgSql, ROLE_TO_ROLE);
generateGrants(plPgSql, PERM_TO_ROLE); generateGrants(plPgSql, PERM_TO_ROLE);
plPgSql.ensureSingleEmptyLine();
}); });
} }
private Stream<RbacView.EntityAlias> referencedEntityAliases() { private Stream<RbacView.EntityAlias> referencedEntityAliases() {
return rbacDef.getEntityAliases().values().stream() return rbacDef.getEntityAliases().values().stream()
.filter((ea) -> !rbacDef.isRootEntityAlias(ea)) .filter(ea -> !rbacDef.isRootEntityAlias(ea))
.filter((ea) -> ea.fetchSql() != null); .filter(ea -> ea.dependsOnColum() != null)
.filter(ea -> ea.entityClass() != null)
.filter(ea -> ea.fetchSql() != null);
} }
private Stream<RbacView.EntityAlias> updatableEntityAliases() { private Stream<RbacView.EntityAlias> updatableEntityAliases() {
return referencedEntityAliases() return referencedEntityAliases()
.filter(ea -> rbacDef.getUpdatableColumns().contains(ea.dependsOnColum().column) ); .filter(ea -> rbacDef.getUpdatableColumns().contains(ea.dependsOnColum().column));
} }
private void generateUpdateRolesAndGrantsAfterUpdate(final StringWriter plPgSql) { private void generateUpdateRolesAndGrantsAfterUpdate(final StringWriter plPgSql) {
plPgSql.ensureEmptyLine(); plPgSql.ensureSingleEmptyLine();
plPgSql.writeLn("elsif TG_OP = 'UPDATE' then"); plPgSql.writeLn("elsif TG_OP = 'UPDATE' then");
plPgSql.indented(() -> { plPgSql.indented(() -> {
rbacDef.getEntityAliases().values().stream() updatableEntityAliases()
.filter(ea -> !rbacDef.isRootEntityAlias(ea)) .forEach((ea) -> plPgSql.writeLn(
.filter(ea -> ea.fetchSql() != null) ea.fetchSql().sql + " into " + entityRefVar(OLD, ea) + ";",
.forEach(ea -> { with("ref", OLD.name())));
plPgSql.writeLn(
ea.fetchSql().sql.replace("${ref}", OLD.name()) + " into " + entityRefVar(OLD, ea) + ";");
});
rbacDef.getEntityAliases().values().stream() updatableEntityAliases()
.map(RbacView.EntityAlias::dependsOnColum) .map(RbacView.EntityAlias::dependsOnColum)
.filter(Objects::nonNull)
.filter(this::isUpdatable)
.map(c -> c.column) .map(c -> c.column)
.sorted() .sorted()
.distinct() .distinct()
@ -182,34 +185,48 @@ class RolesGrantsAndPermissionsGenerator {
.filter(RbacView.RbacGrantDefinition::isToCreate) .filter(RbacView.RbacGrantDefinition::isToCreate)
.filter(g -> g.dependsOnColumn(columnName)) .filter(g -> g.dependsOnColumn(columnName))
.forEach(g -> { .forEach(g -> {
plPgSql.writeLn("-- TODO: revoke " + g); plPgSql.ensureSingleEmptyLine();
plPgSql.writeLn(generateRevoke(g));
plPgSql.writeLn(generateGrant(g)); plPgSql.writeLn(generateGrant(g));
plPgSql.writeLn();
}); });
} }
private void generateGrants(final StringWriter plPgSql, final RbacView.RbacGrantDefinition.GrantType grantType) { private void generateGrants(final StringWriter plPgSql, final RbacView.RbacGrantDefinition.GrantType grantType) {
plPgSql.ensureEmptyLine(); plPgSql.ensureSingleEmptyLine();
rbacGrants.stream() rbacGrants.stream()
.filter(g -> g.grantType() == grantType) .filter(g -> g.grantType() == grantType)
.map(this::generateGrant) .map(this::generateGrant)
.sorted() .sorted()
.forEach(plPgSql::writeLn); .forEach(text -> plPgSql.writeLn(text));
}
private String generateRevoke(RbacView.RbacGrantDefinition grantDef) {
return switch (grantDef.grantType()) {
case ROLE_TO_USER -> throw new IllegalArgumentException("unexpected grant");
case ROLE_TO_ROLE -> "call revokeRoleFromRole(${subRoleRef}, ${superRoleRef});"
.replace("${subRoleRef}", roleRef(OLD, grantDef.getSubRoleDef()))
.replace("${superRoleRef}", roleRef(OLD, grantDef.getSuperRoleDef()));
case PERM_TO_ROLE -> "call revokePermissionFromRole(${permRef}, ${superRoleRef});"
.replace("${permRef}", permRef(OLD, grantDef.getPermDef()))
.replace("${superRoleRef}", roleRef(OLD, grantDef.getSuperRoleDef()));
};
} }
private String generateGrant(RbacView.RbacGrantDefinition grantDef) { private String generateGrant(RbacView.RbacGrantDefinition grantDef) {
return switch (grantDef.grantType()) { return switch (grantDef.grantType()) {
case ROLE_TO_USER -> throw new IllegalArgumentException("unexpected grant"); case ROLE_TO_USER -> throw new IllegalArgumentException("unexpected grant");
case ROLE_TO_ROLE -> "call grantRoleToRole(${subRoleRef}, ${superRoleRef}));" case ROLE_TO_ROLE -> "call grantRoleToRole(${subRoleRef}, ${superRoleRef});"
.replace("${subRoleRef}", roleRef(NEW, grantDef.getSubRoleDef())) .replace("${subRoleRef}", roleRef(NEW, grantDef.getSubRoleDef()))
.replace("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef())); .replace("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef()));
case PERM_TO_ROLE -> "call grantPermissionsToRole(${permRef}, ${superRoleRef}));" case PERM_TO_ROLE -> "call grantPermissionsToRole(${permRef}, ${superRoleRef});"
.replace("${permRef}", permRef(NEW, grantDef.getPermDef())) .replace("${permRef}", permRef(NEW, grantDef.getPermDef()))
.replace("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef())); .replace("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef()));
}; };
} }
private String permRef(final PostgresTriggerReference ref, final RbacPermissionDefinition permDef) { private String permRef(final PostgresTriggerReference ref, final RbacPermissionDefinition permDef) {
return "createPermissions(${entityRef}.uuid, array ['${perm}']" return "createPermissions(${entityRef}.uuid, array ['${perm}'])"
.replace("${entityRef}", rbacDef.isRootEntityAlias(permDef.entityAlias) .replace("${entityRef}", rbacDef.isRootEntityAlias(permDef.entityAlias)
? ref.name() ? ref.name()
: refVarName(ref, permDef.entityAlias)) : refVarName(ref, permDef.entityAlias))
@ -232,10 +249,12 @@ class RolesGrantsAndPermissionsGenerator {
+ "(" + entityRefVar + ")"; + "(" + entityRefVar + ")";
} }
private static String entityRefVar( private String entityRefVar(
final PostgresTriggerReference rootRefVar, final PostgresTriggerReference rootRefVar,
final RbacView.EntityAlias entityAlias) { final RbacView.EntityAlias entityAlias) {
return rootRefVar.name().toLowerCase() + capitalize(entityAlias.aliasName()); return rbacDef.isRootEntityAlias(entityAlias)
? rootRefVar.name()
: rootRefVar.name().toLowerCase() + capitalize(entityAlias.aliasName());
} }
private void createRolesWithGrantsSql(final StringWriter plPgSql, final RbacView.Role role) { private void createRolesWithGrantsSql(final StringWriter plPgSql, final RbacView.Role role) {
@ -369,7 +388,7 @@ class RolesGrantsAndPermissionsGenerator {
private void generateInsertTrigger(final StringWriter plPgSql) { private void generateInsertTrigger(final StringWriter plPgSql) {
plPgSql.writeLn(""" plPgSql.writeLn("""
/* /*
An AFTER INSERT TRIGGER which creates the role structure for a new ${simpleEntityName} AFTER INSERT TRIGGER to create the role+grant structure for a new ${rawTableName} row.
*/ */
create or replace function insertTriggerFor${simpleEntityName}_tf() create or replace function insertTriggerFor${simpleEntityName}_tf()
@ -396,7 +415,7 @@ class RolesGrantsAndPermissionsGenerator {
private void generateUpdateTrigger(final StringWriter plPgSql) { private void generateUpdateTrigger(final StringWriter plPgSql) {
plPgSql.writeLn(""" plPgSql.writeLn("""
/* /*
An AFTER UPDATE TRIGGER which re-wires the grant structure for an updated ${simpleEntityName} AFTER INSERT TRIGGER to re-wire the grant structure for a new ${rawTableName} row.
*/ */
create or replace function updateTriggerFor${simpleEntityName}_tf() create or replace function updateTriggerFor${simpleEntityName}_tf()

View File

@ -2,6 +2,8 @@ package net.hostsharing.hsadminng.rbac.rbacdef;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import java.util.regex.Pattern;
import static java.util.Arrays.stream; import static java.util.Arrays.stream;
import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.joining;
@ -10,24 +12,22 @@ public class StringWriter {
private final StringBuilder string = new StringBuilder(); private final StringBuilder string = new StringBuilder();
private int indentLevel = 0; private int indentLevel = 0;
static VarDef with(final String var, final String name) {
return new VarDef(var, name);
}
void writeLn(final String text) { void writeLn(final String text) {
string.append( indented(text)); string.append( indented(text));
writeLn(); writeLn();
} }
void writeLn() { void writeLn(final String text, final VarDef... varDefs) {
string.append( "\n"); string.append( indented( new VarReplacer(varDefs).apply(text) ));
writeLn();
} }
private String indented(final String text) { void writeLn() {
if ( indentLevel == 0) { string.append( "\n");
return text;
}
final var indentation = StringUtils.repeat(" ", indentLevel);
final var indented = stream(text.split("\n"))
.map(line -> line.trim().isBlank() ? "" : indentation + line)
.collect(joining("\n"));
return indented;
} }
void indent() { void indent() {
@ -58,14 +58,46 @@ public class StringWriter {
}; };
} }
void ensureEmptyLine() { void ensureSingleEmptyLine() {
if (!string.toString().endsWith("\n\n")) { chopEmptyLines();
writeLn(); writeLn();
}
} }
@Override @Override
public String toString() { public String toString() {
return string.toString(); return string.toString();
} }
private String indented(final String text) {
if ( indentLevel == 0) {
return text;
}
final var indentation = StringUtils.repeat(" ", indentLevel);
final var indented = stream(text.split("\n"))
.map(line -> line.trim().isBlank() ? "" : indentation + line)
.collect(joining("\n"));
return indented;
}
record VarDef(String name, String value){}
private static final class VarReplacer {
private final VarDef[] varDefs;
private String text;
private VarReplacer(VarDef[] varDefs) {
this.varDefs = varDefs;
}
String apply(final String text) {
this.text = text;
stream(varDefs).forEach(varDef -> {
final var pattern = Pattern.compile("\\$\\{" + varDef.name() + "}", Pattern.CASE_INSENSITIVE);
final var matcher = pattern.matcher(text);
this.text = matcher.replaceAll(varDef.value());
});
return this.text;
}
}
} }