1
0

RBAC Diagram+PostgreSQL Generator and view->SELECT etc. refactoring (#21)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/21
Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
This commit is contained in:
Michael Hoennig
2024-03-11 12:30:43 +01:00
parent d9558f2cfe
commit 187c0db8e2
91 changed files with 4181 additions and 856 deletions

View File

@ -1,6 +1,6 @@
package net.hostsharing.hsadminng.errors;
import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
import java.util.UUID;
@ -8,7 +8,7 @@ public class ReferenceNotFoundException extends RuntimeException {
private final Class<?> entityClass;
private final UUID uuid;
public <E extends HasUuid> ReferenceNotFoundException(final Class<E> entityClass, final UUID uuid, final Throwable exc) {
public <E extends RbacObject> ReferenceNotFoundException(final Class<E> entityClass, final UUID uuid, final Throwable exc) {
super(exc);
this.entityClass = entityClass;
this.uuid = uuid;

View File

@ -4,6 +4,7 @@ import lombok.*;
import lombok.experimental.FieldNameConstants;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
@ -11,8 +12,13 @@ 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.*;
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.stringify.Stringify.stringify;
@Entity
@ -50,4 +56,25 @@ public class HsOfficeBankAccountEntity implements HasUuid, Stringifyable {
public String toShortString() {
return holder;
}
public static RbacView rbac() {
return rbacViewFor("bankAccount", HsOfficeBankAccountEntity.class)
.withIdentityView(SQL.projection("iban || ':' || holder"))
.withUpdatableColumns("holder", "iban", "bic")
.createRole(OWNER, (with) -> {
with.owningUser(CREATOR);
with.incomingSuperRole(GLOBAL, ADMIN);
with.permission(DELETE);
})
.createSubRole(ADMIN, (with) -> {
with.permission(UPDATE);
})
.createSubRole(REFERRER, (with) -> {
with.permission(SELECT);
});
}
public static void main(String[] args) throws IOException {
rbac().generateWithBaseFileName("243-hs-office-bankaccount-rbac-generated");
}
}

View File

@ -4,13 +4,21 @@ import lombok.*;
import lombok.experimental.FieldNameConstants;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.persistence.HasUuid;
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.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;
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.rbacViewFor;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity
@ -28,7 +36,6 @@ public class HsOfficeContactEntity implements Stringifyable, HasUuid {
.withProp(Fields.label, HsOfficeContactEntity::getLabel)
.withProp(Fields.emailAddresses, HsOfficeContactEntity::getEmailAddresses);
@Id
@GeneratedValue(generator = "UUID")
@GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
@ -53,4 +60,25 @@ public class HsOfficeContactEntity implements Stringifyable, HasUuid {
public String toShortString() {
return label;
}
public static RbacView rbac() {
return rbacViewFor("contact", HsOfficeContactEntity.class)
.withIdentityView(SQL.projection("label"))
.withUpdatableColumns("label", "postalAddress", "emailAddresses", "phoneNumbers")
.createRole(OWNER, (with) -> {
with.owningUser(CREATOR);
with.incomingSuperRole(GLOBAL, ADMIN);
with.permission(DELETE);
})
.createSubRole(ADMIN, (with) -> {
with.permission(UPDATE);
})
.createSubRole(REFERRER, (with) -> {
with.permission(SELECT);
});
}
public static void main(String[] args) throws IOException {
rbac().generateWithBaseFileName("203-hs-office-contact-rbac-generated");
}
}

View File

@ -4,16 +4,25 @@ import lombok.*;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity;
import net.hostsharing.hsadminng.persistence.HasUuid;
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.Stringifyable;
import org.hibernate.annotations.GenericGenerator;
import jakarta.persistence.*;
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.fetchedBySql;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity
@ -78,7 +87,7 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable {
private String defaultPrefix;
private String getDebitorNumberString() {
if (partner == null || partner.getPartnerNumber() == null || debitorNumberSuffix == null ) {
if (partner == null || partner.getPartnerNumber() == null || debitorNumberSuffix == null) {
return null;
}
return partner.getPartnerNumber() + String.format("%02d", debitorNumberSuffix);
@ -97,4 +106,71 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable {
public String toShortString() {
return DEBITOR_NUMBER_TAG + getDebitorNumberString();
}
public static RbacView rbac() {
return rbacViewFor("debitor", HsOfficeDebitorEntity.class)
.withIdentityView(SQL.query("""
SELECT debitor.uuid,
'D-' || (SELECT partner.partnerNumber
FROM hs_office_partner partner
JOIN hs_office_relationship partnerRel
ON partnerRel.uuid = partner.partnerRoleUUid AND partnerRel.relType = 'PARTNER'
JOIN hs_office_relationship debitorRel
ON debitorRel.relAnchorUuid = partnerRel.relHolderUuid AND partnerRel.relType = 'ACCOUNTING'
WHERE debitorRel.uuid = debitor.debitorRelUuid)
|| to_char(debitorNumberSuffix, 'fm00')
from hs_office_debitor as debitor
"""))
.withUpdatableColumns(
"debitorRel",
"billable",
"debitorUuid",
"refundBankAccountUuid",
"vatId",
"vatCountryCode",
"vatBusiness",
"vatReverseCharge",
"defaultPrefix" /* TODO: do we want that updatable? */)
.createPermission(custom("new-debitor")).grantedTo("global", ADMIN)
.importRootEntityAliasProxy("debitorRel", HsOfficeRelationshipEntity.class,
fetchedBySql("""
SELECT *
FROM hs_office_relationship AS r
WHERE r.relType = 'ACCOUNTING' AND r.relHolderUuid = ${REF}.debitorRelUuid
"""),
dependsOnColumn("debitorRelUuid"))
.createPermission(DELETE).grantedTo("debitorRel", OWNER)
.createPermission(UPDATE).grantedTo("debitorRel", ADMIN)
.createPermission(SELECT).grantedTo("debitorRel", TENANT)
.importEntityAlias("refundBankAccount", HsOfficeBankAccountEntity.class,
dependsOnColumn("refundBankAccountUuid"), fetchedBySql("""
SELECT *
FROM hs_office_relationship AS r
WHERE r.relType = 'ACCOUNTING' AND r.relHolderUuid = ${REF}.debitorRelUuid
""")
)
.toRole("refundBankAccount", ADMIN).grantRole("debitorRel", AGENT)
.toRole("debitorRel", AGENT).grantRole("refundBankAccount", REFERRER)
.importEntityAlias("partnerRel", HsOfficeRelationshipEntity.class,
dependsOnColumn("partnerRelUuid"), fetchedBySql("""
SELECT *
FROM hs_office_relationship AS partnerRel
WHERE ${debitorRel}.relAnchorUuid = partnerRel.relHolderUuid
""")
)
.toRole("partnerRel", ADMIN).grantRole("debitorRel", ADMIN)
.toRole("partnerRel", AGENT).grantRole("debitorRel", AGENT)
.toRole("debitorRel", AGENT).grantRole("partnerRel", TENANT)
.declarePlaceholderEntityAliases("partnerPerson", "operationalPerson")
.forExampleRole("partnerPerson", ADMIN).wouldBeGrantedTo("partnerRel", ADMIN)
.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-generated");
}
}

View File

@ -8,12 +8,12 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartne
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerPatchResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerRoleInsertResource;
import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity;
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipRepository;
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipType;
import net.hostsharing.hsadminng.mapper.Mapper;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@ -158,7 +158,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
return entity;
}
private <E extends HasUuid> E ref(final Class<E> entityClass, final UUID uuid) {
private <E extends RbacObject> E ref(final Class<E> entityClass, final UUID uuid) {
try {
return em.getReference(entityClass, uuid);
} catch (final Throwable exc) {

View File

@ -2,14 +2,23 @@ package net.hostsharing.hsadminng.hs.office.partner;
import lombok.*;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity;
import net.hostsharing.hsadminng.persistence.HasUuid;
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.Stringifyable;
import jakarta.persistence.*;
import java.io.IOException;
import java.time.LocalDate;
import java.util.UUID;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.fetchedBySql;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity
@ -55,6 +64,45 @@ public class HsOfficePartnerDetailsEntity implements HasUuid, Stringifyable {
return registrationNumber != null ? registrationNumber
: birthName != null ? birthName
: birthday != null ? birthday.toString()
: dateOfDeath != null ? dateOfDeath.toString() : "<empty details>";
: dateOfDeath != null ? dateOfDeath.toString()
: "<empty details>";
}
public static RbacView rbac() {
return rbacViewFor("partnerDetails", HsOfficePartnerDetailsEntity.class)
.withIdentityView(SQL.query("""
SELECT partner_iv.idName || '-details'
FROM hs_office_partner_details AS partnerDetails
JOIN hs_office_partner partner ON partner.detailsUuid = partnerDetails.uuid
JOIN hs_office_partner_iv partner_iv ON partner_iv.uuid = partner.uuid
"""))
.withUpdatableColumns(
"registrationOffice",
"registrationNumber",
"birthPlace",
"birthName",
"birthday",
"dateOfDeath")
.createPermission(custom("new-partner-details")).grantedTo("global", ADMIN)
.importRootEntityAliasProxy("partnerRel", HsOfficeRelationshipEntity.class,
fetchedBySql("""
SELECT partnerRel.*
FROM hs_office_relationship AS partnerRel
JOIN hs_office_partner AS partner
ON partner.detailsUuid = ${ref}.uuid
WHERE partnerRel.uuid = partner.partnerRoleUuid
"""),
dependsOnColumn("partnerRoleUuid"))
// The grants are defined in HsOfficePartnerEntity.rbac()
// because they have to be changed when its partnerRel changes,
// not when anything in partner details changes.
;
}
public static void main(String[] args) throws IOException {
rbac().generateWithBaseFileName("234-hs-office-partner-details-rbac-generated");
}
}

View File

@ -6,15 +6,24 @@ import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity;
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.Stringifyable;
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
import jakarta.persistence.*;
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.fetchedBySql;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity
@ -68,4 +77,37 @@ public class HsOfficePartnerEntity implements Stringifyable, HasUuid {
public String toShortString() {
return Optional.ofNullable(person).map(HsOfficePersonEntity::toShortString).orElse("<person=null>");
}
public static RbacView rbac() {
return rbacViewFor("partner", HsOfficePartnerEntity.class)
.withIdentityView(SQL.query("""
SELECT partner.partnerNumber
|| ':' || (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)
FROM hs_office_partner AS partner
"""))
.withUpdatableColumns(
"partnerRoleUuid",
"personUuid",
"contactUuid")
.createPermission(custom("new-partner")).grantedTo("global", ADMIN)
.importRootEntityAliasProxy("partnerRel", HsOfficeRelationshipEntity.class,
fetchedBySql("SELECT * FROM hs_office_relationship AS r WHERE r.uuid = ${ref}.partnerRoleUuid"),
dependsOnColumn("partnerRelUuid"))
.createPermission(DELETE).grantedTo("partnerRel", ADMIN)
.createPermission(UPDATE).grantedTo("partnerRel", AGENT)
.createPermission(SELECT).grantedTo("partnerRel", TENANT)
.importSubEntityAlias("partnerDetails", HsOfficePartnerDetailsEntity.class,
fetchedBySql("SELECT * FROM hs_office_partner_details AS d WHERE d.uuid = ${ref}.detailsUuid"),
dependsOnColumn("detailsUuid"))
.createPermission("partnerDetails", DELETE).grantedTo("partnerRel", ADMIN)
.createPermission("partnerDetails", UPDATE).grantedTo("partnerRel", AGENT)
.createPermission("partnerDetails", SELECT).grantedTo("partnerRel", AGENT);
}
public static void main(String[] args) throws IOException {
rbac().generateWithBaseFileName("233-hs-office-partner-rbac-generated");
}
}

View File

@ -4,13 +4,21 @@ import lombok.*;
import lombok.experimental.FieldNameConstants;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.persistence.HasUuid;
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.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;
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.rbacViewFor;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity
@ -56,4 +64,26 @@ public class HsOfficePersonEntity implements HasUuid, Stringifyable {
return personType + " " +
(!StringUtils.isEmpty(tradeName) ? tradeName : (familyName + ", " + givenName));
}
public static RbacView rbac() {
return rbacViewFor("person", HsOfficePersonEntity.class)
.withIdentityView(SQL.projection("concat(tradeName, familyName, givenName)"))
.withUpdatableColumns("personType", "tradeName", "givenName", "familyName")
.createRole(OWNER, (with) -> {
with.permission(DELETE);
with.owningUser(CREATOR);
with.incomingSuperRole(GLOBAL, ADMIN);
})
.createSubRole(ADMIN, (with) -> {
with.permission(UPDATE);
})
.createSubRole(REFERRER, (with) -> {
with.permission(SELECT);
});
}
public static void main(String[] args) throws IOException {
rbac().generateWithBaseFileName("213-hs-office-person-rbac-generated");
}
}

View File

@ -3,14 +3,24 @@ package net.hostsharing.hsadminng.hs.office.relationship;
import lombok.*;
import lombok.experimental.FieldNameConstants;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
import net.hostsharing.hsadminng.persistence.HasUuid;
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.Stringifyable;
import jakarta.persistence.*;
import java.io.IOException;
import java.util.UUID;
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.fetchedBySql;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity
@ -67,4 +77,52 @@ public class HsOfficeRelationshipEntity implements HasUuid, Stringifyable {
public String toShortString() {
return toShortString.apply(this);
}
public static RbacView rbac() {
return rbacViewFor("relationship", HsOfficeRelationshipEntity.class)
.withIdentityView(SQL.projection("""
(select idName from hs_office_person_iv p where p.uuid = relAnchorUuid)
|| '-with-' || target.relType || '-'
|| (select idName from hs_office_person_iv p where p.uuid = relHolderUuid)
"""))
.withRestrictedViewOrderBy(SQL.expression(
"(select idName from hs_office_person_iv p where p.uuid = target.relHolderUuid)"))
.withUpdatableColumns("contactUuid")
.importEntityAlias("anchorPerson", HsOfficePersonEntity.class,
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")
)
.importEntityAlias("contact", HsOfficeContactEntity.class,
dependsOnColumn("contactUuid"),
fetchedBySql("select * from hs_office_contact as c where c.uuid = ${REF}.contactUuid")
)
.createRole(OWNER, (with) -> {
with.owningUser(CREATOR);
with.incomingSuperRole(GLOBAL, ADMIN);
with.permission(DELETE);
})
.createSubRole(ADMIN, (with) -> {
with.incomingSuperRole("anchorPerson", ADMIN);
with.permission(UPDATE);
})
.createSubRole(AGENT, (with) -> {
with.incomingSuperRole("holderPerson", ADMIN);
})
.createSubRole(TENANT, (with) -> {
with.incomingSuperRole("holderPerson", ADMIN);
with.incomingSuperRole("contact", ADMIN);
with.outgoingSubRole("anchorPerson", REFERRER);
with.outgoingSubRole("holderPerson", REFERRER);
with.outgoingSubRole("contact", REFERRER);
with.permission(SELECT);
});
}
public static void main(String[] args) throws IOException {
rbac().generateWithBaseFileName("223-hs-office-relationship-rbac-generated");
}
}

View File

@ -6,16 +6,26 @@ import lombok.*;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
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.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.Type;
import jakarta.persistence.*;
import java.io.IOException;
import java.time.LocalDate;
import java.util.UUID;
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;
@Entity
@ -84,4 +94,35 @@ public class HsOfficeSepaMandateEntity implements Stringifyable, HasUuid {
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.permission(DELETE);
})
.createSubRole(ADMIN, (with) -> {
with.permission(UPDATE);
})
.createSubRole(AGENT, (with) -> {
with.outgoingSubRole("bankAccount", REFERRER);
with.outgoingSubRole("debitorRel", AGENT);
})
.createSubRole(REFERRER, (with) -> {
with.incomingSuperRole("bankAccount", ADMIN);
with.incomingSuperRole("debitorRel", AGENT);
with.outgoingSubRole("debitorRel", TENANT);
with.permission(SELECT);
});
}
public static void main(String[] args) throws IOException {
rbac().generateWithBaseFileName("253-hs-office-sepamandate-rbac-generated");
}
}

View File

@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.persistence;
import java.util.UUID;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
public interface HasUuid {
UUID getUuid();
// TODO: remove this interface, I just wanted to avoid to many changes in that PR
public interface HasUuid extends RbacObject {
}

View File

@ -0,0 +1,165 @@
package net.hostsharing.hsadminng.rbac.rbacdef;
import java.util.Optional;
import java.util.function.BinaryOperator;
import java.util.stream.Stream;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.PERM_TO_ROLE;
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;
public class InsertTriggerGenerator {
private final RbacView rbacDef;
private final String liquibaseTagPrefix;
public InsertTriggerGenerator(final RbacView rbacDef, final String liqibaseTagPrefix) {
this.rbacDef = rbacDef;
this.liquibaseTagPrefix = liqibaseTagPrefix;
}
void generateTo(final StringWriter plPgSql) {
generateLiquibaseChangesetHeader(plPgSql);
generateGrantInsertRoleToExistingCustomers(plPgSql);
generateInsertPermissionGrantTrigger(plPgSql);
generateInsertCheckTrigger(plPgSql);
plPgSql.writeLn("--//");
}
private void generateLiquibaseChangesetHeader(final StringWriter plPgSql) {
plPgSql.writeLn("""
-- ============================================================================
--changeset ${liquibaseTagPrefix}-rbac-INSERT:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
""",
with("liquibaseTagPrefix", liquibaseTagPrefix));
}
private void generateGrantInsertRoleToExistingCustomers(final StringWriter plPgSql) {
getOptionalInsertSuperRole().ifPresent( superRoleDef -> {
plPgSql.writeLn("""
/*
Creates INSERT INTO ${rawSubTableName} permissions for the related ${rawSuperTableName} rows.
*/
do language plpgsql $$
declare
row ${rawSuperTableName};
permissionUuid uuid;
roleUuid uuid;
begin
call defineContext('create INSERT INTO ${rawSubTableName} permissions for the related ${rawSuperTableName} rows');
FOR row IN SELECT * FROM ${rawSuperTableName}
LOOP
roleUuid := findRoleId(${rawSuperRoleDescriptor}(row));
permissionUuid := createPermission(row.uuid, 'INSERT', '${rawSubTableName}');
call grantPermissionToRole(roleUuid, permissionUuid);
END LOOP;
END;
$$;
""",
with("rawSubTableName", rbacDef.getRootEntityAlias().getRawTableName()),
with("rawSuperTableName", superRoleDef.getEntityAlias().getRawTableName()),
with("rawSuperRoleDescriptor", toVar(superRoleDef))
);
});
}
private void generateInsertPermissionGrantTrigger(final StringWriter plPgSql) {
getOptionalInsertSuperRole().ifPresent( superRoleDef -> {
plPgSql.writeLn("""
/**
Adds ${rawSubTableName} INSERT permission to specified role of new ${rawSuperTableName} rows.
*/
create or replace function ${rawSubTableName}_${rawSuperTableName}_insert_tf()
returns trigger
language plpgsql
strict as $$
begin
call grantPermissionToRole(
${rawSuperRoleDescriptor}(NEW),
createPermission(NEW.uuid, 'INSERT', '${rawSubTableName}'));
return NEW;
end; $$;
create trigger ${rawSubTableName}_${rawSuperTableName}_insert_tg
after insert on ${rawSuperTableName}
for each row
execute procedure ${rawSubTableName}_${rawSuperTableName}_insert_tf();
""",
with("rawSubTableName", rbacDef.getRootEntityAlias().getRawTableName()),
with("rawSuperTableName", superRoleDef.getEntityAlias().getRawTableName()),
with("rawSuperRoleDescriptor", toVar(superRoleDef))
);
});
}
private void generateInsertCheckTrigger(final StringWriter plPgSql) {
plPgSql.writeLn("""
/**
Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable}.
*/
create or replace function ${rawSubTable}_insert_permission_missing_tf()
returns trigger
language plpgsql as $$
begin
raise exception '[403] insert into ${rawSubTable} not allowed for current subjects % (%)',
currentSubjects(), currentSubjectsUuids();
end; $$;
""",
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()));
getOptionalInsertGrant().ifPresentOrElse(g -> {
plPgSql.writeLn("""
create trigger ${rawSubTable}_insert_permission_check_tg
before insert on ${rawSubTable}
for each row
when ( not hasInsertPermission(NEW.${referenceColumn}, 'INSERT', '${rawSubTable}') )
execute procedure ${rawSubTable}_insert_permission_missing_tf();
""",
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()),
with("referenceColumn", g.getSuperRoleDef().getEntityAlias().dependsOnColumName() ));
},
() -> {
plPgSql.writeLn("""
create trigger ${rawSubTable}_insert_permission_check_tg
before insert on ${rawSubTable}
for each row
-- As there is no explicit INSERT grant specified for this table,
-- only global admins are allowed to insert any rows.
when ( not isGlobalAdmin() )
execute procedure ${rawSubTable}_insert_permission_missing_tf();
""",
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()));
});
}
private Stream<RbacView.RbacGrantDefinition> getInsertGrants() {
return rbacDef.getGrantDefs().stream()
.filter(g -> g.grantType() == PERM_TO_ROLE)
.filter(g -> g.getPermDef().toCreate && g.getPermDef().getPermission() == INSERT);
}
private Optional<RbacView.RbacGrantDefinition> getOptionalInsertGrant() {
return getInsertGrants()
.reduce(singleton());
}
private Optional<RbacView.RbacRoleDefinition> getOptionalInsertSuperRole() {
return getInsertGrants()
.map(RbacView.RbacGrantDefinition::getSuperRoleDef)
.reduce(singleton());
}
private static <T> BinaryOperator<T> singleton() {
return (x, y) -> {
throw new IllegalStateException("only a single INSERT permission grant allowed");
};
}
private static String toVar(final RbacView.RbacRoleDefinition roleDef) {
return uncapitalize(roleDef.getEntityAlias().simpleName()) + capitalize(roleDef.getRole().roleName());
}
}

View File

@ -0,0 +1,5 @@
package net.hostsharing.hsadminng.rbac.rbacdef;
public enum PostgresTriggerReference {
NEW, OLD
}

View File

@ -0,0 +1,45 @@
package net.hostsharing.hsadminng.rbac.rbacdef;
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with;
public class RbacIdentityViewGenerator {
private final RbacView rbacDef;
private final String liquibaseTagPrefix;
private final String simpleEntityVarName;
private final String rawTableName;
public RbacIdentityViewGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) {
this.rbacDef = rbacDef;
this.liquibaseTagPrefix = liquibaseTagPrefix;
this.simpleEntityVarName = rbacDef.getRootEntityAlias().simpleName();
this.rawTableName = rbacDef.getRootEntityAlias().getRawTableName();
}
void generateTo(final StringWriter plPgSql) {
plPgSql.writeLn("""
-- ============================================================================
--changeset ${liquibaseTagPrefix}-rbac-IDENTITY-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
""",
with("liquibaseTagPrefix", liquibaseTagPrefix));
plPgSql.writeLn(
switch (rbacDef.getIdentityViewSqlQuery().part) {
case SQL_PROJECTION -> """
call generateRbacIdentityViewFromProjection('${rawTableName}', $idName$
${identityViewSqlPart}
$idName$);
""";
case SQL_QUERY -> """
call generateRbacIdentityViewFromProjection('${rawTableName}', $idName$
${identityViewSqlPart}
$idName$);
""";
default -> throw new IllegalStateException("illegal SQL part given");
},
with("identityViewSqlPart", rbacDef.getIdentityViewSqlQuery().sql),
with("rawTableName", rawTableName));
plPgSql.writeLn("--//");
}
}

View File

@ -0,0 +1,27 @@
package net.hostsharing.hsadminng.rbac.rbacdef;
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with;
public class RbacObjectGenerator {
private final String liquibaseTagPrefix;
private final String rawTableName;
public RbacObjectGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) {
this.liquibaseTagPrefix = liquibaseTagPrefix;
this.rawTableName = rbacDef.getRootEntityAlias().getRawTableName();
}
void generateTo(final StringWriter plPgSql) {
plPgSql.writeLn("""
-- ============================================================================
--changeset ${liquibaseTagPrefix}-rbac-OBJECT:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
call generateRelatedRbacObject('${rawTableName}');
--//
""",
with("liquibaseTagPrefix", liquibaseTagPrefix),
with("rawTableName", rawTableName));
}
}

View File

@ -0,0 +1,41 @@
package net.hostsharing.hsadminng.rbac.rbacdef;
import static java.util.stream.Collectors.joining;
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.indented;
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with;
public class RbacRestrictedViewGenerator {
private final RbacView rbacDef;
private final String liquibaseTagPrefix;
private final String simpleEntityVarName;
private final String rawTableName;
public RbacRestrictedViewGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) {
this.rbacDef = rbacDef;
this.liquibaseTagPrefix = liquibaseTagPrefix;
this.simpleEntityVarName = rbacDef.getRootEntityAlias().simpleName();
this.rawTableName = rbacDef.getRootEntityAlias().getRawTableName();
}
void generateTo(final StringWriter plPgSql) {
plPgSql.writeLn("""
-- ============================================================================
--changeset ${liquibaseTagPrefix}-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
call generateRbacRestrictedView('${rawTableName}',
'${orderBy}',
$updates$
${updates}
$updates$);
--//
""",
with("liquibaseTagPrefix", liquibaseTagPrefix),
with("orderBy", rbacDef.getOrderBySqlExpression().sql),
with("updates", indented(rbacDef.getUpdatableColumns().stream()
.map(c -> c + " = new." + c)
.collect(joining(",\n")), 2)),
with("rawTableName", rawTableName));
}
}

View File

@ -0,0 +1,30 @@
package net.hostsharing.hsadminng.rbac.rbacdef;
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with;
public class RbacRoleDescriptorsGenerator {
private final String liquibaseTagPrefix;
private final String simpleEntityVarName;
private final String rawTableName;
public RbacRoleDescriptorsGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) {
this.liquibaseTagPrefix = liquibaseTagPrefix;
this.simpleEntityVarName = rbacDef.getRootEntityAlias().simpleName();
this.rawTableName = rbacDef.getRootEntityAlias().getRawTableName();
}
void generateTo(final StringWriter plPgSql) {
plPgSql.writeLn("""
-- ============================================================================
--changeset ${liquibaseTagPrefix}-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
call generateRbacRoleDescriptors('${simpleEntityVarName}', '${rawTableName}');
--//
""",
with("liquibaseTagPrefix", liquibaseTagPrefix),
with("simpleEntityVarName", simpleEntityVarName),
with("rawTableName", rawTableName));
}
}

View File

@ -0,0 +1,830 @@
package net.hostsharing.hsadminng.rbac.rbacdef;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
import net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionEntity;
import net.hostsharing.hsadminng.hs.office.coopshares.HsOfficeCoopSharesTransactionEntity;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerDetailsEntity;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity;
import net.hostsharing.hsadminng.hs.office.sepamandate.HsOfficeSepaMandateEntity;
import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
import net.hostsharing.hsadminng.test.cust.TestCustomerEntity;
import net.hostsharing.hsadminng.test.dom.TestDomainEntity;
import net.hostsharing.hsadminng.test.pac.TestPackageEntity;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import jakarta.validation.constraints.NotNull;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Stream;
import static java.lang.reflect.Modifier.isStatic;
import static java.util.Arrays.stream;
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;
import static org.apache.commons.lang3.StringUtils.uncapitalize;
@Getter
public class RbacView {
public static final String GLOBAL = "global";
public static final String OUTPUT_BASEDIR = "src/main/resources/db/changelog";
private final EntityAlias rootEntityAlias;
private final Set<RbacUserReference> userDefs = new LinkedHashSet<>();
private final Set<RbacRoleDefinition> roleDefs = new LinkedHashSet<>();
private final Set<RbacPermissionDefinition> permDefs = new LinkedHashSet<>();
private final Map<String, EntityAlias> entityAliases = new HashMap<>() {
@Override
public EntityAlias put(final String key, final EntityAlias value) {
if (containsKey(key)) {
throw new IllegalArgumentException("duplicate entityAlias: " + key);
}
return super.put(key, value);
}
};
private final Set<String> updatableColumns = new LinkedHashSet<>();
private final Set<RbacGrantDefinition> grantDefs = new LinkedHashSet<>();
private SQL identityViewSqlQuery;
private SQL orderBySqlExpression;
private EntityAlias rootEntityAliasProxy;
private RbacRoleDefinition previousRoleDef;
public static <E extends RbacObject> RbacView rbacViewFor(final String alias, final Class<E> entityClass) {
return new RbacView(alias, entityClass);
}
RbacView(final String alias, final Class<? extends RbacObject> entityClass) {
rootEntityAlias = new EntityAlias(alias, entityClass);
entityAliases.put(alias, rootEntityAlias);
new RbacUserReference(CREATOR);
entityAliases.put("global", new EntityAlias("global"));
}
public RbacView withUpdatableColumns(final String... columnNames) {
Collections.addAll(updatableColumns, columnNames);
verifyVersionColumnExists();
return this;
}
public RbacView withIdentityView(final SQL sqlExpression) {
this.identityViewSqlQuery = sqlExpression;
return this;
}
public RbacView withRestrictedViewOrderBy(final SQL orderBySqlExpression) {
this.orderBySqlExpression = orderBySqlExpression;
return this;
}
public RbacView createRole(final Role role, final Consumer<RbacRoleDefinition> with) {
final RbacRoleDefinition newRoleDef = findRbacRole(rootEntityAlias, role).toCreate();
with.accept(newRoleDef);
previousRoleDef = newRoleDef;
return this;
}
public RbacView createSubRole(final Role role) {
final RbacRoleDefinition newRoleDef = findRbacRole(rootEntityAlias, role).toCreate();
findOrCreateGrantDef(newRoleDef, previousRoleDef).toCreate();
previousRoleDef = newRoleDef;
return this;
}
public RbacView createSubRole(final Role role, final Consumer<RbacRoleDefinition> with) {
final RbacRoleDefinition newRoleDef = findRbacRole(rootEntityAlias, role).toCreate();
findOrCreateGrantDef(newRoleDef, previousRoleDef).toCreate();
with.accept(newRoleDef);
previousRoleDef = newRoleDef;
return this;
}
public RbacPermissionDefinition createPermission(final Permission permission) {
return createPermission(rootEntityAlias, permission);
}
public RbacPermissionDefinition createPermission(final String entityAliasName, final Permission permission) {
return createPermission(findEntityAlias(entityAliasName), permission);
}
private RbacPermissionDefinition createPermission(final EntityAlias entityAlias, final Permission permission) {
return new RbacPermissionDefinition(entityAlias, permission, null, true);
}
public <EC extends RbacObject> RbacView declarePlaceholderEntityAliases(final String... aliasNames) {
for (String alias : aliasNames) {
entityAliases.put(alias, new EntityAlias(alias));
}
return this;
}
public <EC extends RbacObject> RbacView importRootEntityAliasProxy(
final String aliasName,
final Class<? extends HasUuid> entityClass,
final SQL fetchSql,
final Column dependsOnColum) {
if (rootEntityAliasProxy != null) {
throw new IllegalStateException("there is already an entityAliasProxy: " + rootEntityAliasProxy);
}
rootEntityAliasProxy = importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, false);
return this;
}
public RbacView importSubEntityAlias(
final String aliasName, final Class<? extends HasUuid> entityClass,
final SQL fetchSql, final Column dependsOnColum) {
importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, true);
return this;
}
public RbacView importEntityAlias(
final String aliasName, final Class<? extends HasUuid> entityClass,
final Column dependsOnColum, final SQL fetchSql) {
importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, false);
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(
final String aliasName, final Class<? extends HasUuid> entityClass,
final SQL fetchSql, final Column dependsOnColum, boolean asSubEntity) {
final var entityAlias = new EntityAlias(aliasName, entityClass, fetchSql, dependsOnColum, asSubEntity);
entityAliases.put(aliasName, entityAlias);
try {
importAsAlias(aliasName, rbacDefinition(entityClass), asSubEntity);
} catch (final ReflectiveOperationException exc) {
throw new RuntimeException("cannot import entity: " + entityClass, exc);
}
return entityAlias;
}
private static RbacView rbacDefinition(final Class<? extends RbacObject> entityClass)
throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
return (RbacView) entityClass.getMethod("rbac").invoke(null);
}
private RbacView importAsAlias(final String aliasName, final RbacView importedRbacView, final boolean asSubEntity) {
final var mapper = new AliasNameMapper(importedRbacView, aliasName,
asSubEntity ? entityAliases.keySet() : null);
importedRbacView.getEntityAliases().values().stream()
.filter(entityAlias -> !importedRbacView.isRootEntityAlias(entityAlias))
.filter(entityAlias -> !entityAlias.isGlobal())
.filter(entityAlias -> !asSubEntity || !entityAliases.containsKey(entityAlias.aliasName))
.forEach(entityAlias -> {
final String mappedAliasName = mapper.map(entityAlias.aliasName);
entityAliases.put(mappedAliasName, new EntityAlias(mappedAliasName, entityAlias.entityClass));
});
importedRbacView.getRoleDefs().forEach(roleDef -> {
new RbacRoleDefinition(findEntityAlias(mapper.map(roleDef.entityAlias.aliasName)), roleDef.role);
});
importedRbacView.getGrantDefs().forEach(grantDef -> {
if (grantDef.grantType() == RbacGrantDefinition.GrantType.ROLE_TO_ROLE) {
final var importedGrantDef = findOrCreateGrantDef(
findRbacRole(
mapper.map(grantDef.getSubRoleDef().entityAlias.aliasName),
grantDef.getSubRoleDef().getRole()),
findRbacRole(
mapper.map(grantDef.getSuperRoleDef().entityAlias.aliasName),
grantDef.getSuperRoleDef().getRole())
);
if (!grantDef.isAssumed()) {
importedGrantDef.unassumed();
}
}
});
return this;
}
private void verifyVersionColumnExists() {
if (stream(rootEntityAlias.entityClass.getDeclaredFields())
.noneMatch(f -> f.getAnnotation(Version.class) != null)) {
// TODO: convert this into throw Exception once RbacEntity is a base class with @Version field
System.err.println("@Version field required in updatable entity " + rootEntityAlias.entityClass);
}
}
public RbacGrantBuilder toRole(final String entityAlias, final Role role) {
return new RbacGrantBuilder(entityAlias, role);
}
public RbacExampleRole forExampleRole(final String entityAlias, final Role role) {
return new RbacExampleRole(entityAlias, role);
}
private RbacGrantDefinition grantRoleToUser(final RbacRoleDefinition roleDefinition, final RbacUserReference user) {
return findOrCreateGrantDef(roleDefinition, user).toCreate();
}
private RbacGrantDefinition grantPermissionToRole(
final RbacPermissionDefinition permDef,
final RbacRoleDefinition roleDef) {
return findOrCreateGrantDef(permDef, roleDef).toCreate();
}
private RbacGrantDefinition grantSubRoleToSuperRole(
final RbacRoleDefinition subRoleDefinition,
final RbacRoleDefinition superRoleDefinition) {
return findOrCreateGrantDef(subRoleDefinition, superRoleDefinition).toCreate();
}
boolean isRootEntityAlias(final EntityAlias entityAlias) {
return entityAlias == this.rootEntityAlias;
}
public boolean isEntityAliasProxy(final EntityAlias entityAlias) {
return entityAlias == rootEntityAliasProxy;
}
public SQL getOrderBySqlExpression() {
if (orderBySqlExpression == null) {
return identityViewSqlQuery;
}
return orderBySqlExpression;
}
public void generateWithBaseFileName(final String baseFileName) {
new RbacViewMermaidFlowchartGenerator(this).generateToMarkdownFile(Path.of(OUTPUT_BASEDIR, baseFileName + ".md"));
new RbacViewPostgresGenerator(this).generateToChangeLog(Path.of(OUTPUT_BASEDIR, baseFileName + ".sql"));
}
public class RbacGrantBuilder {
private final RbacRoleDefinition superRoleDef;
private RbacGrantBuilder(final String entityAlias, final Role role) {
this.superRoleDef = findRbacRole(entityAlias, role);
}
public RbacView grantRole(final String entityAlias, final Role role) {
findOrCreateGrantDef(findRbacRole(entityAlias, role), superRoleDef).toCreate();
return RbacView.this;
}
public RbacView grantPermission(final String entityAliasName, final Permission perm) {
final var entityAlias = findEntityAlias(entityAliasName);
final var forTable = entityAlias.getRawTableName();
findOrCreateGrantDef(findRbacPerm(entityAlias, perm, forTable), superRoleDef).toCreate();
return RbacView.this;
}
}
@Getter
@EqualsAndHashCode
public class RbacGrantDefinition {
private final RbacUserReference userDef;
private final RbacRoleDefinition superRoleDef;
private final RbacRoleDefinition subRoleDef;
private final RbacPermissionDefinition permDef;
private boolean assumed = true;
private boolean toCreate = false;
@Override
public String toString() {
final var arrow = isAssumed() ? " --> " : " -- // --> ";
return switch (grantType()) {
case ROLE_TO_USER -> userDef.toString() + arrow + subRoleDef.toString();
case ROLE_TO_ROLE -> superRoleDef + arrow + subRoleDef;
case PERM_TO_ROLE -> superRoleDef + arrow + permDef;
};
}
RbacGrantDefinition(final RbacRoleDefinition subRoleDef, final RbacRoleDefinition superRoleDef) {
this.userDef = null;
this.subRoleDef = subRoleDef;
this.superRoleDef = superRoleDef;
this.permDef = null;
register(this);
}
public RbacGrantDefinition(final RbacPermissionDefinition permDef, final RbacRoleDefinition roleDef) {
this.userDef = null;
this.subRoleDef = null;
this.superRoleDef = roleDef;
this.permDef = permDef;
register(this);
}
public RbacGrantDefinition(final RbacRoleDefinition roleDef, final RbacUserReference userDef) {
this.userDef = userDef;
this.subRoleDef = roleDef;
this.superRoleDef = null;
this.permDef = null;
register(this);
}
private void register(final RbacGrantDefinition rbacGrantDefinition) {
grantDefs.add(rbacGrantDefinition);
}
@NotNull
GrantType grantType() {
return permDef != null ? GrantType.PERM_TO_ROLE
: userDef != null ? GrantType.ROLE_TO_USER
: GrantType.ROLE_TO_ROLE;
}
boolean isAssumed() {
return assumed;
}
boolean isToCreate() {
return toCreate;
}
RbacGrantDefinition toCreate() {
toCreate = true;
return this;
}
boolean dependsOnColumn(final String columnName) {
return dependsRoleDefOnColumnName(this.superRoleDef, columnName)
|| dependsRoleDefOnColumnName(this.subRoleDef, columnName);
}
private Boolean dependsRoleDefOnColumnName(final RbacRoleDefinition superRoleDef, final String columnName) {
return ofNullable(superRoleDef)
.map(r -> r.getEntityAlias().dependsOnColum())
.map(d -> columnName.equals(d.column))
.orElse(false);
}
public void unassumed() {
this.assumed = false;
}
public enum GrantType {
ROLE_TO_USER,
ROLE_TO_ROLE,
PERM_TO_ROLE
}
}
public class RbacExampleRole {
final EntityAlias subRoleEntity;
final Role subRole;
private EntityAlias superRoleEntity;
Role superRole;
public RbacExampleRole(final String entityAlias, final Role role) {
this.subRoleEntity = findEntityAlias(entityAlias);
this.subRole = role;
}
public RbacView wouldBeGrantedTo(final String entityAlias, final Role role) {
this.superRoleEntity = findEntityAlias(entityAlias);
this.superRole = role;
return RbacView.this;
}
}
@Getter
@EqualsAndHashCode
public class RbacPermissionDefinition {
final EntityAlias entityAlias;
final Permission permission;
final String tableName;
final boolean toCreate;
private RbacPermissionDefinition(final EntityAlias entityAlias, final Permission permission, final String tableName, final boolean toCreate) {
this.entityAlias = entityAlias;
this.permission = permission;
this.tableName = tableName;
this.toCreate = toCreate;
permDefs.add(this);
}
public RbacView grantedTo(final String entityAlias, final Role role) {
findOrCreateGrantDef(this, findRbacRole(entityAlias, role)).toCreate();
return RbacView.this;
}
@Override
public String toString() {
return "perm:" + entityAlias.aliasName + permission + ofNullable(tableName).map(tn -> ":" + tn).orElse("");
}
}
@Getter
@EqualsAndHashCode
public class RbacRoleDefinition {
private final EntityAlias entityAlias;
private final Role role;
private boolean toCreate;
public RbacRoleDefinition(final EntityAlias entityAlias, final Role role) {
this.entityAlias = entityAlias;
this.role = role;
roleDefs.add(this);
}
public RbacRoleDefinition toCreate() {
this.toCreate = true;
return this;
}
public RbacGrantDefinition owningUser(final RbacUserReference.UserRole userRole) {
return grantRoleToUser(this, findUserRef(userRole));
}
public RbacGrantDefinition permission(final Permission permission) {
return grantPermissionToRole(createPermission(entityAlias, permission), this);
}
public RbacGrantDefinition incomingSuperRole(final String entityAlias, final Role role) {
final var incomingSuperRole = findRbacRole(entityAlias, role);
return grantSubRoleToSuperRole(this, incomingSuperRole);
}
public RbacGrantDefinition outgoingSubRole(final String entityAlias, final Role role) {
final var outgoingSubRole = findRbacRole(entityAlias, role);
return grantSubRoleToSuperRole(outgoingSubRole, this);
}
@Override
public String toString() {
return "role:" + entityAlias.aliasName + role;
}
}
public RbacUserReference findUserRef(final RbacUserReference.UserRole userRole) {
return userDefs.stream().filter(u -> u.role == userRole).findFirst().orElseThrow();
}
@EqualsAndHashCode
public class RbacUserReference {
public enum UserRole {
GLOBAL_ADMIN,
CREATOR
}
final UserRole role;
public RbacUserReference(final UserRole creator) {
this.role = creator;
userDefs.add(this);
}
@Override
public String toString() {
return "user:" + role;
}
}
EntityAlias findEntityAlias(final String aliasName) {
final var found = entityAliases.get(aliasName);
if (found == null) {
throw new IllegalArgumentException("entityAlias not found: " + aliasName);
}
return found;
}
RbacRoleDefinition findRbacRole(final EntityAlias entityAlias, final Role role) {
return roleDefs.stream()
.filter(r -> r.getEntityAlias() == entityAlias && r.getRole().equals(role))
.findFirst()
.orElseGet(() -> new RbacRoleDefinition(entityAlias, role));
}
public RbacRoleDefinition findRbacRole(final String entityAliasName, final Role role) {
return findRbacRole(findEntityAlias(entityAliasName), role);
}
RbacPermissionDefinition findRbacPerm(final EntityAlias entityAlias, final Permission perm, String tableName) {
return permDefs.stream()
.filter(p -> p.getEntityAlias() == entityAlias && p.getPermission() == perm)
.findFirst()
.orElseGet(() -> new RbacPermissionDefinition(entityAlias, perm, tableName, true)); // TODO: true => toCreate
}
RbacPermissionDefinition findRbacPerm(final EntityAlias entityAlias, final Permission perm) {
return findRbacPerm(entityAlias, perm, null);
}
public RbacPermissionDefinition findRbacPerm(final String entityAliasName, final Permission perm, String tableName) {
return findRbacPerm(findEntityAlias(entityAliasName), perm, tableName);
}
public RbacPermissionDefinition findRbacPerm(final String entityAliasName, final Permission perm) {
return findRbacPerm(findEntityAlias(entityAliasName), perm);
}
private RbacGrantDefinition findOrCreateGrantDef(final RbacRoleDefinition roleDefinition, final RbacUserReference user) {
return grantDefs.stream()
.filter(g -> g.subRoleDef == roleDefinition && g.userDef == user)
.findFirst()
.orElseGet(() -> new RbacGrantDefinition(roleDefinition, user));
}
private RbacGrantDefinition findOrCreateGrantDef(final RbacPermissionDefinition permDef, final RbacRoleDefinition roleDef) {
return grantDefs.stream()
.filter(g -> g.permDef == permDef && g.subRoleDef == roleDef)
.findFirst()
.orElseGet(() -> new RbacGrantDefinition(permDef, roleDef));
}
private RbacGrantDefinition findOrCreateGrantDef(
final RbacRoleDefinition subRoleDefinition,
final RbacRoleDefinition superRoleDefinition) {
return grantDefs.stream()
.filter(g -> g.subRoleDef == subRoleDefinition && g.superRoleDef == superRoleDefinition)
.findFirst()
.orElseGet(() -> new RbacGrantDefinition(subRoleDefinition, superRoleDefinition));
}
record EntityAlias(String aliasName, Class<? extends RbacObject> entityClass, SQL fetchSql, Column dependsOnColum, boolean isSubEntity) {
public EntityAlias(final String aliasName) {
this(aliasName, null, null, null, false);
}
public EntityAlias(final String aliasName, final Class<? extends RbacObject> entityClass) {
this(aliasName, entityClass, null, null, false);
}
boolean isGlobal() {
return aliasName().equals("global");
}
boolean isPlaceholder() {
return entityClass == null;
}
@NotNull
@Override
public SQL fetchSql() {
if (fetchSql == null) {
return SQL.noop();
}
return switch (fetchSql.part) {
case SQL_QUERY -> fetchSql;
case AUTO_FETCH ->
SQL.query("SELECT * FROM " + getRawTableName() + " WHERE uuid = ${ref}." + dependsOnColum.column);
default -> throw new IllegalStateException("unexpected SQL definition: " + fetchSql);
};
}
public boolean hasFetchSql() {
return fetchSql != null;
}
private String withoutEntitySuffix(final String simpleEntityName) {
return simpleEntityName.substring(0, simpleEntityName.length() - "Entity".length());
}
String simpleName() {
return isGlobal()
? aliasName
: uncapitalize(withoutEntitySuffix(entityClass.getSimpleName()));
}
String getRawTableName() {
if ( aliasName.equals("global")) {
return "global"; // TODO: maybe we should introduce a GlobalEntity class?
}
return withoutRvSuffix(entityClass.getAnnotation(Table.class).name());
}
String dependsOnColumName() {
if (dependsOnColum == null) {
throw new IllegalStateException(
"Entity " + aliasName + "(" + entityClass.getSimpleName() + ")" + ": please add dependsOnColum");
}
return dependsOnColum.column;
}
}
public static String withoutRvSuffix(final String tableName) {
return tableName.substring(0, tableName.length() - "_rv".length());
}
public record Role(String roleName) {
public static final Role OWNER = new Role("owner");
public static final Role ADMIN = new Role("admin");
public static final Role AGENT = new Role("agent");
public static final Role TENANT = new Role("tenant");
public static final Role REFERRER = new Role("referrer");
@Override
public String toString() {
return ":" + roleName;
}
@Override
public boolean equals(final Object obj) {
return ((obj instanceof Role) && ((Role) obj).roleName.equals(this.roleName));
}
}
public record Permission(String permission) {
public static final Permission INSERT = new Permission("INSERT");
public static final Permission DELETE = new Permission("DELETE");
public static final Permission UPDATE = new Permission("UPDATE");
public static final Permission SELECT = new Permission("SELECT");
public static Permission custom(final String permission) {
return new Permission(permission);
}
@Override
public String toString() {
return ":" + permission;
}
}
public static class SQL {
/**
* DSL method to specify an SQL SELECT expression which fetches the related entity,
* using the reference `${ref}` of the root entity.
* `${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`.
*
* @param sql an SQL SELECT expression (not ending with ';)
* @return the wrapped SQL expression
*/
public static SQL fetchedBySql(final String sql) {
validateExpression(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);
}
/**
* DSL method to explicitly specify that there is no SQL query.
*
* @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 ';)
* @return the wrapped SQL expression
*/
public static SQL query(final String sql) {
validateExpression(sql);
return new SQL(sql, Part.SQL_QUERY);
}
/**
* 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);
}
public static SQL expression(final String sqlExpression) {
// TODO: validate
return new SQL(sqlExpression, Part.SQL_EXPRESSION);
}
enum Part {
NOOP,
SQL_QUERY,
AUTO_FETCH,
SQL_PROJECTION,
SQL_EXPRESSION
}
final String sql;
final Part part;
private SQL(final String sql, final Part part) {
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) {
if (sql.matches(";[ \t]*$")) {
throw new IllegalArgumentException("SQL expression must not end with ';': " + sql);
}
}
}
public static class Column {
public static Column dependsOnColumn(final String column) {
return new Column(column);
}
public final String column;
private Column(final String column) {
this.column = column;
}
}
private static class AliasNameMapper {
private final RbacView importedRbacView;
private final String outerAliasName;
private final Set<String> outerAliasNames;
AliasNameMapper(final RbacView importedRbacView, final String outerAliasName, final Set<String> outerAliasNames) {
this.importedRbacView = importedRbacView;
this.outerAliasName = outerAliasName;
this.outerAliasNames = (outerAliasNames == null) ? Collections.emptySet() : outerAliasNames;
}
String map(final String originalAliasName) {
if (outerAliasNames.contains(originalAliasName) || originalAliasName.equals("global")) {
return originalAliasName;
}
if (originalAliasName.equals(importedRbacView.rootEntityAlias.aliasName)) {
return outerAliasName;
}
return outerAliasName + "." + originalAliasName;
}
}
public static void main(String[] args) {
Stream.of(
TestCustomerEntity.class,
TestPackageEntity.class,
TestDomainEntity.class,
HsOfficePersonEntity.class,
HsOfficePartnerEntity.class,
HsOfficePartnerDetailsEntity.class,
HsOfficeBankAccountEntity.class,
HsOfficeDebitorEntity.class,
HsOfficeRelationshipEntity.class,
HsOfficeCoopAssetsTransactionEntity.class,
HsOfficeContactEntity.class,
HsOfficeSepaMandateEntity.class,
HsOfficeCoopSharesTransactionEntity.class,
HsOfficeMembershipEntity.class
).forEach(c -> {
final Method mainMethod = stream(c.getMethods()).filter(
m -> isStatic(m.getModifiers()) && m.getName().equals("main")
)
.findFirst()
.orElse(null);
if (mainMethod != null) {
try {
mainMethod.invoke(null, new Object[] { null });
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
} else {
System.err.println("no main method in: " + c.getName());
}
});
}
}

View File

@ -0,0 +1,164 @@
package net.hostsharing.hsadminng.rbac.rbacdef;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import java.nio.file.*;
import java.time.LocalDateTime;
import static java.util.stream.Collectors.joining;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.*;
public class RbacViewMermaidFlowchartGenerator {
public static final String HOSTSHARING_DARK_ORANGE = "#dd4901";
public static final String HOSTSHARING_LIGHT_ORANGE = "#feb28c";
public static final String HOSTSHARING_DARK_BLUE = "#274d6e";
public static final String HOSTSHARING_LIGHT_BLUE = "#99bcdb";
private final RbacView rbacDef;
private final StringWriter flowchart = new StringWriter();
public RbacViewMermaidFlowchartGenerator(final RbacView rbacDef) {
this.rbacDef = rbacDef;
flowchart.writeLn("""
%%{init:{'flowchart':{'htmlLabels':false}}}%%
flowchart TB
""");
renderEntitySubgraphs();
renderGrants();
}
private void renderEntitySubgraphs() {
rbacDef.getEntityAliases().values().stream()
.filter(entityAlias -> !rbacDef.isEntityAliasProxy(entityAlias))
.filter(entityAlias -> !entityAlias.isPlaceholder())
.forEach(this::renderEntitySubgraph);
}
private void renderEntitySubgraph(final RbacView.EntityAlias entity) {
final var color = rbacDef.isRootEntityAlias(entity) ? HOSTSHARING_DARK_ORANGE
: entity.isSubEntity() ? HOSTSHARING_LIGHT_ORANGE
: HOSTSHARING_LIGHT_BLUE;
flowchart.writeLn("""
subgraph %{aliasName}["`**%{aliasName}**`"]
direction TB
style %{aliasName} fill:%{fillColor},stroke:%{strokeColor},stroke-width:8px
"""
.replace("%{aliasName}", entity.aliasName())
.replace("%{fillColor}", color )
.replace("%{strokeColor}", HOSTSHARING_DARK_BLUE ));
flowchart.indented( () -> {
rbacDef.getEntityAliases().values().stream()
.filter(e -> e.aliasName().startsWith(entity.aliasName() + "."))
.forEach(this::renderEntitySubgraph);
wrapOutputInSubgraph(entity.aliasName() + ":roles", color,
rbacDef.getRoleDefs().stream()
.filter(r -> r.getEntityAlias() == entity)
.map(this::roleDef)
.collect(joining("\n")));
wrapOutputInSubgraph(entity.aliasName() + ":permissions", color,
rbacDef.getPermDefs().stream()
.filter(p -> p.getEntityAlias() == entity)
.map(this::permDef)
.collect(joining("\n")));
if (rbacDef.isRootEntityAlias(entity) && rbacDef.getRootEntityAliasProxy() != null ) {
renderEntitySubgraph(rbacDef.getRootEntityAliasProxy());
}
});
flowchart.chopEmptyLines();
flowchart.writeLn("end");
flowchart.writeLn();
}
private void wrapOutputInSubgraph(final String name, final String color, final String content) {
if (!StringUtils.isEmpty(content)) {
flowchart.ensureSingleEmptyLine();
flowchart.writeLn("subgraph " + name + "[ ]\n");
flowchart.indented(() -> {
flowchart.writeLn("style %{aliasName} fill:%{fillColor},stroke:white"
.replace("%{aliasName}", name)
.replace("%{fillColor}", color));
flowchart.writeLn();
flowchart.writeLn(content);
});
flowchart.chopEmptyLines();
flowchart.writeLn("end");
flowchart.writeLn();
}
}
private void renderGrants() {
renderGrants(ROLE_TO_USER, "%% granting roles to users");
renderGrants(ROLE_TO_ROLE, "%% granting roles to roles");
renderGrants(PERM_TO_ROLE, "%% granting permissions to roles");
}
private void renderGrants(final RbacView.RbacGrantDefinition.GrantType grantType, final String comment) {
final var grantsOfRequestedType = rbacDef.getGrantDefs().stream()
.filter(g -> g.grantType() == grantType)
.toList();
if ( !grantsOfRequestedType.isEmpty()) {
flowchart.ensureSingleEmptyLine();
flowchart.writeLn(comment);
grantsOfRequestedType.forEach(g -> flowchart.writeLn(grantDef(g)));
}
}
private String grantDef(final RbacView.RbacGrantDefinition grant) {
final var arrow = (grant.isToCreate() ? " ==>" : " -.->")
+ (grant.isAssumed() ? " " : "|XX| ");
return switch (grant.grantType()) {
case ROLE_TO_USER ->
// TODO: other user types not implemented yet
"user:creator" + arrow + roleId(grant.getSubRoleDef());
case ROLE_TO_ROLE ->
roleId(grant.getSuperRoleDef()) + arrow + roleId(grant.getSubRoleDef());
case PERM_TO_ROLE -> roleId(grant.getSuperRoleDef()) + arrow + permId(grant.getPermDef());
};
}
private String permDef(final RbacView.RbacPermissionDefinition perm) {
return permId(perm) + "{{" + perm.getEntityAlias().aliasName() + perm.getPermission() + "}}";
}
private static String permId(final RbacView.RbacPermissionDefinition permDef) {
return "perm:" + permDef.getEntityAlias().aliasName() + permDef.getPermission();
}
private String roleDef(final RbacView.RbacRoleDefinition roleDef) {
return roleId(roleDef) + "[[" + roleDef.getEntityAlias().aliasName() + roleDef.getRole() + "]]";
}
private static String roleId(final RbacView.RbacRoleDefinition r) {
return "role:" + r.getEntityAlias().aliasName() + r.getRole();
}
@Override
public String toString() {
return flowchart.toString();
}
@SneakyThrows
public void generateToMarkdownFile(final Path path) {
Files.writeString(
path,
"""
### rbac %{entityAlias}
This code generated was by RbacViewMermaidFlowchartGenerator at %{timestamp}.
```mermaid
%{flowchart}
```
"""
.replace("%{entityAlias}", rbacDef.getRootEntityAlias().aliasName())
.replace("%{timestamp}", LocalDateTime.now().toString())
.replace("%{flowchart}", flowchart.toString()),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
System.out.println("Markdown-File: " + path.toAbsolutePath());
}
}

View File

@ -0,0 +1,52 @@
package net.hostsharing.hsadminng.rbac.rbacdef;
import lombok.SneakyThrows;
import java.nio.file.Files;
import java.nio.file.Path;
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 {
private final RbacView rbacDef;
private final String liqibaseTagPrefix;
private final StringWriter plPgSql = new StringWriter();
public RbacViewPostgresGenerator(final RbacView forRbacDef) {
rbacDef = forRbacDef;
liqibaseTagPrefix = rbacDef.getRootEntityAlias().getRawTableName().replace("_", "-");
plPgSql.writeLn("""
--liquibase formatted sql
-- This code generated was by ${generator} at ${timestamp}.
""",
with("generator", getClass().getSimpleName()),
with("timestamp", LocalDateTime.now().toString()),
with("ref", NEW.name()));
new RbacObjectGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql);
new RbacRoleDescriptorsGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql);
new RolesGrantsAndPermissionsGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql);
new InsertTriggerGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql);
new RbacIdentityViewGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql);
new RbacRestrictedViewGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql);
}
@Override
public String toString() {
return plPgSql.toString();
}
@SneakyThrows
public void generateToChangeLog(final Path outputPath) {
Files.writeString(
outputPath,
toString(),
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING);
System.out.println(outputPath.toAbsolutePath());
}
}

View File

@ -0,0 +1,507 @@
package net.hostsharing.hsadminng.rbac.rbacdef;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacPermissionDefinition;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toSet;
import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.NEW;
import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.OLD;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT;
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.StringWriter.with;
import static org.apache.commons.lang3.StringUtils.capitalize;
import static org.apache.commons.lang3.StringUtils.uncapitalize;
class RolesGrantsAndPermissionsGenerator {
private final RbacView rbacDef;
private final Set<RbacView.RbacGrantDefinition> rbacGrants = new HashSet<>();
private final String liquibaseTagPrefix;
private final String simpleEntityName;
private final String simpleEntityVarName;
private final String rawTableName;
RolesGrantsAndPermissionsGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) {
this.rbacDef = rbacDef;
this.rbacGrants.addAll(rbacDef.getGrantDefs().stream()
.filter(RbacView.RbacGrantDefinition::isToCreate)
.collect(toSet()));
this.liquibaseTagPrefix = liquibaseTagPrefix;
simpleEntityVarName = rbacDef.getRootEntityAlias().simpleName();
simpleEntityName = capitalize(simpleEntityVarName);
rawTableName = rbacDef.getRootEntityAlias().getRawTableName();
}
void generateTo(final StringWriter plPgSql) {
generateInsertTrigger(plPgSql);
if (hasAnyUpdatableEntityAliases()) {
generateUpdateTrigger(plPgSql);
}
}
private void generateHeader(final StringWriter plPgSql, final String triggerType) {
plPgSql.writeLn("""
-- ============================================================================
--changeset ${liquibaseTagPrefix}-rbac-${triggerType}-trigger:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
""",
with("liquibaseTagPrefix", liquibaseTagPrefix),
with("triggerType", triggerType));
}
private void generateInsertTriggerFunction(final StringWriter plPgSql) {
plPgSql.writeLn("""
/*
Creates the roles, grants and permission for the AFTER INSERT TRIGGER.
*/
create or replace procedure buildRbacSystemFor${simpleEntityName}(
NEW ${rawTableName}
)
language plpgsql as $$
declare
"""
.replace("${simpleEntityName}", simpleEntityName)
.replace("${rawTableName}", rawTableName));
plPgSql.chopEmptyLines();
plPgSql.indented(() -> {
referencedEntityAliases()
.forEach((ea) -> plPgSql.writeLn(entityRefVar(NEW, ea) + " " + ea.getRawTableName() + ";"));
});
plPgSql.writeLn();
plPgSql.writeLn("begin");
plPgSql.indented(() -> {
plPgSql.writeLn("call enterTriggerForObjectUuid(NEW.uuid);");
generateCreateRolesAndGrantsAfterInsert(plPgSql);
plPgSql.ensureSingleEmptyLine();
plPgSql.writeLn("call leaveTriggerForObjectUuid(NEW.uuid);");
});
plPgSql.writeLn("end; $$;");
plPgSql.writeLn();
}
private void generateUpdateTriggerFunction(final StringWriter plPgSql) {
plPgSql.writeLn("""
/*
Called from the AFTER UPDATE TRIGGER to re-wire the grants.
*/
create or replace procedure updateRbacRulesFor${simpleEntityName}(
OLD ${rawTableName},
NEW ${rawTableName}
)
language plpgsql as $$
declare
"""
.replace("${simpleEntityName}", simpleEntityName)
.replace("${rawTableName}", rawTableName));
plPgSql.chopEmptyLines();
plPgSql.indented(() -> {
updatableEntityAliases()
.forEach((ea) -> {
plPgSql.writeLn(entityRefVar(OLD, ea) + " " + ea.getRawTableName() + ";");
plPgSql.writeLn(entityRefVar(NEW, ea) + " " + ea.getRawTableName() + ";");
});
});
plPgSql.writeLn();
plPgSql.writeLn("begin");
plPgSql.indented(() -> {
plPgSql.writeLn("call enterTriggerForObjectUuid(NEW.uuid);");
generateUpdateRolesAndGrantsAfterUpdate(plPgSql);
plPgSql.ensureSingleEmptyLine();
plPgSql.writeLn("call leaveTriggerForObjectUuid(NEW.uuid);");
});
plPgSql.writeLn("end; $$;");
plPgSql.writeLn();
}
private boolean hasAnyUpdatableEntityAliases() {
return updatableEntityAliases().anyMatch(e -> true);
}
private void generateCreateRolesAndGrantsAfterInsert(final StringWriter plPgSql) {
referencedEntityAliases()
.forEach((ea) -> plPgSql.writeLn(
ea.fetchSql().sql + " into " + entityRefVar(NEW, ea) + ";",
with("ref", NEW.name())));
createRolesWithGrantsSql(plPgSql, OWNER);
createRolesWithGrantsSql(plPgSql, ADMIN);
createRolesWithGrantsSql(plPgSql, AGENT);
createRolesWithGrantsSql(plPgSql, TENANT);
createRolesWithGrantsSql(plPgSql, REFERRER);
generateGrants(plPgSql, ROLE_TO_USER);
generateGrants(plPgSql, ROLE_TO_ROLE);
generateGrants(plPgSql, PERM_TO_ROLE);
}
private Stream<RbacView.EntityAlias> referencedEntityAliases() {
return rbacDef.getEntityAliases().values().stream()
.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));
}
private void generateUpdateRolesAndGrantsAfterUpdate(final StringWriter plPgSql) {
plPgSql.ensureSingleEmptyLine();
updatableEntityAliases()
.forEach((ea) -> {
plPgSql.writeLn(
ea.fetchSql().sql + " into " + entityRefVar(OLD, ea) + ";",
with("ref", OLD.name()));
plPgSql.writeLn(
ea.fetchSql().sql + " into " + entityRefVar(NEW, ea) + ";",
with("ref", NEW.name()));
});
updatableEntityAliases()
.map(RbacView.EntityAlias::dependsOnColum)
.map(c -> c.column)
.sorted()
.distinct()
.forEach(columnName -> {
plPgSql.writeLn();
plPgSql.writeLn("if NEW." + columnName + " <> OLD." + columnName + " then");
plPgSql.indented(() -> {
updateGrantsDependingOn(plPgSql, columnName);
});
plPgSql.writeLn("end if;");
});
}
private boolean isUpdatable(final RbacView.Column c) {
return rbacDef.getUpdatableColumns().contains(c);
}
private void updateGrantsDependingOn(final StringWriter plPgSql, final String columnName) {
rbacDef.getGrantDefs().stream()
.filter(RbacView.RbacGrantDefinition::isToCreate)
.filter(g -> g.dependsOnColumn(columnName))
.forEach(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.ensureSingleEmptyLine();
rbacGrants.stream()
.filter(g -> g.grantType() == grantType)
.map(this::generateGrant)
.sorted()
.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}", findPerm(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}${assumed});"
.replace("${assumed}", grantDef.isAssumed() ? "" : ", unassumed()")
.replace("${subRoleRef}", roleRef(NEW, grantDef.getSubRoleDef()))
.replace("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef()));
case PERM_TO_ROLE ->
grantDef.getPermDef().getPermission() == INSERT ? ""
: "call grantPermissionToRole(${permRef}, ${superRoleRef});"
.replace("${permRef}", createPerm(NEW, grantDef.getPermDef()))
.replace("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef()));
};
}
private String findPerm(final PostgresTriggerReference ref, final RbacPermissionDefinition permDef) {
return permRef("findPermissionId", ref, permDef);
}
private String createPerm(final PostgresTriggerReference ref, final RbacPermissionDefinition permDef) {
return permRef("createPermission", ref, permDef);
}
private String permRef(final String functionName, final PostgresTriggerReference ref, final RbacPermissionDefinition permDef) {
return "${prefix}(${entityRef}.uuid, '${perm}')"
.replace("${prefix}", functionName)
.replace("${entityRef}", rbacDef.isRootEntityAlias(permDef.entityAlias)
? ref.name()
: refVarName(ref, permDef.entityAlias))
.replace("${perm}", permDef.permission.permission());
}
private String refVarName(final PostgresTriggerReference ref, final RbacView.EntityAlias entityAlias) {
return ref.name().toLowerCase() + capitalize(entityAlias.aliasName());
}
private String roleRef(final PostgresTriggerReference rootRefVar, final RbacView.RbacRoleDefinition roleDef) {
if (roleDef == null) {
System.out.println("null");
}
if (roleDef.getEntityAlias().isGlobal()) {
return "globalAdmin()";
}
final String entityRefVar = entityRefVar(rootRefVar, roleDef.getEntityAlias());
return roleDef.getEntityAlias().simpleName() + capitalize(roleDef.getRole().roleName())
+ "(" + entityRefVar + ")";
}
private String entityRefVar(
final PostgresTriggerReference rootRefVar,
final RbacView.EntityAlias entityAlias) {
return rbacDef.isRootEntityAlias(entityAlias)
? rootRefVar.name()
: rootRefVar.name().toLowerCase() + capitalize(entityAlias.aliasName());
}
private void createRolesWithGrantsSql(final StringWriter plPgSql, final RbacView.Role role) {
final var isToCreate = rbacDef.getRoleDefs().stream()
.filter(roleDef -> rbacDef.isRootEntityAlias(roleDef.getEntityAlias()) && roleDef.getRole() == role)
.findFirst().map(RbacView.RbacRoleDefinition::isToCreate).orElse(false);
if (!isToCreate) {
return;
}
plPgSql.writeLn();
plPgSql.writeLn("perform createRoleWithGrants(");
plPgSql.indented(() -> {
plPgSql.writeLn("${simpleVarName)${roleSuffix}(NEW),"
.replace("${simpleVarName)", simpleEntityVarName)
.replace("${roleSuffix}", capitalize(role.roleName())));
generatePermissionsForRole(plPgSql, role);
generateUserGrantsForRole(plPgSql, role);
generateIncomingSuperRolesForRole(plPgSql, role);
generateOutgoingSubRolesForRole(plPgSql, role);
plPgSql.chopTail(",\n");
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 + "'")
.sorted()
.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 arrayElements = incomingGrants.stream()
.map(g -> toPlPgSqlReference(NEW, g.getSuperRoleDef(), g.isAssumed()))
.toList();
plPgSql.indented(() ->
plPgSql.writeLn("incomingSuperRoles => array[" + joinArrayElements(arrayElements, 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(g -> toPlPgSqlReference(NEW, g.getSubRoleDef(), g.isAssumed()))
.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) {
final var roleDef = rbacDef.findRbacRole(entityAlias, role);
return rbacGrants.stream()
.filter(g -> g.grantType() == PERM_TO_ROLE && g.getSuperRoleDef() == roleDef)
.collect(toSet());
}
private Set<RbacView.RbacGrantDefinition> findGrantsToUserForRole(
final RbacView.EntityAlias entityAlias,
final RbacView.Role role) {
final var roleDef = rbacDef.findRbacRole(entityAlias, role);
return rbacGrants.stream()
.filter(g -> g.grantType() == ROLE_TO_USER && g.getSubRoleDef() == roleDef)
.collect(toSet());
}
private Set<RbacView.RbacGrantDefinition> findIncomingSuperRolesForRole(
final RbacView.EntityAlias entityAlias,
final RbacView.Role role) {
final var roleDef = rbacDef.findRbacRole(entityAlias, role);
return rbacGrants.stream()
.filter(g -> g.grantType() == ROLE_TO_ROLE && g.getSubRoleDef() == roleDef)
.collect(toSet());
}
private Set<RbacView.RbacGrantDefinition> findOutgoingSuperRolesForRole(
final RbacView.EntityAlias entityAlias,
final RbacView.Role role) {
final var roleDef = rbacDef.findRbacRole(entityAlias, role);
return rbacGrants.stream()
.filter(g -> g.grantType() == ROLE_TO_ROLE && g.getSuperRoleDef() == roleDef)
.filter(g -> g.getSubRoleDef().getEntityAlias() != entityAlias)
.collect(toSet());
}
private void generateInsertTrigger(final StringWriter plPgSql) {
generateHeader(plPgSql, "insert");
generateInsertTriggerFunction(plPgSql);
plPgSql.writeLn("""
/*
AFTER INSERT TRIGGER to create the role+grant structure for a new ${rawTableName} row.
*/
create or replace function insertTriggerFor${simpleEntityName}_tf()
returns trigger
language plpgsql
strict as $$
begin
call buildRbacSystemFor${simpleEntityName}(NEW);
return NEW;
end; $$;
create trigger insertTriggerFor${simpleEntityName}_tg
after insert on ${rawTableName}
for each row
execute procedure insertTriggerFor${simpleEntityName}_tf();
"""
.replace("${simpleEntityName}", simpleEntityName)
.replace("${rawTableName}", rawTableName)
);
generateFooter(plPgSql);
}
private void generateUpdateTrigger(final StringWriter plPgSql) {
generateHeader(plPgSql, "update");
generateUpdateTriggerFunction(plPgSql);
plPgSql.writeLn("""
/*
AFTER INSERT TRIGGER to re-wire the grant structure for a new ${rawTableName} row.
*/
create or replace function updateTriggerFor${simpleEntityName}_tf()
returns trigger
language plpgsql
strict as $$
begin
call updateRbacRulesFor${simpleEntityName}(OLD, NEW);
return NEW;
end; $$;
create trigger updateTriggerFor${simpleEntityName}_tg
after update on ${rawTableName}
for each row
execute procedure updateTriggerFor${simpleEntityName}_tf();
"""
.replace("${simpleEntityName}", simpleEntityName)
.replace("${rawTableName}", rawTableName)
);
generateFooter(plPgSql);
}
private static void generateFooter(final StringWriter plPgSql) {
plPgSql.writeLn("--//");
plPgSql.writeLn();
}
private String toPlPgSqlReference(final RbacView.RbacUserReference userRef) {
return switch (userRef.role) {
case CREATOR -> "currentUserUuid()";
default -> throw new IllegalArgumentException("unknown user role: " + userRef);
};
}
private String toPlPgSqlReference(
final PostgresTriggerReference triggerRef,
final RbacView.RbacRoleDefinition roleDef,
final boolean assumed) {
final var assumedArg = assumed ? "" : ", unassumed()";
return toRoleRef(roleDef) +
(roleDef.getEntityAlias().isGlobal() ? ( assumed ? "()" : "(unassumed())")
: rbacDef.isRootEntityAlias(roleDef.getEntityAlias()) ? ("(" + triggerRef.name() + ")")
: "(" + toTriggerReference(triggerRef, roleDef.getEntityAlias()) + assumedArg + ")");
}
private static String toRoleRef(final RbacView.RbacRoleDefinition roleDef) {
return uncapitalize(roleDef.getEntityAlias().simpleName()) + capitalize(roleDef.getRole().roleName());
}
private static String toTriggerReference(
final PostgresTriggerReference triggerRef,
final RbacView.EntityAlias entityAlias) {
return triggerRef.name().toLowerCase() + capitalize(entityAlias.aliasName());
}
}

View File

@ -0,0 +1,111 @@
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;
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(final String text, final VarDef... varDefs) {
string.append( indented( new VarReplacer(varDefs).apply(text) ));
writeLn();
}
void writeLn() {
string.append( "\n");
}
void indent() {
++indentLevel;
}
void unindent() {
--indentLevel;
}
void indented(final Runnable indented) {
indent();
indented.run();
unindent();
}
boolean chopTail(final String tail) {
if (string.toString().endsWith(tail)) {
string.setLength(string.length() - tail.length());
return true;
}
return false;
}
void chopEmptyLines() {
while (string.toString().endsWith("\n\n")) {
string.setLength(string.length() - 1);
};
}
void ensureSingleEmptyLine() {
chopEmptyLines();
writeLn();
}
@Override
public String toString() {
return string.toString();
}
public static String indented(final String text, final int indentLevel) {
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;
}
private String indented(final String text) {
if ( indentLevel == 0) {
return text;
}
return indented(text, indentLevel);
}
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 textToAppend) {
try {
text = textToAppend;
stream(varDefs).forEach(varDef -> {
final var pattern = Pattern.compile("\\$\\{" + varDef.name() + "}", Pattern.CASE_INSENSITIVE);
final var matcher = pattern.matcher(text);
text = matcher.replaceAll(varDef.value());
});
return text;
} catch (Exception exc) {
throw exc;
}
}
}
}

View File

@ -0,0 +1,5 @@
package net.hostsharing.hsadminng.rbac.rbacdef;
// TODO: The whole code in this package is more like a quick hack to solve an urgent problem.
// It should be re-written in PostgreSQL pl/pgsql,
// so that no Java is needed to use this RBAC system in it's full extend.

View File

@ -0,0 +1,72 @@
package net.hostsharing.hsadminng.rbac.rbacgrant;
import lombok.*;
import org.springframework.data.annotation.Immutable;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import java.util.List;
import java.util.UUID;
@Entity
@Table(name = "rbacgrants_ev")
@Getter
@Setter
@Builder
@ToString
@Immutable
@NoArgsConstructor
@AllArgsConstructor
public class RawRbacGrantEntity implements Comparable {
@Id
private UUID uuid;
@Column(name = "grantedbyroleidname", updatable = false, insertable = false)
private String grantedByRoleIdName;
@Column(name = "grantedbyroleuuid", updatable = false, insertable = false)
private UUID grantedByRoleUuid;
@Column(name = "ascendantidname", updatable = false, insertable = false)
private String ascendantIdName;
@Column(name = "ascendantuuid", updatable = false, insertable = false)
private UUID ascendingUuid;
@Column(name = "descendantidname", updatable = false, insertable = false)
private String descendantIdName;
@Column(name = "descendantuuid", updatable = false, insertable = false)
private UUID descendantUuid;
@Column(name = "assumed", updatable = false, insertable = false)
private boolean assumed;
public String toDisplay() {
// @formatter:off
return "{ grant " + descendantIdName +
" to " + ascendantIdName +
" by " + ( grantedByRoleUuid == null
? "system"
: grantedByRoleIdName ) +
( assumed ? " and assume" : "") +
" }";
// @formatter:on
}
@NotNull
public static List<String> distinctGrantDisplaysOf(final List<RawRbacGrantEntity> roles) {
// TODO: remove .distinct() once partner.person + partner.contact are removed
return roles.stream().map(RawRbacGrantEntity::toDisplay).sorted().distinct().toList();
}
@Override
public int compareTo(final Object o) {
return uuid.compareTo(((RawRbacGrantEntity)o).uuid);
}
}

View File

@ -0,0 +1,15 @@
package net.hostsharing.hsadminng.rbac.rbacgrant;
import org.springframework.data.repository.Repository;
import java.util.List;
import java.util.UUID;
public interface RawRbacGrantRepository extends Repository<RawRbacGrantEntity, UUID> {
List<RawRbacGrantEntity> findAll();
List<RawRbacGrantEntity> findByAscendingUuid(UUID ascendingUuid);
List<RawRbacGrantEntity> findByDescendantUuid(UUID refUuid);
}

View File

@ -0,0 +1,206 @@
package net.hostsharing.hsadminng.rbac.rbacgrant;
import net.hostsharing.hsadminng.context.Context;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.validation.constraints.NotNull;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.*;
import java.util.stream.Stream;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.joining;
import static net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService.Include.*;
// TODO: cleanup - this code was 'hacked' to quickly fix a specific problem, needs refactoring
@Service
public class RbacGrantsDiagramService {
public static void writeToFile(final String title, final String graph, final String fileName) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))) {
writer.write("""
### all grants to %s
```mermaid
%s
```
""".formatted(title, graph));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public enum Include {
DETAILS,
USERS,
PERMISSIONS,
NOT_ASSUMED,
TEST_ENTITIES,
NON_TEST_ENTITIES
}
@Autowired
private Context context;
@Autowired
private RawRbacGrantRepository rawGrantRepo;
@PersistenceContext
private EntityManager em;
public String allGrantsToCurrentUser(final EnumSet<Include> includes) {
final var graph = new HashSet<RawRbacGrantEntity>();
for ( UUID subjectUuid: context.currentSubjectsUuids() ) {
traverseGrantsTo(graph, subjectUuid, includes);
}
return toMermaidFlowchart(graph, includes);
}
private void traverseGrantsTo(final Set<RawRbacGrantEntity> graph, final UUID refUuid, final EnumSet<Include> includes) {
final var grants = rawGrantRepo.findByAscendingUuid(refUuid);
grants.forEach(g -> {
if (!includes.contains(PERMISSIONS) && g.getDescendantIdName().startsWith("perm ")) {
return;
}
if ( !g.getDescendantIdName().startsWith("role global")) {
if (!includes.contains(TEST_ENTITIES) && g.getDescendantIdName().contains(" test_")) {
return;
}
if (!includes.contains(NON_TEST_ENTITIES) && !g.getDescendantIdName().contains(" test_")) {
return;
}
}
graph.add(g);
if (includes.contains(NOT_ASSUMED) || g.isAssumed()) {
traverseGrantsTo(graph, g.getDescendantUuid(), includes);
}
});
}
public String allGrantsFrom(final UUID targetObject, final String op, final EnumSet<Include> includes) {
final var refUuid = (UUID) em.createNativeQuery("SELECT uuid FROM rbacpermission WHERE objectuuid=:targetObject AND op=:op")
.setParameter("targetObject", targetObject)
.setParameter("op", op)
.getSingleResult();
final var graph = new HashSet<RawRbacGrantEntity>();
traverseGrantsFrom(graph, refUuid, includes);
return toMermaidFlowchart(graph, includes);
}
private void traverseGrantsFrom(final Set<RawRbacGrantEntity> graph, final UUID refUuid, final EnumSet<Include> option) {
final var grants = rawGrantRepo.findByDescendantUuid(refUuid);
grants.forEach(g -> {
if (!option.contains(USERS) && g.getAscendantIdName().startsWith("user ")) {
return;
}
graph.add(g);
if (option.contains(NOT_ASSUMED) || g.isAssumed()) {
traverseGrantsFrom(graph, g.getAscendingUuid(), option);
}
});
}
private String toMermaidFlowchart(final HashSet<RawRbacGrantEntity> graph, final EnumSet<Include> includes) {
final var entities =
includes.contains(DETAILS)
? graph.stream()
.flatMap(g -> Stream.of(
new Node(g.getAscendantIdName(), g.getAscendingUuid()),
new Node(g.getDescendantIdName(), g.getDescendantUuid()))
)
.collect(groupingBy(RbacGrantsDiagramService::renderEntityIdName))
.entrySet().stream()
.map(entity -> "subgraph " + quoted(entity.getKey()) + renderSubgraph(entity.getKey()) + "\n\n "
+ entity.getValue().stream()
.map(n -> renderNode(n.idName(), n.uuid()).replace("\n", "\n "))
.sorted()
.distinct()
.collect(joining("\n\n ")))
.collect(joining("\n\nend\n\n"))
+ "\n\nend\n\n"
: "";
final var grants = graph.stream()
.map(g -> quoted(g.getAscendantIdName())
+ " -->" + (g.isAssumed() ? " " : "|XX| ")
+ quoted(g.getDescendantIdName()))
.sorted()
.collect(joining("\n"));
final var avoidCroppedNodeLabels = "%%{init:{'flowchart':{'htmlLabels':false}}}%%\n\n";
return (includes.contains(DETAILS) ? avoidCroppedNodeLabels : "")
+ "flowchart TB\n\n"
+ entities
+ grants;
}
private String renderSubgraph(final String entityId) {
// this does not work according to Mermaid bug https://github.com/mermaid-js/mermaid/issues/3806
// if (entityId.contains("#")) {
// final var parts = entityId.split("#");
// final var table = parts[0];
// final var entity = parts[1];
// if (table.equals("entity")) {
// return "[" + entity "]";
// }
// return "[" + table + "\n" + entity + "]";
// }
return "[" + entityId + "]";
}
private static String renderEntityIdName(final Node node) {
final var refType = refType(node.idName());
if (refType.equals("user")) {
return "users";
}
if (refType.equals("perm")) {
return node.idName().split(" ", 4)[3];
}
if (refType.equals("role")) {
final var withoutRolePrefix = node.idName().substring("role:".length());
return withoutRolePrefix.substring(0, withoutRolePrefix.lastIndexOf('.'));
}
throw new IllegalArgumentException("unknown refType '" + refType + "' in '" + node.idName() + "'");
}
private String renderNode(final String idName, final UUID uuid) {
return quoted(idName) + renderNodeContent(idName, uuid);
}
private String renderNodeContent(final String idName, final UUID uuid) {
final var refType = refType(idName);
if (refType.equals("user")) {
final var displayName = idName.substring(refType.length()+1);
return "(" + displayName + "\nref:" + uuid + ")";
}
if (refType.equals("role")) {
final var roleType = idName.substring(idName.lastIndexOf('.') + 1);
return "[" + roleType + "\nref:" + uuid + "]";
}
if (refType.equals("perm")) {
final var roleType = idName.split(" ")[1];
return "{{" + roleType + "\nref:" + uuid + "}}";
}
return "";
}
private static String refType(final String idName) {
return idName.split(" ", 2)[0];
}
@NotNull
private static String quoted(final String idName) {
return idName.replace(" ", ":").replaceAll("@.*", "");
}
}
record Node(String idName, UUID uuid) {
}

View File

@ -0,0 +1,8 @@
package net.hostsharing.hsadminng.rbac.rbacobject;
import java.util.UUID;
public interface RbacObject {
UUID getUuid();
}

View File

@ -8,8 +8,8 @@ public interface RbacUserPermission {
String getRoleName();
UUID getPermissionUuid();
String getOp();
String getOpTableName();
String getObjectTable();
String getObjectIdName();
UUID getObjectUuid();
}

View File

@ -10,6 +10,8 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.util.List;
@RestController
@ -24,6 +26,9 @@ public class TestCustomerController implements TestCustomersApi {
@Autowired
private TestCustomerRepository testCustomerRepository;
@PersistenceContext
EntityManager em;
@Override
@Transactional(readOnly = true)
public ResponseEntity<List<TestCustomerResource>> listCustomers(
@ -48,7 +53,6 @@ public class TestCustomerController implements TestCustomersApi {
context.define(currentUser, assumedRoles);
final var saved = testCustomerRepository.save(mapper.map(customer, TestCustomerEntity.class));
final var uri =
MvcUriComponentsBuilder.fromController(getClass())
.path("/api/test/customers/{id}")

View File

@ -4,17 +4,27 @@ import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
import jakarta.persistence.*;
import java.io.IOException;
import java.util.UUID;
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.rbacViewFor;
@Entity
@Table(name = "test_customer_rv")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class TestCustomerEntity {
public class TestCustomerEntity implements HasUuid {
@Id
@GeneratedValue
@ -25,4 +35,29 @@ public class TestCustomerEntity {
@Column(name = "adminusername")
private String adminUserName;
public static RbacView rbac() {
return rbacViewFor("customer", TestCustomerEntity.class)
.withIdentityView(SQL.projection("prefix"))
.withRestrictedViewOrderBy(SQL.expression("reference"))
.withUpdatableColumns("reference", "prefix", "adminUserName")
// TODO: do we want explicit specification of parent-independent insert permissions?
// .toRole("global", ADMIN).grantPermission("customer", INSERT)
.createRole(OWNER, (with) -> {
with.owningUser(CREATOR).unassumed();
with.incomingSuperRole(GLOBAL, ADMIN).unassumed();
with.permission(DELETE);
})
.createSubRole(ADMIN, (with) -> {
with.permission(UPDATE);
})
.createSubRole(TENANT, (with) -> {
with.permission(SELECT);
});
}
public static void main(String[] args) throws IOException {
rbac().generateWithBaseFileName("113-test-customer-rbac");
}
}

View File

@ -0,0 +1,73 @@
package net.hostsharing.hsadminng.test.dom;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
import net.hostsharing.hsadminng.test.pac.TestPackageEntity;
import jakarta.persistence.*;
import java.io.IOException;
import java.util.UUID;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.fetchedBySql;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
@Entity
@Table(name = "test_domain_rv")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class TestDomainEntity implements HasUuid {
@Id
@GeneratedValue
private UUID uuid;
@Version
private int version;
@ManyToOne(optional = false)
@JoinColumn(name = "packageuuid")
private TestPackageEntity pac;
private String name;
private String description;
public static RbacView rbac() {
return rbacViewFor("domain", TestDomainEntity.class)
.withIdentityView(SQL.projection("name"))
.withUpdatableColumns("version", "packageUuid", "description")
.importEntityAlias("package", TestPackageEntity.class,
dependsOnColumn("packageUuid"),
fetchedBySql("""
SELECT * FROM test_package p
WHERE p.uuid= ${ref}.packageUuid
"""))
.toRole("package", ADMIN).grantPermission("domain", INSERT)
.createRole(OWNER, (with) -> {
with.incomingSuperRole("package", ADMIN);
with.outgoingSubRole("package", TENANT);
with.permission(DELETE);
with.permission(UPDATE);
})
.createSubRole(ADMIN, (with) -> {
with.outgoingSubRole("package", TENANT);
with.permission(SELECT);
});
}
public static void main(String[] args) throws IOException {
rbac().generateWithBaseFileName("133-test-domain-rbac");
}
}

View File

@ -4,18 +4,28 @@ import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
import net.hostsharing.hsadminng.test.cust.TestCustomerEntity;
import jakarta.persistence.*;
import java.io.IOException;
import java.util.UUID;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*;
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;
@Entity
@Table(name = "test_package_rv")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class TestPackageEntity {
public class TestPackageEntity implements HasUuid {
@Id
@GeneratedValue
@ -31,4 +41,34 @@ public class TestPackageEntity {
private String name;
private String description;
public static RbacView rbac() {
return rbacViewFor("package", TestPackageEntity.class)
.withIdentityView(SQL.projection("name"))
.withUpdatableColumns("version", "customerUuid", "description")
.importEntityAlias("customer", TestCustomerEntity.class,
dependsOnColumn("customerUuid"),
fetchedBySql("""
SELECT * FROM test_customer c
WHERE c.uuid= ${ref}.customerUuid
"""))
.toRole("customer", ADMIN).grantPermission("package", INSERT)
.createRole(OWNER, (with) -> {
with.incomingSuperRole("customer", ADMIN);
with.permission(DELETE);
with.permission(UPDATE);
})
.createSubRole(ADMIN)
.createSubRole(TENANT, (with) -> {
with.outgoingSubRole("customer", TENANT);
with.permission(SELECT);
});
}
public static void main(String[] args) throws IOException {
rbac().generateWithBaseFileName("123-test-package-rbac");
}
}