RBAC Diagram+PostgreSQL Generator #21
@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)));
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
@ -72,20 +72,25 @@ class RolesGrantsAndPermissionsGenerator {
|
|||||||
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.indented(() -> {
|
plPgSql.writeLn();
|
||||||
plPgSql.writeLn("begin");
|
plPgSql.writeLn("begin");
|
||||||
|
plPgSql.indented(() -> {
|
||||||
|
|
||||||
generateCreateRolesAndGrantsAfterInsert(plPgSql);
|
generateCreateRolesAndGrantsAfterInsert(plPgSql);
|
||||||
if (hasAnyUpdatableEntityAliases()) {
|
if (hasAnyUpdatableEntityAliases()) {
|
||||||
@ -96,7 +101,7 @@ 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();
|
||||||
@ -107,17 +112,17 @@ class RolesGrantsAndPermissionsGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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,13 +132,16 @@ 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() {
|
||||||
@ -142,23 +150,18 @@ class RolesGrantsAndPermissionsGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user