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 227 additions and 121 deletions
Showing only changes of commit dff9803dc3 - Show all commits

View File

@ -58,7 +58,7 @@ public class HsOfficeBankAccountEntity implements HasUuid, Stringifyable {
public static RbacView rbac() { public static RbacView rbac() {
return rbacViewFor("bankAccount", HsOfficeBankAccountEntity.class) return rbacViewFor("bankAccount", HsOfficeBankAccountEntity.class)
.withIdentityView(SQL.query("target.iban || ':' || target.holder")) .withIdentityView(SQL.projection("iban || ':' || holder"))
.withUpdatableColumns("holder", "iban", "bic") .withUpdatableColumns("holder", "iban", "bic")
.createRole(OWNER, (with) -> { .createRole(OWNER, (with) -> {
with.owningUser(CREATOR); with.owningUser(CREATOR);

View File

@ -5,6 +5,7 @@ import lombok.experimental.FieldNameConstants;
import net.hostsharing.hsadminng.errors.DisplayName; import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.persistence.HasUuid; import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable; import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.GenericGenerator; import org.hibernate.annotations.GenericGenerator;
@ -61,7 +62,7 @@ public class HsOfficeContactEntity implements Stringifyable, HasUuid {
public static RbacView rbac() { public static RbacView rbac() {
return rbacViewFor("contact", HsOfficeContactEntity.class) return rbacViewFor("contact", HsOfficeContactEntity.class)
.withIdentityView(RbacView.SQL.query("target.label")) .withIdentityView(SQL.projection("label"))
.withUpdatableColumns("label", "postalAddress", "emailAddresses", "phoneNumbers") .withUpdatableColumns("label", "postalAddress", "emailAddresses", "phoneNumbers")
.createRole(OWNER, (with) -> { .createRole(OWNER, (with) -> {
with.owningUser(CREATOR); with.owningUser(CREATOR);

View File

@ -144,22 +144,22 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable {
.createPermission(VIEW).grantedTo("debitorRel", TENANT) .createPermission(VIEW).grantedTo("debitorRel", TENANT)
.importEntityAlias("refundBankAccount", HsOfficeBankAccountEntity.class, .importEntityAlias("refundBankAccount", HsOfficeBankAccountEntity.class,
fetchedBySql(""" dependsOnColumn("bankAccountUuid"), 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
"""), """)
dependsOnColumn("bankAccountUuid")) )
.toRole("refundBankAccount", ADMIN).grantRole("debitorRel", AGENT) .toRole("refundBankAccount", ADMIN).grantRole("debitorRel", AGENT)
.toRole("debitorRel", AGENT).grantRole("refundBankAccount", REFERRER) .toRole("debitorRel", AGENT).grantRole("refundBankAccount", REFERRER)
.importEntityAlias("partnerRel", HsOfficeRelationshipEntity.class, .importEntityAlias("partnerRel", HsOfficeRelationshipEntity.class,
fetchedBySql(""" dependsOnColumn("debitorRelUuid"), 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
"""), """)
dependsOnColumn("debitorRelUuid")) )
.toRole("partnerRel", ADMIN).grantRole("debitorRel", ADMIN) .toRole("partnerRel", ADMIN).grantRole("debitorRel", ADMIN)
.toRole("partnerRel", AGENT).grantRole("debitorRel", AGENT) .toRole("partnerRel", AGENT).grantRole("debitorRel", AGENT)
.toRole("debitorRel", AGENT).grantRole("partnerRel", TENANT) .toRole("debitorRel", AGENT).grantRole("partnerRel", TENANT)

View File

@ -5,6 +5,7 @@ import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity; import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity;
import net.hostsharing.hsadminng.persistence.HasUuid; import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable; import net.hostsharing.hsadminng.stringify.Stringifyable;
@ -69,7 +70,7 @@ public class HsOfficePartnerDetailsEntity implements HasUuid, Stringifyable {
public static RbacView rbac() { public static RbacView rbac() {
return rbacViewFor("partnerDetails", HsOfficePartnerDetailsEntity.class) return rbacViewFor("partnerDetails", HsOfficePartnerDetailsEntity.class)
.withIdentityView(RbacView.SQL.query(""" .withIdentityView(SQL.query("""
SELECT partner_iv.idName || '-details' SELECT partner_iv.idName || '-details'
FROM hs_office_partner_details AS partnerDetails FROM hs_office_partner_details AS partnerDetails
JOIN hs_office_partner partner ON partner.detailsUuid = partnerDetails.uuid JOIN hs_office_partner partner ON partner.detailsUuid = partnerDetails.uuid

View File

@ -7,8 +7,7 @@ import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity; import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacViewMermaidFlowchart; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacViewPostgresGenerator;
import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable; import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.NotFound; import org.hibernate.annotations.NotFound;
@ -81,11 +80,11 @@ public class HsOfficePartnerEntity implements Stringifyable, HasUuid {
public static RbacView rbac() { public static RbacView rbac() {
return rbacViewFor("partner", HsOfficePartnerEntity.class) return rbacViewFor("partner", HsOfficePartnerEntity.class)
.withIdentityView(RbacView.SQL.query(""" .withIdentityView(SQL.query("""
SELECT partner.partnerNumber SELECT partner.partnerNumber
|| ':' || (SELECT idName FROM hs_office_person_iv p WHERE p.uuid = partner.personuuid) || ':' || (SELECT idName FROM hs_office_person_iv p WHERE p.uuid = partner.personUuid)
|| '-' || (SELECT idName FROM hs_office_contact_iv c WHERE c.uuid = partner.contactuuid) || '-' || (SELECT idName FROM hs_office_contact_iv c WHERE c.uuid = partner.contactUuid)
FROM hs_office_partner AD partner FROM hs_office_partner AS partner
""")) """))
.withUpdatableColumns( .withUpdatableColumns(
"partnerRoleUuid", "partnerRoleUuid",
@ -109,9 +108,6 @@ public class HsOfficePartnerEntity implements Stringifyable, HasUuid {
} }
public static void main(String[] args) throws IOException { public static void main(String[] args) throws IOException {
final RbacView rbac = HsOfficePartnerEntity.rbac(); HsOfficePartnerEntity.rbac().generateWithBaseFileName("233-hs-office-partner-rbac");
new RbacViewMermaidFlowchart(rbac).generateToMarkdownFile();
new RbacViewPostgresGenerator(rbac).generateToChangeLog("233-hs-office-partner-rbac.sql");
} }
} }

View File

@ -16,7 +16,7 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*;
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.Role.*; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.query; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.projection;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify; import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@ -66,7 +66,7 @@ public class HsOfficePersonEntity implements HasUuid, Stringifyable {
public static RbacView rbac() { public static RbacView rbac() {
return rbacViewFor("person", HsOfficePersonEntity.class) return rbacViewFor("person", HsOfficePersonEntity.class)
.withIdentityView(query("concat(target.tradeName, target.familyName, target.givenName)")) .withIdentityView(projection("concat(tradeName, familyName, givenName)"))
.withUpdatableColumns("personType", "tradeName", "givenName", "familyName") .withUpdatableColumns("personType", "tradeName", "givenName", "familyName")
.createRole(OWNER, (with) -> { .createRole(OWNER, (with) -> {
with.permission(ALL); with.permission(ALL);

View File

@ -79,21 +79,21 @@ public class HsOfficeRelationshipEntity implements HasUuid, Stringifyable {
public static RbacView rbac() { public static RbacView rbac() {
return rbacViewFor("relationship", HsOfficeRelationshipEntity.class) return rbacViewFor("relationship", HsOfficeRelationshipEntity.class)
.withIdentityView(SQL.query(""" .withIdentityView(SQL.projection("""
(select idName from hs_office_person_iv p where p.uuid = target.relAnchorUuid) (select idName from hs_office_person_iv p where p.uuid = relAnchorUuid)
|| '-with-' || target.relType || '-' || '-with-' || target.relType || '-'
|| (select idName from hs_office_person_iv p where p.uuid = target.relHolderUuid) || (select idName from hs_office_person_iv p where p.uuid = relHolderUuid)
""")) """))
.withUpdatableColumns("contactUuid") .withUpdatableColumns("contactUuid")
.importEntityAlias("anchorPerson", HsOfficePersonEntity.class, .importEntityAlias("anchorPerson", HsOfficePersonEntity.class,
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")
dependsOnColumn("relAnchorUuid")) )
.importEntityAlias("holderPerson", HsOfficePersonEntity.class, .importEntityAlias("holderPerson", HsOfficePersonEntity.class,
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")
dependsOnColumn("relHolderUuid")) )
.importEntityAlias("contact", HsOfficeContactEntity.class, .importEntityAlias("contact", HsOfficeContactEntity.class,
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")
dependsOnColumn("contactUuid")) )
.createRole(OWNER, (with) -> { .createRole(OWNER, (with) -> {
with.owningUser(CREATOR); with.owningUser(CREATOR);
with.incomingSuperRole(GLOBAL, ADMIN); with.incomingSuperRole(GLOBAL, ADMIN);

View File

@ -6,16 +6,26 @@ import lombok.*;
import net.hostsharing.hsadminng.errors.DisplayName; import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity; import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity;
import net.hostsharing.hsadminng.persistence.HasUuid; import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable; import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.Type; import org.hibernate.annotations.Type;
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;
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.*; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify; import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity @Entity
@ -84,4 +94,33 @@ public class HsOfficeSepaMandateEntity implements Stringifyable, HasUuid {
return reference; return reference;
} }
public static RbacView rbac() {
return rbacViewFor("sepaMandate", HsOfficeSepaMandateEntity.class)
.withIdentityView(projection("concat(tradeName, familyName, givenName)"))
.withUpdatableColumns("reference", "agreement", "validity")
.importEntityAlias("debitorRel", HsOfficeRelationshipEntity.class, dependsOnColumn("debitorRelUuid"))
.importEntityAlias("bankAccount", HsOfficeBankAccountEntity.class, dependsOnColumn("bankAccountUuid"))
.createRole(OWNER, (with) -> {
with.owningUser(CREATOR);
with.incomingSuperRole(GLOBAL, ADMIN);
with.outgoingSubRole("bankAccount", REFERRER);
with.permission(ALL);
})
.createSubRole(ADMIN, (with) -> {
with.permission(EDIT);
})
.createSubRole(AGENT, (with) -> {
with.outgoingSubRole("debitorRel", AGENT);
})
.createSubRole(REFERRER, (with) -> {
with.incomingSuperRole("debitorRel", AGENT);
with.permission(VIEW);
});
}
public static void main(String[] args) throws IOException {
HsOfficeSepaMandateEntity.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.nio.file.Path;
import java.util.function.Consumer; import java.util.function.Consumer;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@ -7,17 +8,21 @@ import lombok.Getter;
import net.hostsharing.hsadminng.persistence.HasUuid; import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
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 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 org.apache.commons.lang3.StringUtils.uncapitalize; import static org.apache.commons.lang3.StringUtils.uncapitalize;
@Getter @Getter
public class RbacView { public class RbacView {
public static final String GLOBAL = "global"; public static final String GLOBAL = "global";
public static final String OUTPUT_BASEDIR = "src/main/resources/db/changelog";
private final EntityAlias rootEntityAlias; private final EntityAlias rootEntityAlias;
@ -123,11 +128,18 @@ public class RbacView {
public RbacView importEntityAlias( public RbacView importEntityAlias(
final String aliasName, final Class<? extends HasUuid> entityClass, final String aliasName, final Class<? extends HasUuid> entityClass,
final SQL fetchSql, final Column dependsOnColum) { final Column dependsOnColum, final SQL fetchSql) {
importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, false); importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, false);
return this; return this;
} }
public RbacView importEntityAlias(
final String aliasName, final Class<? extends HasUuid> entityClass,
final Column dependsOnColum) {
importEntityAliasImpl(aliasName, entityClass, autoFetched(), dependsOnColum, false);
return this;
}
private EntityAlias importEntityAliasImpl( private EntityAlias importEntityAliasImpl(
final String aliasName, final Class<? extends HasUuid> entityClass, final String aliasName, final Class<? extends HasUuid> entityClass,
final SQL fetchSql, final Column dependsOnColum, boolean asSubEntity) { final SQL fetchSql, final Column dependsOnColum, boolean asSubEntity) {
@ -200,6 +212,11 @@ public class RbacView {
return entityAlias == rootEntityAliasProxy; return entityAlias == rootEntityAliasProxy;
} }
public void generateWithBaseFileName(final String baseFileName) {
new RbacViewMermaidFlowchart(this).generateToMarkdownFile(Path.of(OUTPUT_BASEDIR, baseFileName + "-generated.md"));
new RbacViewPostgresGenerator(this).generateToChangeLog(Path.of(OUTPUT_BASEDIR, baseFileName + "-generated.sql"));
}
public class RbacGrantBuilder { public class RbacGrantBuilder {
private final RbacRoleDefinition superRoleDef; private final RbacRoleDefinition superRoleDef;
@ -463,6 +480,18 @@ public class RbacView {
return entityClass == null; return entityClass == null;
} }
@Override
public SQL fetchSql() {
if ( fetchSql == null ) {
return null;
}
return switch (fetchSql.part) {
case SQL_QUERY -> fetchSql;
case AUTO_FETCH -> SQL.query("SELECT * FROM " + getRawTableName(entityClass) + " WHERE uuid = ${ref}." + dependsOnColum.column);
default -> throw new IllegalStateException("unexpected SQL definition: " + fetchSql);
};
}
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());
} }
@ -474,6 +503,13 @@ public class RbacView {
} }
} }
public static String getRawTableName(final Class<?> entityClass) {
return withoutRvSuffix(entityClass.getAnnotation(Table.class).name());
}
public static String withoutRvSuffix(final String tableName) {
return tableName.substring(0, tableName.length()-"_rv".length());
}
public record Role(String roleName) { public record Role(String roleName) {
public static final Role OWNER = new Role("owner"); public static final Role OWNER = new Role("owner");
public static final Role ADMIN = new Role("admin"); public static final Role ADMIN = new Role("admin");
@ -510,7 +546,7 @@ public class RbacView {
public static class SQL { public static class SQL {
/** /**
* DSL methid to specify an SQL SELECT expression which fetches the related entity, * DSL method to specify an SQL SELECT expression which fetches the related entity,
* using the reference `${ref}` of the root entity. * using the reference `${ref}` of the root entity.
* `${ref}` is going to be replaced by either `NEW` or `OLD` of the trigger function. * `${ref}` is going to be replaced by either `NEW` or `OLD` of the trigger function.
* `into ...` will be added with a variable name prefixed with either `new` or `old`. * `into ...` will be added with a variable name prefixed with either `new` or `old`.
@ -520,7 +556,18 @@ public class RbacView {
*/ */
public static SQL fetchedBySql(final String sql) { public static SQL fetchedBySql(final String sql) {
validateExpression(sql); validateExpression(sql);
return new SQL(sql); return new SQL(sql, Part.SQL_QUERY);
}
/**
* DSL method to specify that a related entity is to be fetched by a simple SELECT statement
* using the raw table from the @Table statement of the entity to fetch
* and the dependent column of the root entity.
*
* @return the wrapped SQL definition object
*/
public static SQL autoFetched() {
return new SQL(null, Part.AUTO_FETCH);
} }
/** Generic DSL method to specify an SQL SELECT expression. /** Generic DSL method to specify an SQL SELECT expression.
@ -530,13 +577,39 @@ public class RbacView {
*/ */
public static SQL query(final String sql) { public static SQL query(final String sql) {
validateExpression(sql); validateExpression(sql);
return new SQL(sql); return new SQL(sql, Part.SQL_QUERY);
} }
public final String sql; /** Generic DSL method to specify an SQL SELECT expression by just the projection part.
*
* @param projection an SQL SELECT expression, the list of columns after 'SELECT'
* @return the wrapped SQL projection
*/
public static SQL projection(final String projection) {
validateProjection(projection);
return new SQL(projection, Part.SQL_PROJECTION);
}
private SQL(final String sql) { enum Part {
SQL_QUERY,
AUTO_FETCH, SQL_PROJECTION
}
final String sql;
final Part part;
private SQL(final String sql, final Part part) {
this.sql = sql; this.sql = sql;
this.part = part;
}
private static void validateProjection(final String projection) {
if (projection.toUpperCase().matches("[ \t]*$SELECT[ \t]")) {
throw new IllegalArgumentException("SQL projection must not start with 'SELECT': " + projection);
}
if (projection.matches(";[ \t]*$")) {
throw new IllegalArgumentException("SQL projection must not end with ';': " + projection);
}
} }
private static void validateExpression(final String sql) { private static void validateExpression(final String sql) {

View File

@ -1,13 +1,8 @@
package net.hostsharing.hsadminng.rbac.rbacdef; package net.hostsharing.hsadminng.rbac.rbacdef;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity; import lombok.SneakyThrows;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerDetailsEntity;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.nio.file.*; import java.nio.file.*;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@ -46,10 +41,11 @@ public class RbacViewMermaidFlowchart {
flowchart.writeLn(""" flowchart.writeLn("""
subgraph %{aliasName}["`**%{aliasName}**`"] subgraph %{aliasName}["`**%{aliasName}**`"]
direction TB direction TB
style %{aliasName} fill:%{color},stroke:darkblue,stroke-width:8px style %{aliasName} fill:%{fillColor},stroke:%{strokeColor},stroke-width:8px
""" """
.replace("%{aliasName}", entity.aliasName()) .replace("%{aliasName}", entity.aliasName())
.replace("%{color}", color )); .replace("%{fillColor}", color )
.replace("%{strokeColor}", HOSTSHARING_DARK_BLUE ));
flowchart.indented( () -> { flowchart.indented( () -> {
rbacDef.getEntityAliases().values().stream() rbacDef.getEntityAliases().values().stream()
@ -83,9 +79,9 @@ public class RbacViewMermaidFlowchart {
flowchart.ensureEmptyLine(); flowchart.ensureEmptyLine();
flowchart.writeLn("subgraph " + name + "[ ]\n"); flowchart.writeLn("subgraph " + name + "[ ]\n");
flowchart.indented(() -> { flowchart.indented(() -> {
flowchart.writeLn("style %{aliasName} fill: %{color}" flowchart.writeLn("style %{aliasName} fill:%{fillColor},stroke:white"
.replace("%{aliasName}", name) .replace("%{aliasName}", name)
.replace("%{color}", color)); .replace("%{fillColor}", color));
flowchart.writeLn(); flowchart.writeLn();
flowchart.writeLn(content); flowchart.writeLn(content);
}); });
@ -147,8 +143,8 @@ public class RbacViewMermaidFlowchart {
return flowchart.toString(); return flowchart.toString();
} }
public void generateToMarkdownFile() throws IOException { @SneakyThrows
final Path path = Paths.get("doc", rbacDef.getRootEntityAlias().simpleName() + ".md"); public void generateToMarkdownFile(final Path path) {
Files.writeString( Files.writeString(
path, path,
""" """
@ -164,12 +160,4 @@ public class RbacViewMermaidFlowchart {
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
System.out.println("Markdown-File: " + path.toAbsolutePath()); System.out.println("Markdown-File: " + path.toAbsolutePath());
} }
public static void main(String[] args) throws IOException {
new RbacViewMermaidFlowchart(HsOfficeBankAccountEntity.rbac()).generateToMarkdownFile();
new RbacViewMermaidFlowchart(HsOfficeRelationshipEntity.rbac()).generateToMarkdownFile();
new RbacViewMermaidFlowchart(HsOfficePartnerEntity.rbac()).generateToMarkdownFile();
new RbacViewMermaidFlowchart(HsOfficePartnerDetailsEntity.rbac()).generateToMarkdownFile();
new RbacViewMermaidFlowchart(HsOfficeDebitorEntity.rbac()).generateToMarkdownFile();
}
} }

View File

@ -1,10 +1,7 @@
package net.hostsharing.hsadminng.rbac.rbacdef; package net.hostsharing.hsadminng.rbac.rbacdef;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; import lombok.SneakyThrows;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity;
import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
@ -37,7 +34,8 @@ public class RbacViewPostgresGenerator {
return plPgSql.toString(); return plPgSql.toString();
} }
private static void generatePostgres(final RbacView rbac) throws IOException { @SneakyThrows
private static void generatePostgres(final RbacView rbac) {
final Path outputPath = Paths.get("doc", rbac.getRootEntityAlias().simpleName() + ".sql"); final Path outputPath = Paths.get("doc", rbac.getRootEntityAlias().simpleName() + ".sql");
Files.writeString( Files.writeString(
outputPath, outputPath,
@ -47,8 +45,8 @@ public class RbacViewPostgresGenerator {
System.out.println(outputPath.toAbsolutePath()); System.out.println(outputPath.toAbsolutePath());
} }
public void generateToChangeLog(final String fileName) throws IOException { @SneakyThrows
final Path outputPath = Path.of("src/main/resources/db/changelog", fileName); public void generateToChangeLog(final Path outputPath) {
Files.writeString( Files.writeString(
outputPath, outputPath,
toString(), toString(),
@ -56,10 +54,4 @@ public class RbacViewPostgresGenerator {
StandardOpenOption.TRUNCATE_EXISTING); StandardOpenOption.TRUNCATE_EXISTING);
System.out.println(outputPath.toAbsolutePath()); System.out.println(outputPath.toAbsolutePath());
} }
public static void main(String[] args) throws IOException {
generatePostgres(HsOfficeRelationshipEntity.rbac());
generatePostgres(HsOfficePartnerEntity.rbac());
generatePostgres(HsOfficeDebitorEntity.rbac());
}
} }

View File

@ -2,14 +2,15 @@ package net.hostsharing.hsadminng.rbac.rbacdef;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacPermissionDefinition; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacPermissionDefinition;
import jakarta.persistence.Table;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
import static java.util.stream.Collectors.*; import static java.util.stream.Collectors.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.NEW; 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.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 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;
@ -141,10 +142,6 @@ class RolesGrantsAndPermissionsGenerator {
return ref.name().toLowerCase() + capitalize(entityAlias.aliasName()); return ref.name().toLowerCase() + capitalize(entityAlias.aliasName());
} }
private String getRawTableName(final Class<?> entityClass) {
return withoutRvSuffix(entityClass.getAnnotation(Table.class).name());
}
private String roleRef(final PostgresTriggerReference rootRefVar, final RbacView.RbacRoleDefinition roleDef) { private String roleRef(final PostgresTriggerReference rootRefVar, final RbacView.RbacRoleDefinition roleDef) {
if ( roleDef == null ) { if ( roleDef == null ) {
System.out.println("null"); System.out.println("null");
@ -179,51 +176,13 @@ class RolesGrantsAndPermissionsGenerator {
.replace("${simpleVarName)", simpleEntityVarName) .replace("${simpleVarName)", simpleEntityVarName)
.replace("${roleSuffix}", capitalize(role.roleName()))); .replace("${roleSuffix}", capitalize(role.roleName())));
final var permissionGrantsForRole = findPermissionsGrantsForRole(rbacDef.getRootEntityAlias(), role); generatePermissionsForRole(plPgSql, role);
if (!permissionGrantsForRole.isEmpty()) {
final var permissionsForRoleInPlPgSql = permissionGrantsForRole.stream()
.map(RbacView.RbacGrantDefinition::getPermDef)
.map(RbacPermissionDefinition::getPermission)
.map(RbacView.Permission::permission)
.map(p -> "'" + p + "'")
.collect(joining(", "));
plPgSql.indented( () ->
plPgSql.writeLn("permissions => array[" + permissionsForRoleInPlPgSql + "],\n"));
rbacGrants.removeAll(permissionGrantsForRole);
}
final var grantsToUsers = findGrantsToUserForRole(rbacDef.getRootEntityAlias(), role); generateUserGrantsForRole(plPgSql, role);
if (!grantsToUsers.isEmpty()) {
final var grantsToUsersPlPgSql = grantsToUsers.stream()
.map(RbacView.RbacGrantDefinition::getUserDef)
.map(this::toPlPgSqlReference)
.collect(joining(", "));
plPgSql.indented(() ->
plPgSql.writeLn("userUuids => array[" + grantsToUsersPlPgSql + "],\n"));
rbacGrants.removeAll(grantsToUsers);
}
final var incomingGrants = findIncomingSuperRolesForRole(rbacDef.getRootEntityAlias(), role); generateIncomingSuperRolesForRole(plPgSql, role);
if (!incomingGrants.isEmpty()) {
final var incomingGrantsInPlPgSql = incomingGrants.stream()
.map(RbacView.RbacGrantDefinition::getSuperRoleDef)
.map(r -> toPlPgSqlReference(NEW, r))
.collect(joining(", "));
plPgSql.indented(() ->
plPgSql.writeLn("incomingSuperRoles => array[" + incomingGrantsInPlPgSql + "],\n"));
rbacGrants.removeAll(incomingGrants);
}
final var outgoingGrants = findOutgoingSuperRolesForRole(rbacDef.getRootEntityAlias(), role); generateOutgoingSubRolesForRole(plPgSql, role);
if (!outgoingGrants.isEmpty()) {
final var outgoingGrantsInPlPgSql = outgoingGrants.stream()
.map(RbacView.RbacGrantDefinition::getSuperRoleDef)
.map(r -> toPlPgSqlReference(NEW, r))
.collect(joining(", "));
plPgSql.indented(() ->
plPgSql.writeLn("outgoingSubRoles => array[" + outgoingGrantsInPlPgSql + "],\n"));
rbacGrants.removeAll(outgoingGrants);
}
plPgSql.chopTail(",\n"); plPgSql.chopTail(",\n");
plPgSql.writeLn(); plPgSql.writeLn();
@ -232,6 +191,66 @@ class RolesGrantsAndPermissionsGenerator {
plPgSql.writeLn(");"); plPgSql.writeLn(");");
} }
private void generateUserGrantsForRole(final StringWriter plPgSql, final RbacView.Role role) {
final var grantsToUsers = findGrantsToUserForRole(rbacDef.getRootEntityAlias(), role);
if (!grantsToUsers.isEmpty()) {
final var arrayElements = grantsToUsers.stream()
.map(RbacView.RbacGrantDefinition::getUserDef)
.map(this::toPlPgSqlReference)
.toList();
plPgSql.indented(() ->
plPgSql.writeLn("userUuids => array[" + joinArrayElements(arrayElements, 2) + "],\n"));
rbacGrants.removeAll(grantsToUsers);
}
}
private void generatePermissionsForRole(final StringWriter plPgSql, final RbacView.Role role) {
final var permissionGrantsForRole = findPermissionsGrantsForRole(rbacDef.getRootEntityAlias(), role);
if (!permissionGrantsForRole.isEmpty()) {
final var arrayElements = permissionGrantsForRole.stream()
.map(RbacView.RbacGrantDefinition::getPermDef)
.map(RbacPermissionDefinition::getPermission)
.map(RbacView.Permission::permission)
.map(p -> "'" + p + "'")
.toList();
plPgSql.indented( () ->
plPgSql.writeLn("permissions => array[" + joinArrayElements(arrayElements, 3) + "],\n"));
rbacGrants.removeAll(permissionGrantsForRole);
}
}
private void generateIncomingSuperRolesForRole(final StringWriter plPgSql, final RbacView.Role role) {
final var incomingGrants = findIncomingSuperRolesForRole(rbacDef.getRootEntityAlias(), role);
if (!incomingGrants.isEmpty()) {
final var arraElements = incomingGrants.stream()
.map(RbacView.RbacGrantDefinition::getSuperRoleDef)
.map(r -> toPlPgSqlReference(NEW, r))
.toList();
plPgSql.indented(() ->
plPgSql.writeLn("incomingSuperRoles => array[" + joinArrayElements(arraElements, 1) + "],\n"));
rbacGrants.removeAll(incomingGrants);
}
}
private void generateOutgoingSubRolesForRole(final StringWriter plPgSql, final RbacView.Role role) {
final var outgoingGrants = findOutgoingSuperRolesForRole(rbacDef.getRootEntityAlias(), role);
if (!outgoingGrants.isEmpty()) {
final var arrayElements = outgoingGrants.stream()
.map(RbacView.RbacGrantDefinition::getSubRoleDef)
.map(r -> toPlPgSqlReference(NEW, r))
.toList();
plPgSql.indented(() ->
plPgSql.writeLn("outgoingSubRoles => array[" + joinArrayElements(arrayElements, 1) + "],\n"));
rbacGrants.removeAll(outgoingGrants);
}
}
private String joinArrayElements(final List<String> arrayElements, final int singleLineLimit) {
return arrayElements.size() <= singleLineLimit
? String.join(", ", arrayElements)
: arrayElements.stream().collect(joining(",\n\t", "\n\t", ""));
}
private Set<RbacView.RbacGrantDefinition> findPermissionsGrantsForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) { private Set<RbacView.RbacGrantDefinition> findPermissionsGrantsForRole(final RbacView.EntityAlias entityAlias, final RbacView.Role role) {
final var roleDef = rbacDef.findRbacRole(entityAlias, role); final var roleDef = rbacDef.findRbacRole(entityAlias, role);
return rbacGrants.stream() return rbacGrants.stream()
@ -284,10 +303,6 @@ class RolesGrantsAndPermissionsGenerator {
plPgSql.writeLn(); plPgSql.writeLn();
} }
private String withoutRvSuffix(final String tableName) {
return tableName.substring(0, tableName.length()-"_rv".length());
}
private String toPlPgSqlReference(final RbacView.RbacUserReference userRef) { private String toPlPgSqlReference(final RbacView.RbacUserReference userRef) {
return switch (userRef.role) { return switch (userRef.role) {
case CREATOR -> "currentUserUuid()"; case CREATOR -> "currentUserUuid()";

View File

@ -5,6 +5,7 @@ import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
import jakarta.persistence.*; import jakarta.persistence.*;
@ -39,7 +40,7 @@ public class TestCustomerEntity implements RbacObject {
public static RbacView rbac() { public static RbacView rbac() {
return rbacViewFor("customer", TestCustomerEntity.class) return rbacViewFor("customer", TestCustomerEntity.class)
.withIdentityView(RbacView.SQL.query("target.prefix")) .withIdentityView(SQL.projection("prefix"))
.withUpdatableColumns("reference", "prefix", "adminUserName") .withUpdatableColumns("reference", "prefix", "adminUserName")
.createRole(OWNER, (with) -> { .createRole(OWNER, (with) -> {
with.owningUser(CREATOR); with.owningUser(CREATOR);