RBAC Diagram+PostgreSQL Generator #21
@ -12,6 +12,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.*;
|
||||
@ -72,4 +73,8 @@ public class HsOfficeBankAccountEntity implements HasUuid, Stringifyable {
|
||||
with.permission(VIEW);
|
||||
});
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
rbac().generateWithBaseFileName("243-hs-office-bankaccount-rbac");
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.hibernate.annotations.GenericGenerator;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL;
|
||||
@ -76,4 +77,8 @@ public class HsOfficeContactEntity implements Stringifyable, HasUuid {
|
||||
with.permission(VIEW);
|
||||
});
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
rbac().generateWithBaseFileName("203-hs-office-contact-rbac");
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.hibernate.annotations.GenericGenerator;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@ -123,7 +124,7 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable {
|
||||
.withUpdatableColumns(
|
||||
"debitorRel",
|
||||
"billable",
|
||||
"billingContactUuid",
|
||||
"debitorUuid",
|
||||
"refundBankAccountUuid",
|
||||
"vatId",
|
||||
"vatCountryCode",
|
||||
@ -144,7 +145,7 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable {
|
||||
.createPermission(VIEW).grantedTo("debitorRel", TENANT)
|
||||
|
||||
.importEntityAlias("refundBankAccount", HsOfficeBankAccountEntity.class,
|
||||
dependsOnColumn("bankAccountUuid"), fetchedBySql("""
|
||||
dependsOnColumn("refundBankAccountUuid"), fetchedBySql("""
|
||||
SELECT *
|
||||
FROM hs_office_relationship AS r
|
||||
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)
|
||||
|
||||
.importEntityAlias("partnerRel", HsOfficeRelationshipEntity.class,
|
||||
dependsOnColumn("debitorRelUuid"), fetchedBySql("""
|
||||
dependsOnColumn("partnerRelUuid"), fetchedBySql("""
|
||||
SELECT *
|
||||
FROM hs_office_relationship AS partnerRel
|
||||
WHERE ${debitorRel}.relAnchorUuid = partnerRel.relHolderUuid
|
||||
@ -168,4 +169,8 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable {
|
||||
.forExampleRole("operationalPerson", ADMIN).wouldBeGrantedTo("partnerRel", ADMIN)
|
||||
.forExampleRole("partnerRel", TENANT).wouldBeGrantedTo("partnerPerson", REFERRER);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
rbac().generateWithBaseFileName("273-hs-office-debitor-rbac");
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
|
||||
@ -100,4 +101,8 @@ public class HsOfficePartnerDetailsEntity implements HasUuid, Stringifyable {
|
||||
// not when anything in partner details changes.
|
||||
;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
rbac().generateWithBaseFileName("234-hs-office-partner-details-rbac");
|
||||
}
|
||||
}
|
||||
|
@ -108,6 +108,6 @@ public class HsOfficePartnerEntity implements Stringifyable, HasUuid {
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
HsOfficePartnerEntity.rbac().generateWithBaseFileName("233-hs-office-partner-rbac");
|
||||
rbac().generateWithBaseFileName("233-hs-office-partner-rbac");
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL;
|
||||
@ -80,4 +81,9 @@ public class HsOfficePersonEntity implements HasUuid, Stringifyable {
|
||||
with.permission(VIEW);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
rbac().generateWithBaseFileName("213-hs-office-person-rbac");
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
|
||||
@ -86,13 +87,16 @@ public class HsOfficeRelationshipEntity implements HasUuid, Stringifyable {
|
||||
"""))
|
||||
.withUpdatableColumns("contactUuid")
|
||||
.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,
|
||||
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,
|
||||
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) -> {
|
||||
with.owningUser(CREATOR);
|
||||
@ -115,4 +119,8 @@ public class HsOfficeRelationshipEntity implements HasUuid, Stringifyable {
|
||||
with.permission(VIEW);
|
||||
});
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
rbac().generateWithBaseFileName("223-hs-office-relationship-rbac");
|
||||
}
|
||||
}
|
||||
|
@ -123,6 +123,6 @@ public class HsOfficeSepaMandateEntity implements Stringifyable, HasUuid {
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
HsOfficeSepaMandateEntity.rbac().generateWithBaseFileName("253-hs-office-sepamandate-rbac");
|
||||
rbac().generateWithBaseFileName("253-hs-office-sepamandate-rbac");
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.hostsharing.hsadminng.rbac.rbacdef;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.Path;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@ -12,7 +13,9 @@ import jakarta.persistence.Table;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.lang.reflect.Modifier.isStatic;
|
||||
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.SQL.autoFetched;
|
||||
@ -57,7 +60,6 @@ public class RbacView {
|
||||
new RbacUserReference(CREATOR);
|
||||
entityAliases.put("global", new EntityAlias("global"));
|
||||
}
|
||||
|
||||
public RbacView withUpdatableColumns(final String... columnNames) {
|
||||
Collections.addAll(updatableColumns, columnNames);
|
||||
return this;
|
||||
@ -493,10 +495,11 @@ public class RbacView {
|
||||
return entityClass == null;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public SQL fetchSql() {
|
||||
if ( fetchSql == null ) {
|
||||
return null;
|
||||
return SQL.noop();
|
||||
}
|
||||
return switch (fetchSql.part) {
|
||||
case SQL_QUERY -> fetchSql;
|
||||
@ -505,6 +508,10 @@ public class RbacView {
|
||||
};
|
||||
}
|
||||
|
||||
public boolean hasFetchSql() {
|
||||
return fetchSql != null;
|
||||
}
|
||||
|
||||
private String withoutEntitySuffix(final String simpleEntityName) {
|
||||
return simpleEntityName.substring(0, simpleEntityName.length()-"Entity".length());
|
||||
}
|
||||
@ -583,6 +590,15 @@ public class RbacView {
|
||||
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.
|
||||
*
|
||||
* @param sql an SQL SELECT expression (not ending with ';)
|
||||
@ -604,8 +620,10 @@ public class RbacView {
|
||||
}
|
||||
|
||||
enum Part {
|
||||
NOOP,
|
||||
SQL_QUERY,
|
||||
AUTO_FETCH, SQL_PROJECTION
|
||||
AUTO_FETCH,
|
||||
SQL_PROJECTION
|
||||
}
|
||||
|
||||
final String sql;
|
||||
@ -668,4 +686,35 @@ public class RbacView {
|
||||
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());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ public class RbacViewMermaidFlowchart {
|
||||
|
||||
private void wrapOutputInSubgraph(final String name, final String color, final String content) {
|
||||
if (!StringUtils.isEmpty(content)) {
|
||||
flowchart.ensureEmptyLine();
|
||||
flowchart.ensureSingleEmptyLine();
|
||||
flowchart.writeLn("subgraph " + name + "[ ]\n");
|
||||
flowchart.indented(() -> {
|
||||
flowchart.writeLn("style %{aliasName} fill:%{fillColor},stroke:white"
|
||||
@ -102,7 +102,7 @@ public class RbacViewMermaidFlowchart {
|
||||
.filter(g -> g.grantType() == f)
|
||||
.toList();
|
||||
if ( !userGrants.isEmpty()) {
|
||||
flowchart.ensureEmptyLine();
|
||||
flowchart.ensureSingleEmptyLine();
|
||||
flowchart.writeLn(t);
|
||||
userGrants.forEach(g -> flowchart.writeLn(grantDef(g)));
|
||||
}
|
||||
|
@ -8,6 +8,8 @@ import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
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 {
|
||||
|
||||
@ -21,10 +23,11 @@ public class RbacViewPostgresGenerator {
|
||||
liqibaseTagPrefix = rbacDef.getRootEntityAlias().entityClass().getSimpleName();
|
||||
plPgSql.writeLn("""
|
||||
--liquibase formatted sql
|
||||
-- This code generated was by ${generator} at %{timestamp}.
|
||||
"""
|
||||
.replace("${generator}", getClass().getSimpleName())
|
||||
.replace("%{timestamp}", LocalDateTime.now().toString()));
|
||||
-- This code generated was by ${generator} at ${timestamp}.
|
||||
""",
|
||||
with("generator", getClass().getSimpleName()),
|
||||
with("timestamp", LocalDateTime.now().toString()),
|
||||
with("ref", NEW.name()));
|
||||
|
||||
new RolesGrantsAndPermissionsGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql);
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacPermissionDefinition;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
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.Role.*;
|
||||
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.uncapitalize;
|
||||
|
||||
@ -72,20 +72,25 @@ class RolesGrantsAndPermissionsGenerator {
|
||||
NEW ${rawTableName}
|
||||
)
|
||||
language plpgsql as $$
|
||||
|
||||
declare
|
||||
"""
|
||||
.replace("${simpleEntityName}", simpleEntityName)
|
||||
.replace("${rawTableName}", rawTableName));
|
||||
|
||||
plPgSql.chopEmptyLines();
|
||||
plPgSql.indented(() -> {
|
||||
|
||||
referencedEntityAliases()
|
||||
.forEach((ea) -> plPgSql.writeLn(entityRefVar(NEW, ea) + " " + getRawTableName(ea.entityClass()) + ";"));
|
||||
|
||||
updatableEntityAliases()
|
||||
.forEach((ea) -> {
|
||||
plPgSql.writeLn(entityRefVar(NEW, ea) + " " + getRawTableName(ea.entityClass()) + ";");
|
||||
});
|
||||
.forEach((ea) -> plPgSql.writeLn(entityRefVar(OLD, ea) + " " + getRawTableName(ea.entityClass()) + ";"));
|
||||
});
|
||||
|
||||
plPgSql.writeLn();
|
||||
plPgSql.writeLn("begin");
|
||||
plPgSql.indented(() -> {
|
||||
plPgSql.writeLn("begin");
|
||||
|
||||
generateCreateRolesAndGrantsAfterInsert(plPgSql);
|
||||
if (hasAnyUpdatableEntityAliases()) {
|
||||
@ -96,28 +101,28 @@ class RolesGrantsAndPermissionsGenerator {
|
||||
raise exception 'invalid usage of TRIGGER';
|
||||
end if;
|
||||
""");
|
||||
plPgSql.ensureEmptyLine();
|
||||
plPgSql.ensureSingleEmptyLine();
|
||||
});
|
||||
plPgSql.writeLn("end; $$;");
|
||||
plPgSql.writeLn();
|
||||
}
|
||||
|
||||
private boolean hasAnyUpdatableEntityAliases() {
|
||||
return updatableEntityAliases().anyMatch(e -> true);
|
||||
return updatableEntityAliases().anyMatch(e -> true);
|
||||
}
|
||||
|
||||
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.indented(() -> {
|
||||
|
||||
updatableEntityAliases()
|
||||
.forEach((ea) -> {
|
||||
plPgSql.writeLn(
|
||||
ea.fetchSql().sql.replace("${ref}", NEW.name()) + " into " + entityRefVar(NEW, ea) + ";");
|
||||
});
|
||||
|
||||
plPgSql.chopEmptyLines();
|
||||
createRolesWithGrantsSql(plPgSql, OWNER);
|
||||
createRolesWithGrantsSql(plPgSql, ADMIN);
|
||||
createRolesWithGrantsSql(plPgSql, AGENT);
|
||||
@ -127,38 +132,36 @@ class RolesGrantsAndPermissionsGenerator {
|
||||
generateGrants(plPgSql, ROLE_TO_USER);
|
||||
generateGrants(plPgSql, ROLE_TO_ROLE);
|
||||
generateGrants(plPgSql, PERM_TO_ROLE);
|
||||
plPgSql.ensureSingleEmptyLine();
|
||||
});
|
||||
}
|
||||
|
||||
private Stream<RbacView.EntityAlias> referencedEntityAliases() {
|
||||
return rbacDef.getEntityAliases().values().stream()
|
||||
.filter((ea) -> !rbacDef.isRootEntityAlias(ea))
|
||||
.filter((ea) -> ea.fetchSql() != null);
|
||||
.filter(ea -> !rbacDef.isRootEntityAlias(ea))
|
||||
.filter(ea -> ea.dependsOnColum() != null)
|
||||
.filter(ea -> ea.entityClass() != null)
|
||||
.filter(ea -> ea.fetchSql() != null);
|
||||
}
|
||||
|
||||
private Stream<RbacView.EntityAlias> updatableEntityAliases() {
|
||||
return referencedEntityAliases()
|
||||
.filter(ea -> rbacDef.getUpdatableColumns().contains(ea.dependsOnColum().column) );
|
||||
.filter(ea -> rbacDef.getUpdatableColumns().contains(ea.dependsOnColum().column));
|
||||
}
|
||||
|
||||
private void generateUpdateRolesAndGrantsAfterUpdate(final StringWriter plPgSql) {
|
||||
plPgSql.ensureEmptyLine();
|
||||
plPgSql.ensureSingleEmptyLine();
|
||||
plPgSql.writeLn("elsif TG_OP = 'UPDATE' then");
|
||||
|
||||
plPgSql.indented(() -> {
|
||||
|
||||
rbacDef.getEntityAliases().values().stream()
|
||||
.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) + ";");
|
||||
});
|
||||
updatableEntityAliases()
|
||||
.forEach((ea) -> plPgSql.writeLn(
|
||||
ea.fetchSql().sql + " into " + entityRefVar(OLD, ea) + ";",
|
||||
with("ref", OLD.name())));
|
||||
|
||||
rbacDef.getEntityAliases().values().stream()
|
||||
updatableEntityAliases()
|
||||
.map(RbacView.EntityAlias::dependsOnColum)
|
||||
.filter(Objects::nonNull)
|
||||
.filter(this::isUpdatable)
|
||||
.map(c -> c.column)
|
||||
.sorted()
|
||||
.distinct()
|
||||
@ -182,34 +185,48 @@ class RolesGrantsAndPermissionsGenerator {
|
||||
.filter(RbacView.RbacGrantDefinition::isToCreate)
|
||||
.filter(g -> g.dependsOnColumn(columnName))
|
||||
.forEach(g -> {
|
||||
plPgSql.writeLn("-- TODO: revoke " + g);
|
||||
plPgSql.ensureSingleEmptyLine();
|
||||
plPgSql.writeLn(generateRevoke(g));
|
||||
plPgSql.writeLn(generateGrant(g));
|
||||
plPgSql.writeLn();
|
||||
});
|
||||
}
|
||||
|
||||
private void generateGrants(final StringWriter plPgSql, final RbacView.RbacGrantDefinition.GrantType grantType) {
|
||||
plPgSql.ensureEmptyLine();
|
||||
plPgSql.ensureSingleEmptyLine();
|
||||
rbacGrants.stream()
|
||||
.filter(g -> g.grantType() == grantType)
|
||||
.map(this::generateGrant)
|
||||
.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) {
|
||||
return switch (grantDef.grantType()) {
|
||||
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("${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("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef()));
|
||||
};
|
||||
}
|
||||
|
||||
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)
|
||||
? ref.name()
|
||||
: refVarName(ref, permDef.entityAlias))
|
||||
@ -232,10 +249,12 @@ class RolesGrantsAndPermissionsGenerator {
|
||||
+ "(" + entityRefVar + ")";
|
||||
}
|
||||
|
||||
private static String entityRefVar(
|
||||
private String entityRefVar(
|
||||
final PostgresTriggerReference rootRefVar,
|
||||
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) {
|
||||
@ -369,7 +388,7 @@ class RolesGrantsAndPermissionsGenerator {
|
||||
private void generateInsertTrigger(final StringWriter plPgSql) {
|
||||
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()
|
||||
@ -396,7 +415,7 @@ class RolesGrantsAndPermissionsGenerator {
|
||||
private void generateUpdateTrigger(final StringWriter plPgSql) {
|
||||
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()
|
||||
|
@ -2,6 +2,8 @@ package net.hostsharing.hsadminng.rbac.rbacdef;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
|
||||
@ -10,24 +12,22 @@ public class StringWriter {
|
||||
private final StringBuilder string = new StringBuilder();
|
||||
private int indentLevel = 0;
|
||||
|
||||
static VarDef with(final String var, final String name) {
|
||||
return new VarDef(var, name);
|
||||
}
|
||||
|
||||
void writeLn(final String text) {
|
||||
string.append( indented(text));
|
||||
writeLn();
|
||||
}
|
||||
|
||||
void writeLn() {
|
||||
string.append( "\n");
|
||||
void writeLn(final String text, final VarDef... varDefs) {
|
||||
string.append( indented( new VarReplacer(varDefs).apply(text) ));
|
||||
writeLn();
|
||||
}
|
||||
|
||||
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;
|
||||
void writeLn() {
|
||||
string.append( "\n");
|
||||
}
|
||||
|
||||
void indent() {
|
||||
@ -58,14 +58,46 @@ public class StringWriter {
|
||||
};
|
||||
}
|
||||
|
||||
void ensureEmptyLine() {
|
||||
if (!string.toString().endsWith("\n\n")) {
|
||||
writeLn();
|
||||
}
|
||||
void ensureSingleEmptyLine() {
|
||||
chopEmptyLines();
|
||||
writeLn();
|
||||
}
|
||||
|
||||
@Override
|
||||
public 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user