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:
@@ -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;
|
||||
|
||||
+27
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
+29
-1
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
+78
-2
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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) {
|
||||
|
||||
+49
-1
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
+59
-1
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
+41
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package net.hostsharing.hsadminng.rbac.rbacdef;
|
||||
|
||||
public enum PostgresTriggerReference {
|
||||
NEW, OLD
|
||||
}
|
||||
@@ -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("--//");
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
+30
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+164
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
+507
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package net.hostsharing.hsadminng.rbac.rbacobject;
|
||||
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public interface RbacObject {
|
||||
UUID getUuid();
|
||||
}
|
||||
@@ -8,8 +8,8 @@ public interface RbacUserPermission {
|
||||
String getRoleName();
|
||||
UUID getPermissionUuid();
|
||||
String getOp();
|
||||
String getOpTableName();
|
||||
String getObjectTable();
|
||||
String getObjectIdName();
|
||||
UUID getObjectUuid();
|
||||
|
||||
}
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,22 +23,27 @@ end; $$;
|
||||
Defines the transaction context.
|
||||
*/
|
||||
create or replace procedure defineContext(
|
||||
currentTask varchar,
|
||||
currentRequest varchar = null,
|
||||
currentUser varchar = null,
|
||||
assumedRoles varchar = null
|
||||
currentTask varchar(96),
|
||||
currentRequest text = null,
|
||||
currentUser varchar(63) = null,
|
||||
assumedRoles varchar(256) = null
|
||||
)
|
||||
language plpgsql as $$
|
||||
begin
|
||||
currentTask := coalesce(currentTask, '');
|
||||
assert length(currentTask) <= 96, FORMAT('currentTask must not be longer than 96 characters: "%s"', currentTask);
|
||||
assert length(currentTask) > 8, FORMAT('currentTask must be at least 8 characters long: "%s""', currentTask);
|
||||
execute format('set local hsadminng.currentTask to %L', currentTask);
|
||||
|
||||
currentRequest := coalesce(currentRequest, '');
|
||||
execute format('set local hsadminng.currentRequest to %L', currentRequest);
|
||||
|
||||
currentUser := coalesce(currentUser, '');
|
||||
assert length(currentUser) <= 63, FORMAT('currentUser must not be longer than 63 characters: "%s"', currentUser);
|
||||
execute format('set local hsadminng.currentUser to %L', currentUser);
|
||||
|
||||
assumedRoles := coalesce(assumedRoles, '');
|
||||
assert length(assumedRoles) <= 256, FORMAT('assumedRoles must not be longer than 256 characters: "%s"', assumedRoles);
|
||||
execute format('set local hsadminng.assumedRoles to %L', assumedRoles);
|
||||
|
||||
call contextDefined(currentTask, currentRequest, currentUser, assumedRoles);
|
||||
|
||||
@@ -27,9 +27,9 @@ create table tx_context
|
||||
txId bigint not null,
|
||||
txTimestamp timestamp not null,
|
||||
currentUser varchar(63) not null, -- not the uuid, because users can be deleted
|
||||
assumedRoles varchar not null, -- not the uuids, because roles can be deleted
|
||||
assumedRoles varchar(256) not null, -- not the uuids, because roles can be deleted
|
||||
currentTask varchar(96) not null,
|
||||
currentRequest varchar(512) not null
|
||||
currentRequest text not null
|
||||
);
|
||||
|
||||
create index on tx_context using brin (txTimestamp);
|
||||
|
||||
@@ -86,29 +86,6 @@ create or replace function findRbacUserId(userName varchar)
|
||||
language sql as $$
|
||||
select uuid from RbacUser where name = userName
|
||||
$$;
|
||||
|
||||
create type RbacWhenNotExists as enum ('fail', 'create');
|
||||
|
||||
create or replace function getRbacUserId(userName varchar, whenNotExists RbacWhenNotExists)
|
||||
returns uuid
|
||||
returns null on null input
|
||||
language plpgsql as $$
|
||||
declare
|
||||
userUuid uuid;
|
||||
begin
|
||||
userUuid = findRbacUserId(userName);
|
||||
if (userUuid is null) then
|
||||
if (whenNotExists = 'fail') then
|
||||
raise exception 'RbacUser with name="%" not found', userName;
|
||||
end if;
|
||||
if (whenNotExists = 'create') then
|
||||
userUuid = createRbacUser(userName);
|
||||
end if;
|
||||
end if;
|
||||
return userUuid;
|
||||
end;
|
||||
$$;
|
||||
|
||||
--//
|
||||
|
||||
-- ============================================================================
|
||||
@@ -203,15 +180,33 @@ create type RbacRoleDescriptor as
|
||||
(
|
||||
objectTable varchar(63), -- for human readability and easier debugging
|
||||
objectUuid uuid,
|
||||
roleType RbacRoleType
|
||||
roleType RbacRoleType,
|
||||
assumed boolean
|
||||
);
|
||||
|
||||
create or replace function roleDescriptor(objectTable varchar(63), objectUuid uuid, roleType RbacRoleType)
|
||||
create or replace function assumed()
|
||||
returns boolean
|
||||
stable -- leakproof
|
||||
language sql as $$
|
||||
select true;
|
||||
$$;
|
||||
|
||||
create or replace function unassumed()
|
||||
returns boolean
|
||||
stable -- leakproof
|
||||
language sql as $$
|
||||
select false;
|
||||
$$;
|
||||
|
||||
|
||||
create or replace function roleDescriptor(
|
||||
objectTable varchar(63), objectUuid uuid, roleType RbacRoleType,
|
||||
assumed boolean = true) -- just for DSL readability, belongs actually to the grant
|
||||
returns RbacRoleDescriptor
|
||||
returns null on null input
|
||||
stable -- leakproof
|
||||
language sql as $$
|
||||
select objectTable, objectUuid, roleType::RbacRoleType;
|
||||
select objectTable, objectUuid, roleType::RbacRoleType, assumed;
|
||||
$$;
|
||||
|
||||
create or replace function createRole(roleDescriptor RbacRoleDescriptor)
|
||||
@@ -275,21 +270,17 @@ create or replace function findRoleId(roleDescriptor RbacRoleDescriptor)
|
||||
select uuid from RbacRole where objectUuid = roleDescriptor.objectUuid and roleType = roleDescriptor.roleType;
|
||||
$$;
|
||||
|
||||
create or replace function getRoleId(roleDescriptor RbacRoleDescriptor, whenNotExists RbacWhenNotExists)
|
||||
create or replace function getRoleId(roleDescriptor RbacRoleDescriptor)
|
||||
returns uuid
|
||||
returns null on null input
|
||||
language plpgsql as $$
|
||||
declare
|
||||
roleUuid uuid;
|
||||
begin
|
||||
roleUuid = findRoleId(roleDescriptor);
|
||||
assert roleDescriptor is not null, 'roleDescriptor must not be null';
|
||||
|
||||
roleUuid := findRoleId(roleDescriptor);
|
||||
if (roleUuid is null) then
|
||||
if (whenNotExists = 'fail') then
|
||||
raise exception 'RbacRole "%#%.%" not found', roleDescriptor.objectTable, roleDescriptor.objectUuid, roleDescriptor.roleType;
|
||||
end if;
|
||||
if (whenNotExists = 'create') then
|
||||
roleUuid = createRole(roleDescriptor);
|
||||
end if;
|
||||
raise exception 'RbacRole "%#%.%" not found', roleDescriptor.objectTable, roleDescriptor.objectUuid, roleDescriptor.roleType;
|
||||
end if;
|
||||
return roleUuid;
|
||||
end;
|
||||
@@ -365,38 +356,63 @@ create trigger deleteRbacRolesOfRbacObject_Trigger
|
||||
/*
|
||||
|
||||
*/
|
||||
create domain RbacOp as varchar(67)
|
||||
create domain RbacOp as varchar(67) -- TODO: shorten to 8, once the deprecated values are gone
|
||||
check (
|
||||
VALUE = '*'
|
||||
or VALUE = 'delete'
|
||||
or VALUE = 'edit'
|
||||
or VALUE = 'view'
|
||||
or VALUE = 'assume'
|
||||
VALUE = 'DELETE'
|
||||
or VALUE = 'UPDATE'
|
||||
or VALUE = 'SELECT'
|
||||
or VALUE = 'INSERT'
|
||||
or VALUE = 'ASSUME'
|
||||
-- TODO: all values below are deprecated, use insert with table
|
||||
or VALUE ~ '^add-[a-z]+$'
|
||||
or VALUE ~ '^new-[a-z-]+$'
|
||||
);
|
||||
|
||||
create table RbacPermission
|
||||
(
|
||||
uuid uuid primary key references RbacReference (uuid) on delete cascade,
|
||||
objectUuid uuid not null references RbacObject,
|
||||
op RbacOp not null,
|
||||
uuid uuid primary key references RbacReference (uuid) on delete cascade,
|
||||
objectUuid uuid not null references RbacObject,
|
||||
op RbacOp not null,
|
||||
opTableName varchar(60),
|
||||
unique (objectUuid, op)
|
||||
);
|
||||
|
||||
call create_journal('RbacPermission');
|
||||
|
||||
create or replace function permissionExists(forObjectUuid uuid, forOp RbacOp)
|
||||
returns bool
|
||||
language sql as $$
|
||||
select exists(
|
||||
select op
|
||||
from RbacPermission p
|
||||
where p.objectUuid = forObjectUuid
|
||||
and p.op in ('*', forOp)
|
||||
);
|
||||
$$;
|
||||
create or replace function createPermission(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null)
|
||||
returns uuid
|
||||
language plpgsql as $$
|
||||
declare
|
||||
permissionUuid uuid;
|
||||
begin
|
||||
if (forObjectUuid is null) then
|
||||
raise exception 'forObjectUuid must not be null';
|
||||
end if;
|
||||
if (forOp = 'INSERT' and forOpTableName is null) then
|
||||
raise exception 'INSERT permissions needs forOpTableName';
|
||||
end if;
|
||||
if (forOp <> 'INSERT' and forOpTableName is not null) then
|
||||
raise exception 'forOpTableName must only be specified for ops: [INSERT]'; -- currently no other
|
||||
end if;
|
||||
|
||||
permissionUuid = (select uuid from RbacPermission where objectUuid = forObjectUuid and op = forOp and opTableName = forOpTableName);
|
||||
if (permissionUuid is null) then
|
||||
insert into RbacReference ("type")
|
||||
values ('RbacPermission')
|
||||
returning uuid into permissionUuid;
|
||||
begin
|
||||
insert into RbacPermission (uuid, objectUuid, op, opTableName)
|
||||
values (permissionUuid, forObjectUuid, forOp, forOpTableName);
|
||||
exception
|
||||
when others then
|
||||
raise exception 'insert into RbacPermission (uuid, objectUuid, op, opTableName)
|
||||
values (%, %, %, %);', permissionUuid, forObjectUuid, forOp, forOpTableName;
|
||||
end;
|
||||
end if;
|
||||
return permissionUuid;
|
||||
end; $$;
|
||||
|
||||
-- TODO: deprecated, remove and amend all usages to createPermission
|
||||
create or replace function createPermissions(forObjectUuid uuid, permitOps RbacOp[])
|
||||
returns uuid[]
|
||||
language plpgsql as $$
|
||||
@@ -407,9 +423,6 @@ begin
|
||||
if (forObjectUuid is null) then
|
||||
raise exception 'forObjectUuid must not be null';
|
||||
end if;
|
||||
if (array_length(permitOps, 1) > 1 and '*' = any (permitOps)) then
|
||||
raise exception '"*" operation must not be assigned along with other operations: %', permitOps;
|
||||
end if;
|
||||
|
||||
for i in array_lower(permitOps, 1)..array_upper(permitOps, 1)
|
||||
loop
|
||||
@@ -430,7 +443,19 @@ begin
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function findPermissionId(forObjectUuid uuid, forOp RbacOp)
|
||||
create or replace function findEffectivePermissionId(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null)
|
||||
returns uuid
|
||||
returns null on null input
|
||||
stable -- leakproof
|
||||
language sql as $$
|
||||
select uuid
|
||||
from RbacPermission p
|
||||
where p.objectUuid = forObjectUuid
|
||||
and (forOp = 'SELECT' or p.op = forOp) -- all other RbacOp include 'SELECT'
|
||||
and p.opTableName = forOpTableName
|
||||
$$;
|
||||
|
||||
create or replace function findPermissionId(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null)
|
||||
returns uuid
|
||||
returns null on null input
|
||||
stable -- leakproof
|
||||
@@ -439,23 +464,8 @@ select uuid
|
||||
from RbacPermission p
|
||||
where p.objectUuid = forObjectUuid
|
||||
and p.op = forOp
|
||||
and p.opTableName = forOpTableName
|
||||
$$;
|
||||
|
||||
create or replace function findEffectivePermissionId(forObjectUuid uuid, forOp RbacOp)
|
||||
returns uuid
|
||||
returns null on null input
|
||||
stable -- leakproof
|
||||
language plpgsql as $$
|
||||
declare
|
||||
permissionId uuid;
|
||||
begin
|
||||
permissionId := findPermissionId(forObjectUuid, forOp);
|
||||
if permissionId is null and forOp <> '*' then
|
||||
permissionId := findPermissionId(forObjectUuid, '*');
|
||||
end if;
|
||||
return permissionId;
|
||||
end $$;
|
||||
|
||||
--//
|
||||
|
||||
-- ============================================================================
|
||||
@@ -552,6 +562,18 @@ select exists(
|
||||
);
|
||||
$$;
|
||||
|
||||
create or replace function hasInsertPermission(objectUuid uuid, forOp RbacOp, tableName text )
|
||||
returns BOOL
|
||||
stable -- leakproof
|
||||
language plpgsql as $$
|
||||
declare
|
||||
permissionUuid uuid;
|
||||
begin
|
||||
permissionUuid = findPermissionId(objectUuid, forOp, tableName);
|
||||
return permissionUuid is not null;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function hasGlobalRoleGranted(userUuid uuid)
|
||||
returns bool
|
||||
stable -- leakproof
|
||||
@@ -566,6 +588,27 @@ select exists(
|
||||
);
|
||||
$$;
|
||||
|
||||
create or replace procedure grantPermissionToRole(roleUuid uuid, permissionUuid uuid)
|
||||
language plpgsql as $$
|
||||
begin
|
||||
perform assertReferenceType('roleId (ascendant)', roleUuid, 'RbacRole');
|
||||
perform assertReferenceType('permissionId (descendant)', permissionUuid, 'RbacPermission');
|
||||
|
||||
insert
|
||||
into RbacGrants (grantedByTriggerOf, ascendantUuid, descendantUuid, assumed)
|
||||
values (currentTriggerObjectUuid(), roleUuid, permissionUuid, true)
|
||||
on conflict do nothing; -- allow granting multiple times
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace procedure grantPermissionToRole(roleDesc RbacRoleDescriptor, permissionUuid uuid)
|
||||
language plpgsql as $$
|
||||
begin
|
||||
call grantPermissionToRole(findRoleId(roleDesc), permissionUuid);
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- TODO: deprecated, remove and use grantPermissionToRole(...)
|
||||
create or replace procedure grantPermissionsToRole(roleUuid uuid, permissionIds uuid[])
|
||||
language plpgsql as $$
|
||||
begin
|
||||
@@ -697,7 +740,7 @@ begin
|
||||
select descendantUuid
|
||||
from grants) as granted
|
||||
join RbacPermission perm
|
||||
on granted.descendantUuid = perm.uuid and perm.op in ('*', requiredOp)
|
||||
on granted.descendantUuid = perm.uuid and (requiredOp = 'SELECT' or perm.op = requiredOp)
|
||||
join RbacObject obj on obj.uuid = perm.objectUuid and obj.objectTable = forObjectTable
|
||||
limit maxObjects + 1;
|
||||
|
||||
@@ -789,6 +832,5 @@ do $$
|
||||
create role restricted;
|
||||
grant all privileges on all tables in schema public to restricted;
|
||||
end if;
|
||||
end $$
|
||||
end $$;
|
||||
--//
|
||||
|
||||
|
||||
@@ -30,24 +30,35 @@ begin
|
||||
insert
|
||||
into RbacGrants (grantedByRoleUuid, ascendantUuid, descendantUuid, assumed)
|
||||
values (grantedByRoleUuid, userUuid, roleUuid, doAssume);
|
||||
-- TODO.spec: What should happen on mupltiple grants? What if options (doAssume) are not the same?
|
||||
-- TODO.spec: What should happen on multiple grants? What if options (doAssume) are not the same?
|
||||
-- Most powerful or latest grant wins? What about managed?
|
||||
-- on conflict do nothing; -- allow granting multiple times
|
||||
end; $$;
|
||||
|
||||
create or replace procedure grantRoleToUser(grantedByRoleUuid uuid, grantedRoleUuid uuid, userUuid uuid, doAssume boolean = true)
|
||||
language plpgsql as $$
|
||||
declare
|
||||
grantedByRoleIdName text;
|
||||
grantedRoleIdName text;
|
||||
begin
|
||||
perform assertReferenceType('grantingRoleUuid', grantedByRoleUuid, 'RbacRole');
|
||||
perform assertReferenceType('grantedRoleUuid (descendant)', grantedRoleUuid, 'RbacRole');
|
||||
perform assertReferenceType('userUuid (ascendant)', userUuid, 'RbacUser');
|
||||
|
||||
if NOT isGranted(currentSubjectsUuids(), grantedByRoleUuid) then
|
||||
raise exception '[403] Access to granted-by-role % forbidden for %', grantedByRoleUuid, currentSubjects();
|
||||
end if;
|
||||
assert grantedByRoleUuid is not null, 'grantedByRoleUuid must not be null';
|
||||
assert grantedRoleUuid is not null, 'grantedRoleUuid must not be null';
|
||||
assert userUuid is not null, 'userUuid must not be null';
|
||||
|
||||
if NOT isGranted(currentSubjectsUuids(), grantedByRoleUuid) then
|
||||
select roleIdName from rbacRole_ev where uuid=grantedByRoleUuid into grantedByRoleIdName;
|
||||
raise exception '[403] Access to granted-by-role % (%) forbidden for % (%)',
|
||||
grantedByRoleIdName, grantedByRoleUuid, currentSubjects(), currentSubjectsUuids();
|
||||
end if;
|
||||
if NOT isGranted(grantedByRoleUuid, grantedRoleUuid) then
|
||||
raise exception '[403] Access to granted role % forbidden for %', grantedRoleUuid, currentSubjects();
|
||||
select roleIdName from rbacRole_ev where uuid=grantedByRoleUuid into grantedByRoleIdName;
|
||||
select roleIdName from rbacRole_ev where uuid=grantedRoleUuid into grantedRoleIdName;
|
||||
raise exception '[403] Access to granted role % (%) forbidden for % (%)',
|
||||
grantedRoleIdName, grantedRoleUuid, grantedByRoleIdName, grantedByRoleUuid;
|
||||
end if;
|
||||
|
||||
insert
|
||||
@@ -99,4 +110,17 @@ begin
|
||||
where g.ascendantUuid = userUuid and g.descendantUuid = grantedRoleUuid
|
||||
and g.grantedByRoleUuid = revokeRoleFromUser.grantedByRoleUuid;
|
||||
end; $$;
|
||||
--/
|
||||
--//
|
||||
|
||||
-- ============================================================================
|
||||
--changeset rbac-user-grant-REVOKE-PERMISSION-FROM-ROLE:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
create or replace procedure revokePermissionFromRole(permissionUuid uuid, superRoleUuid uuid)
|
||||
language plpgsql as $$
|
||||
begin
|
||||
raise INFO 'delete from RbacGrants where ascendantUuid = % and descendantUuid = %', superRoleUuid, permissionUuid;
|
||||
delete from RbacGrants as g
|
||||
where g.ascendantUuid = superRoleUuid and g.descendantUuid = permissionUuid;
|
||||
end; $$;
|
||||
--//
|
||||
|
||||
@@ -337,11 +337,9 @@ grant all privileges on RbacOwnGrantedPermissions_rv to ${HSADMINNG_POSTGRES_RES
|
||||
/*
|
||||
Returns all permissions granted to the given user,
|
||||
which are also visible to the current user or assumed roles.
|
||||
|
||||
|
||||
*/
|
||||
create or replace function grantedPermissions(targetUserUuid uuid)
|
||||
returns table(roleUuid uuid, roleName text, permissionUuid uuid, op RbacOp, objectTable varchar, objectIdName varchar, objectUuid uuid)
|
||||
*/
|
||||
create or replace function grantedPermissionsRaw(targetUserUuid uuid)
|
||||
returns table(roleUuid uuid, roleName text, permissionUuid uuid, op RbacOp, opTableName varchar(60), objectTable varchar(60), objectIdName varchar, objectUuid uuid)
|
||||
returns null on null input
|
||||
language plpgsql as $$
|
||||
declare
|
||||
@@ -357,11 +355,13 @@ begin
|
||||
return query select
|
||||
xp.roleUuid,
|
||||
(xp.roleObjectTable || '#' || xp.roleObjectIdName || '.' || xp.roleType) as roleName,
|
||||
xp.permissionUuid, xp.op, xp.permissionObjectTable, xp.permissionObjectIdName, xp.permissionObjectUuid
|
||||
xp.permissionUuid, xp.op, xp.opTableName,
|
||||
xp.permissionObjectTable, xp.permissionObjectIdName, xp.permissionObjectUuid
|
||||
from (select
|
||||
r.uuid as roleUuid, r.roletype, ro.objectTable as roleObjectTable,
|
||||
findIdNameByObjectUuid(ro.objectTable, ro.uuid) as roleObjectIdName,
|
||||
p.uuid as permissionUuid, p.op, po.objecttable as permissionObjectTable,
|
||||
p.uuid as permissionUuid, p.op, p.opTableName,
|
||||
po.objecttable as permissionObjectTable,
|
||||
findIdNameByObjectUuid(po.objectTable, po.uuid) as permissionObjectIdName,
|
||||
po.uuid as permissionObjectUuid
|
||||
from queryPermissionsGrantedToSubjectId( targetUserUuid) as p
|
||||
@@ -373,4 +373,15 @@ begin
|
||||
) xp;
|
||||
-- @formatter:on
|
||||
end; $$;
|
||||
|
||||
create or replace function grantedPermissions(targetUserUuid uuid)
|
||||
returns table(roleUuid uuid, roleName text, permissionUuid uuid, op RbacOp, opTableName varchar(60), objectTable varchar(60), objectIdName varchar, objectUuid uuid)
|
||||
returns null on null input
|
||||
language sql as $$
|
||||
select * from grantedPermissionsRaw(targetUserUuid)
|
||||
union all
|
||||
select roleUuid, roleName, permissionUuid, 'SELECT'::RbacOp, opTableName, objectTable, objectIdName, objectUuid
|
||||
from grantedPermissionsRaw(targetUserUuid)
|
||||
where op <> 'SELECT'::RbacOp;
|
||||
$$;
|
||||
--//
|
||||
|
||||
@@ -13,24 +13,6 @@ begin
|
||||
return createPermissions(forObjectUuid, permitOps);
|
||||
end; $$;
|
||||
|
||||
create or replace function toRoleUuids(roleDescriptors RbacRoleDescriptor[])
|
||||
returns uuid[]
|
||||
language plpgsql
|
||||
strict as $$
|
||||
declare
|
||||
superRoleDescriptor RbacRoleDescriptor;
|
||||
superRoleUuids uuid[] := array []::uuid[];
|
||||
begin
|
||||
foreach superRoleDescriptor in array roleDescriptors
|
||||
loop
|
||||
if superRoleDescriptor is not null then
|
||||
superRoleUuids := superRoleUuids || getRoleId(superRoleDescriptor, 'fail');
|
||||
end if;
|
||||
end loop;
|
||||
|
||||
return superRoleUuids;
|
||||
end; $$;
|
||||
|
||||
|
||||
-- =================================================================
|
||||
-- CREATE ROLE
|
||||
@@ -50,32 +32,37 @@ create or replace function createRoleWithGrants(
|
||||
language plpgsql as $$
|
||||
declare
|
||||
roleUuid uuid;
|
||||
superRoleUuid uuid;
|
||||
subRoleDesc RbacRoleDescriptor;
|
||||
superRoleDesc RbacRoleDescriptor;
|
||||
subRoleUuid uuid;
|
||||
superRoleUuid uuid;
|
||||
userUuid uuid;
|
||||
grantedByRoleUuid uuid;
|
||||
begin
|
||||
roleUuid := createRole(roleDescriptor);
|
||||
|
||||
if cardinality(permissions) >0 then
|
||||
if cardinality(permissions) > 0 then
|
||||
call grantPermissionsToRole(roleUuid, toPermissionUuids(roleDescriptor.objectuuid, permissions));
|
||||
end if;
|
||||
|
||||
foreach superRoleUuid in array toRoleUuids(incomingSuperRoles)
|
||||
foreach superRoleDesc in array array_remove(incomingSuperRoles, null)
|
||||
loop
|
||||
call grantRoleToRole(roleUuid, superRoleUuid);
|
||||
superRoleUuid := getRoleId(superRoleDesc);
|
||||
call grantRoleToRole(roleUuid, superRoleUuid, superRoleDesc.assumed);
|
||||
end loop;
|
||||
|
||||
foreach subRoleUuid in array toRoleUuids(outgoingSubRoles)
|
||||
foreach subRoleDesc in array array_remove(outgoingSubRoles, null)
|
||||
loop
|
||||
call grantRoleToRole(subRoleUuid, roleUuid);
|
||||
subRoleUuid := getRoleId(subRoleDesc);
|
||||
call grantRoleToRole(subRoleUuid, roleUuid, subRoleDesc.assumed);
|
||||
end loop;
|
||||
|
||||
if cardinality(userUuids) > 0 then
|
||||
if grantedByRole is null then
|
||||
raise exception 'to directly assign users to roles, grantingRole has to be given';
|
||||
grantedByRoleUuid := roleUuid;
|
||||
else
|
||||
grantedByRoleUuid := getRoleId(grantedByRole);
|
||||
end if;
|
||||
grantedByRoleUuid := getRoleId(grantedByRole, 'fail');
|
||||
foreach userUuid in array userUuids
|
||||
loop
|
||||
call grantRoleToUserUnchecked(grantedByRoleUuid, roleUuid, userUuid);
|
||||
|
||||
@@ -13,8 +13,7 @@ declare
|
||||
begin
|
||||
createInsertTriggerSQL = format($sql$
|
||||
create trigger createRbacObjectFor_%s_Trigger
|
||||
before insert
|
||||
on %s
|
||||
before insert on %s
|
||||
for each row
|
||||
execute procedure insertRelatedRbacObject();
|
||||
$sql$, targetTable, targetTable);
|
||||
@@ -36,50 +35,50 @@ end; $$;
|
||||
--changeset rbac-generators-ROLE-DESCRIPTORS:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
create or replace procedure generateRbacRoleDescriptors(prefix text, targetTable text)
|
||||
create procedure generateRbacRoleDescriptors(prefix text, targetTable text)
|
||||
language plpgsql as $$
|
||||
declare
|
||||
sql text;
|
||||
begin
|
||||
sql = format($sql$
|
||||
create or replace function %1$sOwner(entity %2$s)
|
||||
create or replace function %1$sOwner(entity %2$s, assumed boolean = true)
|
||||
returns RbacRoleDescriptor
|
||||
language plpgsql
|
||||
strict as $f$
|
||||
begin
|
||||
return roleDescriptor('%2$s', entity.uuid, 'owner');
|
||||
return roleDescriptor('%2$s', entity.uuid, 'owner', assumed);
|
||||
end; $f$;
|
||||
|
||||
create or replace function %1$sAdmin(entity %2$s)
|
||||
create or replace function %1$sAdmin(entity %2$s, assumed boolean = true)
|
||||
returns RbacRoleDescriptor
|
||||
language plpgsql
|
||||
strict as $f$
|
||||
begin
|
||||
return roleDescriptor('%2$s', entity.uuid, 'admin');
|
||||
return roleDescriptor('%2$s', entity.uuid, 'admin', assumed);
|
||||
end; $f$;
|
||||
|
||||
create or replace function %1$sAgent(entity %2$s)
|
||||
create or replace function %1$sAgent(entity %2$s, assumed boolean = true)
|
||||
returns RbacRoleDescriptor
|
||||
language plpgsql
|
||||
strict as $f$
|
||||
begin
|
||||
return roleDescriptor('%2$s', entity.uuid, 'agent');
|
||||
return roleDescriptor('%2$s', entity.uuid, 'agent', assumed);
|
||||
end; $f$;
|
||||
|
||||
create or replace function %1$sTenant(entity %2$s)
|
||||
create or replace function %1$sTenant(entity %2$s, assumed boolean = true)
|
||||
returns RbacRoleDescriptor
|
||||
language plpgsql
|
||||
strict as $f$
|
||||
begin
|
||||
return roleDescriptor('%2$s', entity.uuid, 'tenant');
|
||||
return roleDescriptor('%2$s', entity.uuid, 'tenant', assumed);
|
||||
end; $f$;
|
||||
|
||||
create or replace function %1$sGuest(entity %2$s)
|
||||
create or replace function %1$sGuest(entity %2$s, assumed boolean = true)
|
||||
returns RbacRoleDescriptor
|
||||
language plpgsql
|
||||
strict as $f$
|
||||
begin
|
||||
return roleDescriptor('%2$s', entity.uuid, 'guest');
|
||||
return roleDescriptor('%2$s', entity.uuid, 'guest', assumed);
|
||||
end; $f$;
|
||||
|
||||
$sql$, prefix, targetTable);
|
||||
@@ -92,7 +91,7 @@ end; $$;
|
||||
--changeset rbac-generators-IDENTITY-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
create or replace procedure generateRbacIdentityView(targetTable text, idNameExpression text)
|
||||
create or replace procedure generateRbacIdentityViewFromQuery(targetTable text, sqlQuery text)
|
||||
language plpgsql as $$
|
||||
declare
|
||||
sql text;
|
||||
@@ -101,11 +100,9 @@ begin
|
||||
|
||||
-- create a view to the target main table which maps an idName to the objectUuid
|
||||
sql = format($sql$
|
||||
create or replace view %1$s_iv as
|
||||
select target.uuid, cleanIdentifier(%2$s) as idName
|
||||
from %1$s as target;
|
||||
create or replace view %1$s_iv as %2$s;
|
||||
grant all privileges on %1$s_iv to ${HSADMINNG_POSTGRES_RESTRICTED_USERNAME};
|
||||
$sql$, targetTable, idNameExpression);
|
||||
$sql$, targetTable, sqlQuery);
|
||||
execute sql;
|
||||
|
||||
-- creates a function which maps an idName to the objectUuid
|
||||
@@ -130,6 +127,20 @@ begin
|
||||
$sql$, targetTable);
|
||||
execute sql;
|
||||
end; $$;
|
||||
|
||||
create or replace procedure generateRbacIdentityViewFromProjection(targetTable text, sqlProjection text)
|
||||
language plpgsql as $$
|
||||
declare
|
||||
sqlQuery text;
|
||||
begin
|
||||
targettable := lower(targettable);
|
||||
|
||||
sqlQuery = format($sql$
|
||||
select target.uuid, cleanIdentifier(%2$s) as idName
|
||||
from %1$s as target;
|
||||
$sql$, targetTable, sqlProjection);
|
||||
call generateRbacIdentityViewFromQuery(targetTable, sqlQuery);
|
||||
end; $$;
|
||||
--//
|
||||
|
||||
|
||||
@@ -145,13 +156,13 @@ begin
|
||||
targetTable := lower(targetTable);
|
||||
|
||||
/*
|
||||
Creates a restricted view based on the 'view' permission of the current subject.
|
||||
Creates a restricted view based on the 'SELECT' permission of the current subject.
|
||||
*/
|
||||
sql := format($sql$
|
||||
set session session authorization default;
|
||||
create view %1$s_rv as
|
||||
with accessibleObjects as (
|
||||
select queryAccessibleObjectUuidsOfSubjectIds('view', '%1$s', currentSubjectsUuids())
|
||||
select queryAccessibleObjectUuidsOfSubjectIds('SELECT', '%1$s', currentSubjectsUuids())
|
||||
)
|
||||
select target.*
|
||||
from %1$s as target
|
||||
@@ -200,7 +211,7 @@ begin
|
||||
returns trigger
|
||||
language plpgsql as $f$
|
||||
begin
|
||||
if old.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('delete', '%1$s', currentSubjectsUuids())) then
|
||||
if old.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('DELETE', '%1$s', currentSubjectsUuids())) then
|
||||
delete from %1$s p where p.uuid = old.uuid;
|
||||
return old;
|
||||
end if;
|
||||
@@ -223,7 +234,7 @@ begin
|
||||
|
||||
/**
|
||||
Instead of update trigger function for the restricted view
|
||||
based on the 'edit' permission of the current subject.
|
||||
based on the 'UPDATE' permission of the current subject.
|
||||
*/
|
||||
if columnUpdates is not null then
|
||||
sql := format($sql$
|
||||
@@ -231,7 +242,7 @@ begin
|
||||
returns trigger
|
||||
language plpgsql as $f$
|
||||
begin
|
||||
if old.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('edit', '%1$s', currentSubjectsUuids())) then
|
||||
if old.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('UPDATE', '%1$s', currentSubjectsUuids())) then
|
||||
update %1$s
|
||||
set %2$s
|
||||
where uuid = old.uuid;
|
||||
|
||||
@@ -22,6 +22,19 @@ grant select on global to ${HSADMINNG_POSTGRES_RESTRICTED_USERNAME};
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset rbac-global-IS-GLOBAL-ADMIN:1 endDelimiter:--//
|
||||
-- ------------------------------------------------------------------
|
||||
|
||||
create or replace function isGlobalAdmin()
|
||||
returns boolean
|
||||
language plpgsql as $$
|
||||
begin
|
||||
return isGranted(currentSubjectsUuids(), findRoleId(globalAdmin()));
|
||||
end; $$;
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset rbac-global-HAS-GLOBAL-PERMISSION:1 endDelimiter:--//
|
||||
-- ------------------------------------------------------------------
|
||||
@@ -96,12 +109,12 @@ commit;
|
||||
/*
|
||||
A global administrator role.
|
||||
*/
|
||||
create or replace function globalAdmin()
|
||||
create or replace function globalAdmin(assumed boolean = true)
|
||||
returns RbacRoleDescriptor
|
||||
returns null on null input
|
||||
stable -- leakproof
|
||||
language sql as $$
|
||||
select 'global', (select uuid from RbacObject where objectTable = 'global'), 'admin'::RbacRoleType;
|
||||
select 'global', (select uuid from RbacObject where objectTable = 'global'), 'admin'::RbacRoleType, assumed;
|
||||
$$;
|
||||
|
||||
begin transaction;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
### rbac customer
|
||||
|
||||
This code generated was by RbacViewMermaidFlowchartGenerator at 2024-03-11T11:29:11.571772062.
|
||||
|
||||
```mermaid
|
||||
%%{init:{'flowchart':{'htmlLabels':false}}}%%
|
||||
flowchart TB
|
||||
|
||||
subgraph customer["`**customer**`"]
|
||||
direction TB
|
||||
style customer fill:#dd4901,stroke:#274d6e,stroke-width:8px
|
||||
|
||||
subgraph customer:roles[ ]
|
||||
style customer:roles fill:#dd4901,stroke:white
|
||||
|
||||
role:customer:owner[[customer:owner]]
|
||||
role:customer:admin[[customer:admin]]
|
||||
role:customer:tenant[[customer:tenant]]
|
||||
end
|
||||
|
||||
subgraph customer:permissions[ ]
|
||||
style customer:permissions fill:#dd4901,stroke:white
|
||||
|
||||
perm:customer:DELETE{{customer:DELETE}}
|
||||
perm:customer:UPDATE{{customer:UPDATE}}
|
||||
perm:customer:SELECT{{customer:SELECT}}
|
||||
end
|
||||
end
|
||||
|
||||
%% granting roles to users
|
||||
user:creator ==>|XX| role:customer:owner
|
||||
|
||||
%% granting roles to roles
|
||||
role:global:admin ==>|XX| role:customer:owner
|
||||
role:customer:owner ==> role:customer:admin
|
||||
role:customer:admin ==> role:customer:tenant
|
||||
|
||||
%% granting permissions to roles
|
||||
role:customer:owner ==> perm:customer:DELETE
|
||||
role:customer:admin ==> perm:customer:UPDATE
|
||||
role:customer:tenant ==> perm:customer:SELECT
|
||||
|
||||
```
|
||||
@@ -1,4 +1,5 @@
|
||||
--liquibase formatted sql
|
||||
-- This code generated was by RbacViewPostgresGenerator at 2024-03-11T11:29:11.584886824.
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-customer-rbac-OBJECT:1 endDelimiter:--//
|
||||
@@ -15,82 +16,103 @@ call generateRbacRoleDescriptors('testCustomer', 'test_customer');
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-customer-rbac-ROLES-CREATION:1 endDelimiter:--//
|
||||
--changeset test-customer-rbac-insert-trigger:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
Creates the roles and their assignments for a new customer for the AFTER INSERT TRIGGER.
|
||||
Creates the roles, grants and permission for the AFTER INSERT TRIGGER.
|
||||
*/
|
||||
|
||||
create or replace function createRbacRolesForTestCustomer()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
strict as $$
|
||||
declare
|
||||
testCustomerOwnerUuid uuid;
|
||||
customerAdminUuid uuid;
|
||||
begin
|
||||
if TG_OP <> 'INSERT' then
|
||||
raise exception 'invalid usage of TRIGGER AFTER INSERT';
|
||||
end if;
|
||||
create or replace procedure buildRbacSystemForTestCustomer(
|
||||
NEW test_customer
|
||||
)
|
||||
language plpgsql as $$
|
||||
|
||||
declare
|
||||
|
||||
begin
|
||||
call enterTriggerForObjectUuid(NEW.uuid);
|
||||
|
||||
-- the owner role with full access for Hostsharing administrators
|
||||
testCustomerOwnerUuid = createRoleWithGrants(
|
||||
perform createRoleWithGrants(
|
||||
testCustomerOwner(NEW),
|
||||
permissions => array['*'],
|
||||
incomingSuperRoles => array[globalAdmin()]
|
||||
);
|
||||
permissions => array['DELETE'],
|
||||
userUuids => array[currentUserUuid()],
|
||||
incomingSuperRoles => array[globalAdmin(unassumed())]
|
||||
);
|
||||
|
||||
-- the admin role for the customer's admins, who can view and add products
|
||||
customerAdminUuid = createRoleWithGrants(
|
||||
perform createRoleWithGrants(
|
||||
testCustomerAdmin(NEW),
|
||||
permissions => array['view', 'add-package'],
|
||||
-- NO auto assume for customer owner to avoid exploding permissions for administrators
|
||||
userUuids => array[getRbacUserId(NEW.adminUserName, 'create')], -- implicitly ignored if null
|
||||
grantedByRole => globalAdmin()
|
||||
);
|
||||
permissions => array['UPDATE'],
|
||||
incomingSuperRoles => array[testCustomerOwner(NEW)]
|
||||
);
|
||||
|
||||
-- allow the customer owner role (thus administrators) to assume the customer admin role
|
||||
call grantRoleToRole(customerAdminUuid, testCustomerOwnerUuid, false);
|
||||
|
||||
-- the tenant role which later can be used by owners+admins of sub-objects
|
||||
perform createRoleWithGrants(
|
||||
testCustomerTenant(NEW),
|
||||
permissions => array['view']
|
||||
);
|
||||
permissions => array['SELECT'],
|
||||
incomingSuperRoles => array[testCustomerAdmin(NEW)]
|
||||
);
|
||||
|
||||
call leaveTriggerForObjectUuid(NEW.uuid);
|
||||
return NEW;
|
||||
end; $$;
|
||||
|
||||
/*
|
||||
An AFTER INSERT TRIGGER which creates the role structure for a new customer.
|
||||
AFTER INSERT TRIGGER to create the role+grant structure for a new test_customer row.
|
||||
*/
|
||||
|
||||
drop trigger if exists createRbacRolesForTestCustomer_Trigger on test_customer;
|
||||
create trigger createRbacRolesForTestCustomer_Trigger
|
||||
after insert
|
||||
on test_customer
|
||||
create or replace function insertTriggerForTestCustomer_tf()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
strict as $$
|
||||
begin
|
||||
call buildRbacSystemForTestCustomer(NEW);
|
||||
return NEW;
|
||||
end; $$;
|
||||
|
||||
create trigger insertTriggerForTestCustomer_tg
|
||||
after insert on test_customer
|
||||
for each row
|
||||
execute procedure createRbacRolesForTestCustomer();
|
||||
execute procedure insertTriggerForTestCustomer_tf();
|
||||
|
||||
--//
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-customer-rbac-INSERT:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
Checks if the user or assumed roles are allowed to insert a row to test_customer.
|
||||
*/
|
||||
create or replace function test_customer_insert_permission_missing_tf()
|
||||
returns trigger
|
||||
language plpgsql as $$
|
||||
begin
|
||||
raise exception '[403] insert into test_customer not allowed for current subjects % (%)',
|
||||
currentSubjects(), currentSubjectsUuids();
|
||||
end; $$;
|
||||
|
||||
create trigger test_customer_insert_permission_check_tg
|
||||
before insert on test_customer
|
||||
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 test_customer_insert_permission_missing_tf();
|
||||
|
||||
--//
|
||||
-- ============================================================================
|
||||
--changeset test-customer-rbac-IDENTITY-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacIdentityView('test_customer', $idName$
|
||||
target.prefix
|
||||
|
||||
call generateRbacIdentityViewFromProjection('test_customer', $idName$
|
||||
prefix
|
||||
$idName$);
|
||||
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-customer-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacRestrictedView('test_customer', 'target.prefix',
|
||||
call generateRbacRestrictedView('test_customer',
|
||||
'reference',
|
||||
$updates$
|
||||
reference = new.reference,
|
||||
prefix = new.prefix,
|
||||
@@ -99,47 +121,3 @@ call generateRbacRestrictedView('test_customer', 'target.prefix',
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-customer-rbac-ADD-CUSTOMER:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
/*
|
||||
Creates a global permission for add-customer and assigns it to the hostsharing admins role.
|
||||
*/
|
||||
do language plpgsql $$
|
||||
declare
|
||||
addCustomerPermissions uuid[];
|
||||
globalObjectUuid uuid;
|
||||
globalAdminRoleUuid uuid ;
|
||||
begin
|
||||
call defineContext('granting global add-customer permission to global admin role', null, null, null);
|
||||
|
||||
globalAdminRoleUuid := findRoleId(globalAdmin());
|
||||
globalObjectUuid := (select uuid from global);
|
||||
addCustomerPermissions := createPermissions(globalObjectUuid, array ['add-customer']);
|
||||
call grantPermissionsToRole(globalAdminRoleUuid, addCustomerPermissions);
|
||||
end;
|
||||
$$;
|
||||
|
||||
/**
|
||||
Used by the trigger to prevent the add-customer to current user respectively assumed roles.
|
||||
*/
|
||||
create or replace function addTestCustomerNotAllowedForCurrentSubjects()
|
||||
returns trigger
|
||||
language PLPGSQL
|
||||
as $$
|
||||
begin
|
||||
raise exception '[403] add-customer not permitted for %',
|
||||
array_to_string(currentSubjects(), ';', 'null');
|
||||
end; $$;
|
||||
|
||||
/**
|
||||
Checks if the user or assumed roles are allowed to add a new customer.
|
||||
*/
|
||||
create trigger test_customer_insert_trigger
|
||||
before insert
|
||||
on test_customer
|
||||
for each row
|
||||
when ( not hasGlobalPermission('add-customer') )
|
||||
execute procedure addTestCustomerNotAllowedForCurrentSubjects();
|
||||
--//
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ declare
|
||||
currentTask varchar;
|
||||
custRowId uuid;
|
||||
custAdminName varchar;
|
||||
custAdminUuid uuid;
|
||||
newCust test_customer;
|
||||
begin
|
||||
currentTask = 'creating RBAC test customer #' || custReference || '/' || custPrefix;
|
||||
call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global.admin');
|
||||
@@ -35,10 +37,19 @@ begin
|
||||
|
||||
custRowId = uuid_generate_v4();
|
||||
custAdminName = 'customer-admin@' || custPrefix || '.example.com';
|
||||
custAdminUuid = createRbacUser(custAdminName);
|
||||
|
||||
insert
|
||||
into test_customer (reference, prefix, adminUserName)
|
||||
values (custReference, custPrefix, custAdminName);
|
||||
|
||||
select * into newCust
|
||||
from test_customer where reference=custReference;
|
||||
call grantRoleToUser(
|
||||
getRoleId(testCustomerOwner(newCust)),
|
||||
getRoleId(testCustomerAdmin(newCust)),
|
||||
custAdminUuid,
|
||||
true);
|
||||
end; $$;
|
||||
--//
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
### rbac package
|
||||
|
||||
This code generated was by RbacViewMermaidFlowchartGenerator at 2024-03-11T11:29:11.624847792.
|
||||
|
||||
```mermaid
|
||||
%%{init:{'flowchart':{'htmlLabels':false}}}%%
|
||||
flowchart TB
|
||||
|
||||
subgraph package["`**package**`"]
|
||||
direction TB
|
||||
style package fill:#dd4901,stroke:#274d6e,stroke-width:8px
|
||||
|
||||
subgraph package:roles[ ]
|
||||
style package:roles fill:#dd4901,stroke:white
|
||||
|
||||
role:package:owner[[package:owner]]
|
||||
role:package:admin[[package:admin]]
|
||||
role:package:tenant[[package:tenant]]
|
||||
end
|
||||
|
||||
subgraph package:permissions[ ]
|
||||
style package:permissions fill:#dd4901,stroke:white
|
||||
|
||||
perm:package:INSERT{{package:INSERT}}
|
||||
perm:package:DELETE{{package:DELETE}}
|
||||
perm:package:UPDATE{{package:UPDATE}}
|
||||
perm:package:SELECT{{package:SELECT}}
|
||||
end
|
||||
end
|
||||
|
||||
subgraph customer["`**customer**`"]
|
||||
direction TB
|
||||
style customer fill:#99bcdb,stroke:#274d6e,stroke-width:8px
|
||||
|
||||
subgraph customer:roles[ ]
|
||||
style customer:roles fill:#99bcdb,stroke:white
|
||||
|
||||
role:customer:owner[[customer:owner]]
|
||||
role:customer:admin[[customer:admin]]
|
||||
role:customer:tenant[[customer:tenant]]
|
||||
end
|
||||
end
|
||||
|
||||
%% granting roles to roles
|
||||
role:global:admin -.->|XX| role:customer:owner
|
||||
role:customer:owner -.-> role:customer:admin
|
||||
role:customer:admin -.-> role:customer:tenant
|
||||
role:customer:admin ==> role:package:owner
|
||||
role:package:owner ==> role:package:admin
|
||||
role:package:admin ==> role:package:tenant
|
||||
role:package:tenant ==> role:customer:tenant
|
||||
|
||||
%% granting permissions to roles
|
||||
role:customer:admin ==> perm:package:INSERT
|
||||
role:package:owner ==> perm:package:DELETE
|
||||
role:package:owner ==> perm:package:UPDATE
|
||||
role:package:tenant ==> perm:package:SELECT
|
||||
|
||||
```
|
||||
@@ -1,4 +1,5 @@
|
||||
--liquibase formatted sql
|
||||
-- This code generated was by RbacViewPostgresGenerator at 2024-03-11T11:29:11.625353859.
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-package-rbac-OBJECT:1 endDelimiter:--//
|
||||
@@ -15,95 +16,211 @@ call generateRbacRoleDescriptors('testPackage', 'test_package');
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-package-rbac-ROLES-CREATION:1 endDelimiter:--//
|
||||
--changeset test-package-rbac-insert-trigger:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
Creates the roles and their assignments for a new package for the AFTER INSERT TRIGGER.
|
||||
Creates the roles, grants and permission for the AFTER INSERT TRIGGER.
|
||||
*/
|
||||
create or replace function createRbacRolesForTestPackage()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
strict as $$
|
||||
|
||||
create or replace procedure buildRbacSystemForTestPackage(
|
||||
NEW test_package
|
||||
)
|
||||
language plpgsql as $$
|
||||
|
||||
declare
|
||||
parentCustomer test_customer;
|
||||
newCustomer test_customer;
|
||||
|
||||
begin
|
||||
if TG_OP <> 'INSERT' then
|
||||
raise exception 'invalid usage of TRIGGER AFTER INSERT';
|
||||
end if;
|
||||
|
||||
call enterTriggerForObjectUuid(NEW.uuid);
|
||||
SELECT * FROM test_customer c
|
||||
WHERE c.uuid= NEW.customerUuid
|
||||
into newCustomer;
|
||||
|
||||
select * from test_customer as c where c.uuid = NEW.customerUuid into parentCustomer;
|
||||
|
||||
-- an owner role is created and assigned to the customer's admin role
|
||||
perform createRoleWithGrants(
|
||||
testPackageOwner(NEW),
|
||||
permissions => array ['*'],
|
||||
incomingSuperRoles => array[testCustomerAdmin(parentCustomer)]
|
||||
);
|
||||
testPackageOwner(NEW),
|
||||
permissions => array['DELETE', 'UPDATE'],
|
||||
incomingSuperRoles => array[testCustomerAdmin(newCustomer)]
|
||||
);
|
||||
|
||||
-- an owner role is created and assigned to the package owner role
|
||||
perform createRoleWithGrants(
|
||||
testPackageAdmin(NEW),
|
||||
permissions => array ['add-domain'],
|
||||
testPackageAdmin(NEW),
|
||||
incomingSuperRoles => array[testPackageOwner(NEW)]
|
||||
);
|
||||
);
|
||||
|
||||
-- and a package tenant role is created and assigned to the package admin as well
|
||||
perform createRoleWithGrants(
|
||||
testPackageTenant(NEW),
|
||||
permissions => array['view'],
|
||||
incomingsuperroles => array[testPackageAdmin(NEW)],
|
||||
outgoingSubRoles => array[testCustomerTenant(parentCustomer)]
|
||||
);
|
||||
testPackageTenant(NEW),
|
||||
permissions => array['SELECT'],
|
||||
incomingSuperRoles => array[testPackageAdmin(NEW)],
|
||||
outgoingSubRoles => array[testCustomerTenant(newCustomer)]
|
||||
);
|
||||
|
||||
call leaveTriggerForObjectUuid(NEW.uuid);
|
||||
return NEW;
|
||||
end; $$;
|
||||
|
||||
/*
|
||||
An AFTER INSERT TRIGGER which creates the role structure for a new package.
|
||||
AFTER INSERT TRIGGER to create the role+grant structure for a new test_package row.
|
||||
*/
|
||||
|
||||
create trigger createRbacRolesForTestPackage_Trigger
|
||||
after insert
|
||||
on test_package
|
||||
create or replace function insertTriggerForTestPackage_tf()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
strict as $$
|
||||
begin
|
||||
call buildRbacSystemForTestPackage(NEW);
|
||||
return NEW;
|
||||
end; $$;
|
||||
|
||||
create trigger insertTriggerForTestPackage_tg
|
||||
after insert on test_package
|
||||
for each row
|
||||
execute procedure createRbacRolesForTestPackage();
|
||||
execute procedure insertTriggerForTestPackage_tf();
|
||||
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-package-rbac-IDENTITY-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacIdentityView('test_package', 'target.name');
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-package-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
|
||||
--changeset test-package-rbac-update-trigger:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
Creates a view to the customer main table which maps the identifying name
|
||||
(in this case, the prefix) to the objectUuid.
|
||||
Called from the AFTER UPDATE TRIGGER to re-wire the grants.
|
||||
*/
|
||||
-- drop view if exists test_package_rv;
|
||||
-- create or replace view test_package_rv as
|
||||
-- select target.*
|
||||
-- from test_package as target
|
||||
-- where target.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('view', 'test_package', currentSubjectsUuids()))
|
||||
-- order by target.name;
|
||||
-- grant all privileges on test_package_rv to ${HSADMINNG_POSTGRES_RESTRICTED_USERNAME};
|
||||
|
||||
call generateRbacRestrictedView('test_package', 'target.name',
|
||||
$updates$
|
||||
version = new.version,
|
||||
customerUuid = new.customerUuid,
|
||||
name = new.name,
|
||||
description = new.description
|
||||
$updates$);
|
||||
create or replace procedure updateRbacRulesForTestPackage(
|
||||
OLD test_package,
|
||||
NEW test_package
|
||||
)
|
||||
language plpgsql as $$
|
||||
|
||||
declare
|
||||
oldCustomer test_customer;
|
||||
newCustomer test_customer;
|
||||
|
||||
begin
|
||||
call enterTriggerForObjectUuid(NEW.uuid);
|
||||
|
||||
SELECT * FROM test_customer c
|
||||
WHERE c.uuid= OLD.customerUuid
|
||||
into oldCustomer;
|
||||
SELECT * FROM test_customer c
|
||||
WHERE c.uuid= NEW.customerUuid
|
||||
into newCustomer;
|
||||
|
||||
if NEW.customerUuid <> OLD.customerUuid then
|
||||
|
||||
call revokePermissionFromRole(findPermissionId(OLD.uuid, 'INSERT'), testCustomerAdmin(oldCustomer));
|
||||
|
||||
call revokeRoleFromRole(testPackageOwner(OLD), testCustomerAdmin(oldCustomer));
|
||||
call grantRoleToRole(testPackageOwner(NEW), testCustomerAdmin(newCustomer));
|
||||
|
||||
call revokeRoleFromRole(testCustomerTenant(oldCustomer), testPackageTenant(OLD));
|
||||
call grantRoleToRole(testCustomerTenant(newCustomer), testPackageTenant(NEW));
|
||||
|
||||
end if;
|
||||
|
||||
call leaveTriggerForObjectUuid(NEW.uuid);
|
||||
end; $$;
|
||||
|
||||
/*
|
||||
AFTER INSERT TRIGGER to re-wire the grant structure for a new test_package row.
|
||||
*/
|
||||
|
||||
create or replace function updateTriggerForTestPackage_tf()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
strict as $$
|
||||
begin
|
||||
call updateRbacRulesForTestPackage(OLD, NEW);
|
||||
return NEW;
|
||||
end; $$;
|
||||
|
||||
create trigger updateTriggerForTestPackage_tg
|
||||
after update on test_package
|
||||
for each row
|
||||
execute procedure updateTriggerForTestPackage_tf();
|
||||
|
||||
--//
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-package-rbac-INSERT:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
Creates INSERT INTO test_package permissions for the related test_customer rows.
|
||||
*/
|
||||
do language plpgsql $$
|
||||
declare
|
||||
row test_customer;
|
||||
permissionUuid uuid;
|
||||
roleUuid uuid;
|
||||
begin
|
||||
call defineContext('create INSERT INTO test_package permissions for the related test_customer rows');
|
||||
|
||||
FOR row IN SELECT * FROM test_customer
|
||||
LOOP
|
||||
roleUuid := findRoleId(testCustomerAdmin(row));
|
||||
permissionUuid := createPermission(row.uuid, 'INSERT', 'test_package');
|
||||
call grantPermissionToRole(roleUuid, permissionUuid);
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
|
||||
/**
|
||||
Adds test_package INSERT permission to specified role of new test_customer rows.
|
||||
*/
|
||||
create or replace function test_package_test_customer_insert_tf()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
strict as $$
|
||||
begin
|
||||
call grantPermissionToRole(
|
||||
testCustomerAdmin(NEW),
|
||||
createPermission(NEW.uuid, 'INSERT', 'test_package'));
|
||||
return NEW;
|
||||
end; $$;
|
||||
|
||||
create trigger test_package_test_customer_insert_tg
|
||||
after insert on test_customer
|
||||
for each row
|
||||
execute procedure test_package_test_customer_insert_tf();
|
||||
|
||||
/**
|
||||
Checks if the user or assumed roles are allowed to insert a row to test_package.
|
||||
*/
|
||||
create or replace function test_package_insert_permission_missing_tf()
|
||||
returns trigger
|
||||
language plpgsql as $$
|
||||
begin
|
||||
raise exception '[403] insert into test_package not allowed for current subjects % (%)',
|
||||
currentSubjects(), currentSubjectsUuids();
|
||||
end; $$;
|
||||
|
||||
create trigger test_package_insert_permission_check_tg
|
||||
before insert on test_package
|
||||
for each row
|
||||
when ( not hasInsertPermission(NEW.customerUuid, 'INSERT', 'test_package') )
|
||||
execute procedure test_package_insert_permission_missing_tf();
|
||||
|
||||
--//
|
||||
-- ============================================================================
|
||||
--changeset test-package-rbac-IDENTITY-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
call generateRbacIdentityViewFromProjection('test_package', $idName$
|
||||
name
|
||||
$idName$);
|
||||
|
||||
--//
|
||||
-- ============================================================================
|
||||
--changeset test-package-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacRestrictedView('test_package',
|
||||
'name',
|
||||
$updates$
|
||||
version = new.version,
|
||||
customerUuid = new.customerUuid,
|
||||
description = new.description
|
||||
$updates$);
|
||||
--//
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ begin
|
||||
|
||||
custAdminUser = 'customer-admin@' || cust.prefix || '.example.com';
|
||||
custAdminRole = 'test_customer#' || cust.prefix || '.admin';
|
||||
call defineContext(currentTask, null, custAdminUser, custAdminRole);
|
||||
call defineContext(currentTask, null, 'superuser-fran@hostsharing.net', custAdminRole);
|
||||
raise notice 'task: % by % as %', currentTask, custAdminUser, custAdminRole;
|
||||
|
||||
insert
|
||||
@@ -35,7 +35,7 @@ begin
|
||||
returning * into pac;
|
||||
|
||||
call grantRoleToUser(
|
||||
getRoleId(testCustomerAdmin(cust), 'fail'),
|
||||
getRoleId(testCustomerAdmin(cust)),
|
||||
findRoleId(testPackageAdmin(pac)),
|
||||
createRbacUser('pac-admin-' || pacName || '@' || cust.prefix || '.example.com'),
|
||||
true);
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
### rbac domain
|
||||
|
||||
This code generated was by RbacViewMermaidFlowchartGenerator at 2024-03-11T11:29:11.644658132.
|
||||
|
||||
```mermaid
|
||||
%%{init:{'flowchart':{'htmlLabels':false}}}%%
|
||||
flowchart TB
|
||||
|
||||
subgraph package.customer["`**package.customer**`"]
|
||||
direction TB
|
||||
style package.customer fill:#99bcdb,stroke:#274d6e,stroke-width:8px
|
||||
|
||||
subgraph package.customer:roles[ ]
|
||||
style package.customer:roles fill:#99bcdb,stroke:white
|
||||
|
||||
role:package.customer:owner[[package.customer:owner]]
|
||||
role:package.customer:admin[[package.customer:admin]]
|
||||
role:package.customer:tenant[[package.customer:tenant]]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph package["`**package**`"]
|
||||
direction TB
|
||||
style package fill:#99bcdb,stroke:#274d6e,stroke-width:8px
|
||||
|
||||
subgraph package.customer["`**package.customer**`"]
|
||||
direction TB
|
||||
style package.customer fill:#99bcdb,stroke:#274d6e,stroke-width:8px
|
||||
|
||||
subgraph package.customer:roles[ ]
|
||||
style package.customer:roles fill:#99bcdb,stroke:white
|
||||
|
||||
role:package.customer:owner[[package.customer:owner]]
|
||||
role:package.customer:admin[[package.customer:admin]]
|
||||
role:package.customer:tenant[[package.customer:tenant]]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph package:roles[ ]
|
||||
style package:roles fill:#99bcdb,stroke:white
|
||||
|
||||
role:package:owner[[package:owner]]
|
||||
role:package:admin[[package:admin]]
|
||||
role:package:tenant[[package:tenant]]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph domain["`**domain**`"]
|
||||
direction TB
|
||||
style domain fill:#dd4901,stroke:#274d6e,stroke-width:8px
|
||||
|
||||
subgraph domain:roles[ ]
|
||||
style domain:roles fill:#dd4901,stroke:white
|
||||
|
||||
role:domain:owner[[domain:owner]]
|
||||
role:domain:admin[[domain:admin]]
|
||||
end
|
||||
|
||||
subgraph domain:permissions[ ]
|
||||
style domain:permissions fill:#dd4901,stroke:white
|
||||
|
||||
perm:domain:INSERT{{domain:INSERT}}
|
||||
perm:domain:DELETE{{domain:DELETE}}
|
||||
perm:domain:UPDATE{{domain:UPDATE}}
|
||||
perm:domain:SELECT{{domain:SELECT}}
|
||||
end
|
||||
end
|
||||
|
||||
%% granting roles to roles
|
||||
role:global:admin -.->|XX| role:package.customer:owner
|
||||
role:package.customer:owner -.-> role:package.customer:admin
|
||||
role:package.customer:admin -.-> role:package.customer:tenant
|
||||
role:package.customer:admin -.-> role:package:owner
|
||||
role:package:owner -.-> role:package:admin
|
||||
role:package:admin -.-> role:package:tenant
|
||||
role:package:tenant -.-> role:package.customer:tenant
|
||||
role:package:admin ==> role:domain:owner
|
||||
role:domain:owner ==> role:package:tenant
|
||||
role:domain:owner ==> role:domain:admin
|
||||
role:domain:admin ==> role:package:tenant
|
||||
|
||||
%% granting permissions to roles
|
||||
role:package:admin ==> perm:domain:INSERT
|
||||
role:domain:owner ==> perm:domain:DELETE
|
||||
role:domain:owner ==> perm:domain:UPDATE
|
||||
role:domain:admin ==> perm:domain:SELECT
|
||||
|
||||
```
|
||||
@@ -1,4 +1,5 @@
|
||||
--liquibase formatted sql
|
||||
-- This code generated was by RbacViewPostgresGenerator at 2024-03-11T11:29:11.645391647.
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-domain-rbac-OBJECT:1 endDelimiter:--//
|
||||
@@ -11,107 +12,214 @@ call generateRelatedRbacObject('test_domain');
|
||||
--changeset test-domain-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacRoleDescriptors('testDomain', 'test_domain');
|
||||
|
||||
create or replace function createTestDomainTenantRoleIfNotExists(domain test_domain)
|
||||
returns uuid
|
||||
returns null on null input
|
||||
language plpgsql as $$
|
||||
declare
|
||||
domainTenantRoleDesc RbacRoleDescriptor;
|
||||
domainTenantRoleUuid uuid;
|
||||
begin
|
||||
domainTenantRoleDesc = testdomainTenant(domain);
|
||||
domainTenantRoleUuid = findRoleId(domainTenantRoleDesc);
|
||||
if domainTenantRoleUuid is not null then
|
||||
return domainTenantRoleUuid;
|
||||
end if;
|
||||
|
||||
return createRoleWithGrants(
|
||||
domainTenantRoleDesc,
|
||||
permissions => array['view'],
|
||||
incomingSuperRoles => array[testdomainAdmin(domain)]
|
||||
);
|
||||
end; $$;
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-domain-rbac-ROLES-CREATION:1 endDelimiter:--//
|
||||
--changeset test-domain-rbac-insert-trigger:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
Creates the roles and their assignments for a new domain for the AFTER INSERT TRIGGER.
|
||||
Creates the roles, grants and permission for the AFTER INSERT TRIGGER.
|
||||
*/
|
||||
|
||||
create or replace function createRbacRulesForTestDomain()
|
||||
create or replace procedure buildRbacSystemForTestDomain(
|
||||
NEW test_domain
|
||||
)
|
||||
language plpgsql as $$
|
||||
|
||||
declare
|
||||
newPackage test_package;
|
||||
|
||||
begin
|
||||
call enterTriggerForObjectUuid(NEW.uuid);
|
||||
SELECT * FROM test_package p
|
||||
WHERE p.uuid= NEW.packageUuid
|
||||
into newPackage;
|
||||
|
||||
perform createRoleWithGrants(
|
||||
testDomainOwner(NEW),
|
||||
permissions => array['DELETE', 'UPDATE'],
|
||||
incomingSuperRoles => array[testPackageAdmin(newPackage)],
|
||||
outgoingSubRoles => array[testPackageTenant(newPackage)]
|
||||
);
|
||||
|
||||
perform createRoleWithGrants(
|
||||
testDomainAdmin(NEW),
|
||||
permissions => array['SELECT'],
|
||||
incomingSuperRoles => array[testDomainOwner(NEW)],
|
||||
outgoingSubRoles => array[testPackageTenant(newPackage)]
|
||||
);
|
||||
|
||||
call leaveTriggerForObjectUuid(NEW.uuid);
|
||||
end; $$;
|
||||
|
||||
/*
|
||||
AFTER INSERT TRIGGER to create the role+grant structure for a new test_domain row.
|
||||
*/
|
||||
|
||||
create or replace function insertTriggerForTestDomain_tf()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
strict as $$
|
||||
declare
|
||||
parentPackage test_package;
|
||||
begin
|
||||
if TG_OP <> 'INSERT' then
|
||||
raise exception 'invalid usage of TRIGGER AFTER INSERT';
|
||||
end if;
|
||||
|
||||
call enterTriggerForObjectUuid(NEW.uuid);
|
||||
|
||||
select * from test_package where uuid = NEW.packageUuid into parentPackage;
|
||||
|
||||
-- an owner role is created and assigned to the package's admin group
|
||||
perform createRoleWithGrants(
|
||||
testDomainOwner(NEW),
|
||||
permissions => array['*'],
|
||||
incomingSuperRoles => array[testPackageAdmin(parentPackage)]
|
||||
);
|
||||
|
||||
-- and a domain admin role is created and assigned to the domain owner as well
|
||||
perform createRoleWithGrants(
|
||||
testDomainAdmin(NEW),
|
||||
permissions => array['edit'],
|
||||
incomingSuperRoles => array[testDomainOwner(NEW)],
|
||||
outgoingSubRoles => array[testPackageTenant(parentPackage)]
|
||||
);
|
||||
|
||||
-- a tenent role is only created on demand
|
||||
|
||||
call leaveTriggerForObjectUuid(NEW.uuid);
|
||||
call buildRbacSystemForTestDomain(NEW);
|
||||
return NEW;
|
||||
end; $$;
|
||||
|
||||
|
||||
/*
|
||||
An AFTER INSERT TRIGGER which creates the role structure for a new domain.
|
||||
*/
|
||||
drop trigger if exists createRbacRulesForTestDomain_Trigger on test_domain;
|
||||
create trigger createRbacRulesForTestDomain_Trigger
|
||||
after insert
|
||||
on test_domain
|
||||
create trigger insertTriggerForTestDomain_tg
|
||||
after insert on test_domain
|
||||
for each row
|
||||
execute procedure createRbacRulesForTestDomain();
|
||||
execute procedure insertTriggerForTestDomain_tf();
|
||||
|
||||
--//
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-domain-rbac-update-trigger:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
Called from the AFTER UPDATE TRIGGER to re-wire the grants.
|
||||
*/
|
||||
|
||||
create or replace procedure updateRbacRulesForTestDomain(
|
||||
OLD test_domain,
|
||||
NEW test_domain
|
||||
)
|
||||
language plpgsql as $$
|
||||
|
||||
declare
|
||||
oldPackage test_package;
|
||||
newPackage test_package;
|
||||
|
||||
begin
|
||||
call enterTriggerForObjectUuid(NEW.uuid);
|
||||
|
||||
SELECT * FROM test_package p
|
||||
WHERE p.uuid= OLD.packageUuid
|
||||
into oldPackage;
|
||||
SELECT * FROM test_package p
|
||||
WHERE p.uuid= NEW.packageUuid
|
||||
into newPackage;
|
||||
|
||||
if NEW.packageUuid <> OLD.packageUuid then
|
||||
|
||||
call revokePermissionFromRole(findPermissionId(OLD.uuid, 'INSERT'), testPackageAdmin(oldPackage));
|
||||
|
||||
call revokeRoleFromRole(testDomainOwner(OLD), testPackageAdmin(oldPackage));
|
||||
call grantRoleToRole(testDomainOwner(NEW), testPackageAdmin(newPackage));
|
||||
|
||||
call revokeRoleFromRole(testPackageTenant(oldPackage), testDomainOwner(OLD));
|
||||
call grantRoleToRole(testPackageTenant(newPackage), testDomainOwner(NEW));
|
||||
|
||||
call revokeRoleFromRole(testPackageTenant(oldPackage), testDomainAdmin(OLD));
|
||||
call grantRoleToRole(testPackageTenant(newPackage), testDomainAdmin(NEW));
|
||||
|
||||
end if;
|
||||
|
||||
call leaveTriggerForObjectUuid(NEW.uuid);
|
||||
end; $$;
|
||||
|
||||
/*
|
||||
AFTER INSERT TRIGGER to re-wire the grant structure for a new test_domain row.
|
||||
*/
|
||||
|
||||
create or replace function updateTriggerForTestDomain_tf()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
strict as $$
|
||||
begin
|
||||
call updateRbacRulesForTestDomain(OLD, NEW);
|
||||
return NEW;
|
||||
end; $$;
|
||||
|
||||
create trigger updateTriggerForTestDomain_tg
|
||||
after update on test_domain
|
||||
for each row
|
||||
execute procedure updateTriggerForTestDomain_tf();
|
||||
|
||||
--//
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-domain-rbac-INSERT:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
Creates INSERT INTO test_domain permissions for the related test_package rows.
|
||||
*/
|
||||
do language plpgsql $$
|
||||
declare
|
||||
row test_package;
|
||||
permissionUuid uuid;
|
||||
roleUuid uuid;
|
||||
begin
|
||||
call defineContext('create INSERT INTO test_domain permissions for the related test_package rows');
|
||||
|
||||
FOR row IN SELECT * FROM test_package
|
||||
LOOP
|
||||
roleUuid := findRoleId(testPackageAdmin(row));
|
||||
permissionUuid := createPermission(row.uuid, 'INSERT', 'test_domain');
|
||||
call grantPermissionToRole(roleUuid, permissionUuid);
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
|
||||
/**
|
||||
Adds test_domain INSERT permission to specified role of new test_package rows.
|
||||
*/
|
||||
create or replace function test_domain_test_package_insert_tf()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
strict as $$
|
||||
begin
|
||||
call grantPermissionToRole(
|
||||
testPackageAdmin(NEW),
|
||||
createPermission(NEW.uuid, 'INSERT', 'test_domain'));
|
||||
return NEW;
|
||||
end; $$;
|
||||
|
||||
create trigger test_domain_test_package_insert_tg
|
||||
after insert on test_package
|
||||
for each row
|
||||
execute procedure test_domain_test_package_insert_tf();
|
||||
|
||||
/**
|
||||
Checks if the user or assumed roles are allowed to insert a row to test_domain.
|
||||
*/
|
||||
create or replace function test_domain_insert_permission_missing_tf()
|
||||
returns trigger
|
||||
language plpgsql as $$
|
||||
begin
|
||||
raise exception '[403] insert into test_domain not allowed for current subjects % (%)',
|
||||
currentSubjects(), currentSubjectsUuids();
|
||||
end; $$;
|
||||
|
||||
create trigger test_domain_insert_permission_check_tg
|
||||
before insert on test_domain
|
||||
for each row
|
||||
when ( not hasInsertPermission(NEW.packageUuid, 'INSERT', 'test_domain') )
|
||||
execute procedure test_domain_insert_permission_missing_tf();
|
||||
|
||||
--//
|
||||
-- ============================================================================
|
||||
--changeset test-domain-rbac-IDENTITY-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacIdentityView('test_domain', $idName$
|
||||
target.name
|
||||
|
||||
call generateRbacIdentityViewFromProjection('test_domain', $idName$
|
||||
name
|
||||
$idName$);
|
||||
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-domain-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
Creates a view to the customer main table which maps the identifying name
|
||||
(in this case, the prefix) to the objectUuid.
|
||||
*/
|
||||
drop view if exists test_domain_rv;
|
||||
create or replace view test_domain_rv as
|
||||
select target.*
|
||||
from test_domain as target
|
||||
where target.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('view', 'domain', currentSubjectsUuids()));
|
||||
grant all privileges on test_domain_rv to ${HSADMINNG_POSTGRES_RESTRICTED_USERNAME};
|
||||
call generateRbacRestrictedView('test_domain',
|
||||
'name',
|
||||
$updates$
|
||||
version = new.version,
|
||||
packageUuid = new.packageUuid,
|
||||
description = new.description
|
||||
$updates$);
|
||||
--//
|
||||
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ begin
|
||||
|
||||
perform createRoleWithGrants(
|
||||
hsOfficeContactOwner(NEW),
|
||||
permissions => array['*'],
|
||||
permissions => array['DELETE'],
|
||||
incomingSuperRoles => array[globalAdmin()],
|
||||
userUuids => array[currentUserUuid()],
|
||||
grantedByRole => globalAdmin()
|
||||
@@ -41,7 +41,7 @@ begin
|
||||
|
||||
perform createRoleWithGrants(
|
||||
hsOfficeContactAdmin(NEW),
|
||||
permissions => array['edit'],
|
||||
permissions => array['UPDATE'],
|
||||
incomingSuperRoles => array[hsOfficeContactOwner(NEW)]
|
||||
);
|
||||
|
||||
@@ -52,7 +52,7 @@ begin
|
||||
|
||||
perform createRoleWithGrants(
|
||||
hsOfficeContactGuest(NEW),
|
||||
permissions => array['view'],
|
||||
permissions => array['SELECT'],
|
||||
incomingSuperRoles => array[hsOfficeContactTenant(NEW)]
|
||||
);
|
||||
|
||||
@@ -75,7 +75,7 @@ execute procedure createRbacRolesForHsOfficeContact();
|
||||
--changeset hs-office-contact-rbac-IDENTITY-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
call generateRbacIdentityView('hs_office_contact', $idName$
|
||||
call generateRbacIdentityViewFromProjection('hs_office_contact', $idName$
|
||||
target.label
|
||||
$idName$);
|
||||
--//
|
||||
|
||||
@@ -31,16 +31,16 @@ begin
|
||||
|
||||
perform createRoleWithGrants(
|
||||
hsOfficePersonOwner(NEW),
|
||||
permissions => array['*'],
|
||||
permissions => array['DELETE'],
|
||||
incomingSuperRoles => array[globalAdmin()],
|
||||
userUuids => array[currentUserUuid()],
|
||||
grantedByRole => globalAdmin()
|
||||
);
|
||||
|
||||
-- TODO: who is admin? the person itself? is it allowed for the person itself or a representative to edit the data?
|
||||
-- TODO: who is admin? the person itself? is it allowed for the person itself or a representative to update the data?
|
||||
perform createRoleWithGrants(
|
||||
hsOfficePersonAdmin(NEW),
|
||||
permissions => array['edit'],
|
||||
permissions => array['UPDATE'],
|
||||
incomingSuperRoles => array[hsOfficePersonOwner(NEW)]
|
||||
);
|
||||
|
||||
@@ -51,7 +51,7 @@ begin
|
||||
|
||||
perform createRoleWithGrants(
|
||||
hsOfficePersonGuest(NEW),
|
||||
permissions => array['view'],
|
||||
permissions => array['SELECT'],
|
||||
incomingSuperRoles => array[hsOfficePersonTenant(NEW)]
|
||||
);
|
||||
|
||||
@@ -73,7 +73,7 @@ execute procedure createRbacRolesForHsOfficePerson();
|
||||
-- ============================================================================
|
||||
--changeset hs-office-person-rbac-IDENTITY-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacIdentityView('hs_office_person', $idName$
|
||||
call generateRbacIdentityViewFromProjection('hs_office_person', $idName$
|
||||
concat(target.tradeName, target.familyName, target.givenName)
|
||||
$idName$);
|
||||
--//
|
||||
|
||||
@@ -42,151 +42,3 @@ subgraph hsOfficeRelationship
|
||||
end
|
||||
```
|
||||
|
||||
if TG_OP = 'INSERT' then
|
||||
|
||||
-- the owner role with full access for admins of the relAnchor global admins
|
||||
ownerRole = createRole(
|
||||
hsOfficeRelationshipOwner(NEW),
|
||||
grantingPermissions(forObjectUuid => NEW.uuid, permitOps => array ['*']),
|
||||
beneathRoles(array[
|
||||
globalAdmin(),
|
||||
hsOfficePersonAdmin(newRelAnchor)])
|
||||
);
|
||||
|
||||
-- the admin role with full access for the owner
|
||||
adminRole = createRole(
|
||||
hsOfficeRelationshipAdmin(NEW),
|
||||
grantingPermissions(forObjectUuid => NEW.uuid, permitOps => array ['edit']),
|
||||
beneathRole(ownerRole)
|
||||
);
|
||||
|
||||
-- the tenant role for those related users who can view the data
|
||||
perform createRole(
|
||||
hsOfficeRelationshipTenant,
|
||||
grantingPermissions(forObjectUuid => NEW.uuid, permitOps => array ['view']),
|
||||
beneathRoles(array[
|
||||
hsOfficePersonAdmin(newRelAnchor),
|
||||
hsOfficePersonAdmin(newRelHolder),
|
||||
hsOfficeContactAdmin(newContact)]),
|
||||
withSubRoles(array[
|
||||
hsOfficePersonTenant(newRelAnchor),
|
||||
hsOfficePersonTenant(newRelHolder),
|
||||
hsOfficeContactTenant(newContact)])
|
||||
);
|
||||
|
||||
-- anchor and holder admin roles need each others tenant role
|
||||
-- to be able to see the joined relationship
|
||||
call grantRoleToRole(hsOfficePersonTenant(newRelAnchor), hsOfficePersonAdmin(newRelHolder));
|
||||
call grantRoleToRole(hsOfficePersonTenant(newRelHolder), hsOfficePersonAdmin(newRelAnchor));
|
||||
call grantRoleToRoleIfNotNull(hsOfficePersonTenant(newRelHolder), hsOfficeContactAdmin(newContact));
|
||||
|
||||
elsif TG_OP = 'UPDATE' then
|
||||
|
||||
if OLD.contactUuid <> NEW.contactUuid then
|
||||
-- nothing but the contact can be updated,
|
||||
-- in other cases, a new relationship needs to be created and the old updated
|
||||
|
||||
select * from hs_office_contact as c where c.uuid = OLD.contactUuid into oldContact;
|
||||
|
||||
call revokeRoleFromRole( hsOfficeRelationshipTenant, hsOfficeContactAdmin(oldContact) );
|
||||
call grantRoleToRole( hsOfficeRelationshipTenant, hsOfficeContactAdmin(newContact) );
|
||||
|
||||
call revokeRoleFromRole( hsOfficeContactTenant(oldContact), hsOfficeRelationshipTenant );
|
||||
call grantRoleToRole( hsOfficeContactTenant(newContact), hsOfficeRelationshipTenant );
|
||||
end if;
|
||||
else
|
||||
raise exception 'invalid usage of TRIGGER';
|
||||
end if;
|
||||
|
||||
return NEW;
|
||||
end; $$;
|
||||
|
||||
/*
|
||||
An AFTER INSERT TRIGGER which creates the role structure for a new customer.
|
||||
*/
|
||||
create trigger createRbacRolesForHsOfficeRelationship_Trigger
|
||||
after insert
|
||||
on hs_office_relationship
|
||||
for each row
|
||||
execute procedure hsOfficeRelationshipRbacRolesTrigger();
|
||||
|
||||
/*
|
||||
An AFTER UPDATE TRIGGER which updates the role structure of a customer.
|
||||
*/
|
||||
create trigger updateRbacRolesForHsOfficeRelationship_Trigger
|
||||
after update
|
||||
on hs_office_relationship
|
||||
for each row
|
||||
execute procedure hsOfficeRelationshipRbacRolesTrigger();
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset hs-office-relationship-rbac-IDENTITY-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacIdentityView('hs_office_relationship', $idName$
|
||||
(select idName from hs_office_person_iv p where p.uuid = target.relAnchorUuid)
|
||||
|| '-with-' || target.relType || '-' ||
|
||||
(select idName from hs_office_person_iv p where p.uuid = target.relHolderUuid)
|
||||
$idName$);
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset hs-office-relationship-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacRestrictedView('hs_office_relationship',
|
||||
'(select idName from hs_office_person_iv p where p.uuid = target.relHolderUuid)',
|
||||
$updates$
|
||||
contactUuid = new.contactUuid
|
||||
$updates$);
|
||||
--//
|
||||
|
||||
-- TODO: exception if one tries to amend any other column
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset hs-office-relationship-rbac-NEW-RELATHIONSHIP:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
/*
|
||||
Creates a global permission for new-relationship and assigns it to the hostsharing admins role.
|
||||
*/
|
||||
do language plpgsql $$
|
||||
declare
|
||||
addCustomerPermissions uuid[];
|
||||
globalObjectUuid uuid;
|
||||
globalAdminRoleUuid uuid ;
|
||||
begin
|
||||
call defineContext('granting global new-relationship permission to global admin role', null, null, null);
|
||||
|
||||
globalAdminRoleUuid := findRoleId(globalAdmin());
|
||||
globalObjectUuid := (select uuid from global);
|
||||
addCustomerPermissions := createPermissions(globalObjectUuid, array ['new-relationship']);
|
||||
call grantPermissionsToRole(globalAdminRoleUuid, addCustomerPermissions);
|
||||
end;
|
||||
$$;
|
||||
|
||||
/**
|
||||
Used by the trigger to prevent the add-customer to current user respectively assumed roles.
|
||||
*/
|
||||
create or replace function addHsOfficeRelationshipNotAllowedForCurrentSubjects()
|
||||
returns trigger
|
||||
language PLPGSQL
|
||||
as $$
|
||||
begin
|
||||
raise exception '[403] new-relationship not permitted for %',
|
||||
array_to_string(currentSubjects(), ';', 'null');
|
||||
end; $$;
|
||||
|
||||
/**
|
||||
Checks if the user or assumed roles are allowed to create a new customer.
|
||||
*/
|
||||
create trigger hs_office_relationship_insert_trigger
|
||||
before insert
|
||||
on hs_office_relationship
|
||||
for each row
|
||||
-- TODO.spec: who is allowed to create new relationships
|
||||
when ( not hasAssumedRole() )
|
||||
execute procedure addHsOfficeRelationshipNotAllowedForCurrentSubjects();
|
||||
--//
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ begin
|
||||
|
||||
perform createRoleWithGrants(
|
||||
hsOfficeRelationshipOwner(NEW),
|
||||
permissions => array['*'],
|
||||
permissions => array['DELETE'],
|
||||
incomingSuperRoles => array[
|
||||
globalAdmin(),
|
||||
hsOfficePersonAdmin(newRelAnchor)]
|
||||
@@ -53,14 +53,14 @@ begin
|
||||
|
||||
perform createRoleWithGrants(
|
||||
hsOfficeRelationshipAdmin(NEW),
|
||||
permissions => array['edit'],
|
||||
permissions => array['UPDATE'],
|
||||
incomingSuperRoles => array[hsOfficeRelationshipOwner(NEW)]
|
||||
);
|
||||
|
||||
-- the tenant role for those related users who can view the data
|
||||
perform createRoleWithGrants(
|
||||
hsOfficeRelationshipTenant,
|
||||
permissions => array['view'],
|
||||
permissions => array['SELECT'],
|
||||
incomingSuperRoles => array[
|
||||
hsOfficeRelationshipAdmin(NEW),
|
||||
hsOfficePersonAdmin(newRelAnchor),
|
||||
@@ -124,7 +124,7 @@ execute procedure hsOfficeRelationshipRbacRolesTrigger();
|
||||
-- ============================================================================
|
||||
--changeset hs-office-relationship-rbac-IDENTITY-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacIdentityView('hs_office_relationship', $idName$
|
||||
call generateRbacIdentityViewFromProjection('hs_office_relationship', $idName$
|
||||
(select idName from hs_office_person_iv p where p.uuid = target.relAnchorUuid)
|
||||
|| '-with-' || target.relType || '-' ||
|
||||
(select idName from hs_office_person_iv p where p.uuid = target.relHolderUuid)
|
||||
|
||||
@@ -48,13 +48,13 @@ begin
|
||||
|
||||
perform createRoleWithGrants(
|
||||
hsOfficePartnerOwner(NEW),
|
||||
permissions => array['*'],
|
||||
permissions => array['DELETE'],
|
||||
incomingSuperRoles => array[globalAdmin()]
|
||||
);
|
||||
|
||||
perform createRoleWithGrants(
|
||||
hsOfficePartnerAdmin(NEW),
|
||||
permissions => array['edit'],
|
||||
permissions => array['UPDATE'],
|
||||
incomingSuperRoles => array[
|
||||
hsOfficePartnerOwner(NEW)],
|
||||
outgoingSubRoles => array[
|
||||
@@ -84,7 +84,7 @@ begin
|
||||
|
||||
perform createRoleWithGrants(
|
||||
hsOfficePartnerGuest(NEW),
|
||||
permissions => array['view'],
|
||||
permissions => array['SELECT'],
|
||||
incomingSuperRoles => array[hsOfficePartnerTenant(NEW)]
|
||||
);
|
||||
|
||||
@@ -98,21 +98,21 @@ begin
|
||||
--Attention: Cannot be in partner-details because of insert order (partner is not in database yet)
|
||||
|
||||
call grantPermissionsToRole(
|
||||
getRoleId(hsOfficePartnerOwner(NEW), 'fail'),
|
||||
createPermissions(NEW.detailsUuid, array ['*'])
|
||||
getRoleId(hsOfficePartnerOwner(NEW)),
|
||||
createPermissions(NEW.detailsUuid, array ['DELETE'])
|
||||
);
|
||||
|
||||
call grantPermissionsToRole(
|
||||
getRoleId(hsOfficePartnerAdmin(NEW), 'fail'),
|
||||
createPermissions(NEW.detailsUuid, array ['edit'])
|
||||
getRoleId(hsOfficePartnerAdmin(NEW)),
|
||||
createPermissions(NEW.detailsUuid, array ['UPDATE'])
|
||||
);
|
||||
|
||||
call grantPermissionsToRole(
|
||||
-- Yes, here hsOfficePartnerAGENT is used, not hsOfficePartnerTENANT.
|
||||
-- Do NOT grant view permission on partner-details to hsOfficePartnerTENANT!
|
||||
-- Otherwise package-admins etc. would be able to read the data.
|
||||
getRoleId(hsOfficePartnerAgent(NEW), 'fail'),
|
||||
createPermissions(NEW.detailsUuid, array ['view'])
|
||||
getRoleId(hsOfficePartnerAgent(NEW)),
|
||||
createPermissions(NEW.detailsUuid, array ['SELECT'])
|
||||
);
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ execute procedure hsOfficePartnerRbacRolesTrigger();
|
||||
-- ============================================================================
|
||||
--changeset hs-office-partner-rbac-IDENTITY-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacIdentityView('hs_office_partner', $idName$
|
||||
call generateRbacIdentityViewFromProjection('hs_office_partner', $idName$
|
||||
partnerNumber || ':' ||
|
||||
(select idName from hs_office_person_iv p where p.uuid = target.personuuid)
|
||||
|| '-' ||
|
||||
|
||||
@@ -7,13 +7,10 @@ call generateRelatedRbacObject('hs_office_partner_details');
|
||||
--//
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset hs-office-partner-details-rbac-IDENTITY-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacIdentityView('hs_office_partner_details', $idName$
|
||||
call generateRbacIdentityViewFromProjection('hs_office_partner_details', $idName$
|
||||
(select idName || '-details' from hs_office_partner_iv partner_iv
|
||||
join hs_office_partner partner on (partner_iv.uuid = partner.uuid)
|
||||
where partner.detailsUuid = target.uuid)
|
||||
@@ -38,7 +35,7 @@ call generateRbacRestrictedView('hs_office_partner_details',
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset hs-office-partner-details-rbac-NEW-CONTACT:1 endDelimiter:--//
|
||||
--changeset hs-office-partner-details-rbac-NEW-PARTNER-DETAILS:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
/*
|
||||
Creates a global permission for new-partner-details and assigns it to the hostsharing admins role.
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
flowchart TB
|
||||
|
||||
subgraph global
|
||||
style hsOfficeBankAccount fill: #e9f7ef
|
||||
style global fill: lightgray
|
||||
|
||||
role:global.admin[global.admin]
|
||||
end
|
||||
|
||||
subgraph hsOfficeBankAccount
|
||||
direction TB
|
||||
style hsOfficeBankAccount fill: #e9f7ef
|
||||
style hsOfficeBankAccount fill: lightgreen
|
||||
|
||||
user:hsOfficeBankAccount.creator([bankAccount.creator])
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ begin
|
||||
|
||||
perform createRoleWithGrants(
|
||||
hsOfficeBankAccountOwner(NEW),
|
||||
permissions => array['delete'],
|
||||
permissions => array['DELETE'],
|
||||
incomingSuperRoles => array[globalAdmin()],
|
||||
userUuids => array[currentUserUuid()],
|
||||
grantedByRole => globalAdmin()
|
||||
@@ -51,7 +51,7 @@ begin
|
||||
|
||||
perform createRoleWithGrants(
|
||||
hsOfficeBankAccountGuest(NEW),
|
||||
permissions => array['view'],
|
||||
permissions => array['SELECT'],
|
||||
incomingSuperRoles => array[hsOfficeBankAccountTenant(NEW)]
|
||||
);
|
||||
|
||||
@@ -74,7 +74,7 @@ execute procedure createRbacRolesForHsOfficeBankAccount();
|
||||
--changeset hs-office-bankaccount-rbac-IDENTITY-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
call generateRbacIdentityView('hs_office_bankaccount', $idName$
|
||||
call generateRbacIdentityViewFromProjection('hs_office_bankaccount', $idName$
|
||||
target.holder
|
||||
$idName$);
|
||||
--//
|
||||
|
||||
@@ -41,13 +41,13 @@ begin
|
||||
|
||||
perform createRoleWithGrants(
|
||||
hsOfficeSepaMandateOwner(NEW),
|
||||
permissions => array['*'],
|
||||
permissions => array['DELETE'],
|
||||
incomingSuperRoles => array[globalAdmin()]
|
||||
);
|
||||
|
||||
perform createRoleWithGrants(
|
||||
hsOfficeSepaMandateAdmin(NEW),
|
||||
permissions => array['edit'],
|
||||
permissions => array['UPDATE'],
|
||||
incomingSuperRoles => array[hsOfficeSepaMandateOwner(NEW)],
|
||||
outgoingSubRoles => array[hsOfficeBankAccountTenant(newHsOfficeBankAccount)]
|
||||
);
|
||||
@@ -66,7 +66,7 @@ begin
|
||||
|
||||
perform createRoleWithGrants(
|
||||
hsOfficeSepaMandateGuest(NEW),
|
||||
permissions => array['view'],
|
||||
permissions => array['SELECT'],
|
||||
incomingSuperRoles => array[hsOfficeSepaMandateTenant(NEW)]
|
||||
);
|
||||
|
||||
@@ -94,7 +94,7 @@ execute procedure hsOfficeSepaMandateRbacRolesTrigger();
|
||||
-- ============================================================================
|
||||
--changeset hs-office-sepamandate-rbac-IDENTITY-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacIdentityView('hs_office_sepamandate', idNameExpression => 'target.reference');
|
||||
call generateRbacIdentityViewFromProjection('hs_office_sepamandate', 'target.reference');
|
||||
--//
|
||||
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ begin
|
||||
|
||||
perform createRoleWithGrants(
|
||||
hsOfficeDebitorOwner(NEW),
|
||||
permissions => array['*'],
|
||||
permissions => array['DELETE'],
|
||||
incomingSuperRoles => array[globalAdmin()],
|
||||
userUuids => array[currentUserUuid()],
|
||||
grantedByRole => globalAdmin()
|
||||
@@ -57,7 +57,7 @@ begin
|
||||
|
||||
perform createRoleWithGrants(
|
||||
hsOfficeDebitorAdmin(NEW),
|
||||
permissions => array['edit'],
|
||||
permissions => array['UPDATE'],
|
||||
incomingSuperRoles => array[hsOfficeDebitorOwner(NEW)]
|
||||
);
|
||||
|
||||
@@ -85,7 +85,7 @@ begin
|
||||
|
||||
perform createRoleWithGrants(
|
||||
hsOfficeDebitorGuest(NEW),
|
||||
permissions => array['view'],
|
||||
permissions => array['SELECT'],
|
||||
incomingSuperRoles => array[
|
||||
hsOfficeDebitorTenant(NEW)]
|
||||
);
|
||||
@@ -173,7 +173,7 @@ execute procedure hsOfficeDebitorRbacRolesTrigger();
|
||||
-- ============================================================================
|
||||
--changeset hs-office-debitor-rbac-IDENTITY-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacIdentityView('hs_office_debitor', $idName$
|
||||
call generateRbacIdentityViewFromProjection('hs_office_debitor', $idName$
|
||||
'#' ||
|
||||
(select partnerNumber from hs_office_partner p where p.uuid = target.partnerUuid) ||
|
||||
to_char(debitorNumberSuffix, 'fm00') ||
|
||||
|
||||
@@ -41,13 +41,13 @@ begin
|
||||
|
||||
perform createRoleWithGrants(
|
||||
hsOfficeMembershipOwner(NEW),
|
||||
permissions => array['*'],
|
||||
permissions => array['DELETE'],
|
||||
incomingSuperRoles => array[globalAdmin()]
|
||||
);
|
||||
|
||||
perform createRoleWithGrants(
|
||||
hsOfficeMembershipAdmin(NEW),
|
||||
permissions => array['edit'],
|
||||
permissions => array['UPDATE'],
|
||||
incomingSuperRoles => array[hsOfficeMembershipOwner(NEW)]
|
||||
);
|
||||
|
||||
@@ -65,7 +65,7 @@ begin
|
||||
|
||||
perform createRoleWithGrants(
|
||||
hsOfficeMembershipGuest(NEW),
|
||||
permissions => array['view'],
|
||||
permissions => array['SELECT'],
|
||||
incomingSuperRoles => array[hsOfficeMembershipTenant(NEW), hsOfficePartnerTenant(newHsOfficePartner), hsOfficeDebitorTenant(newHsOfficeDebitor)]
|
||||
);
|
||||
|
||||
@@ -93,7 +93,7 @@ execute procedure hsOfficeMembershipRbacRolesTrigger();
|
||||
-- ============================================================================
|
||||
--changeset hs-office-membership-rbac-IDENTITY-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacIdentityView('hs_office_membership', idNameExpression => $idName$
|
||||
call generateRbacIdentityViewFromProjection('hs_office_membership', $idName$
|
||||
'#' ||
|
||||
(select partnerNumber from hs_office_partner p where p.uuid = target.partnerUuid) ||
|
||||
memberNumberSuffix ||
|
||||
|
||||
@@ -42,8 +42,8 @@ begin
|
||||
|
||||
-- coopsharestransactions cannot be edited nor deleted, just created+viewed
|
||||
call grantPermissionsToRole(
|
||||
getRoleId(hsOfficeMembershipTenant(newHsOfficeMembership), 'fail'),
|
||||
createPermissions(NEW.uuid, array ['view'])
|
||||
getRoleId(hsOfficeMembershipTenant(newHsOfficeMembership)),
|
||||
createPermissions(NEW.uuid, array ['SELECT'])
|
||||
);
|
||||
|
||||
else
|
||||
@@ -68,8 +68,7 @@ execute procedure hsOfficeCoopSharesTransactionRbacRolesTrigger();
|
||||
-- ============================================================================
|
||||
--changeset hs-office-coopSharesTransaction-rbac-IDENTITY-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacIdentityView('hs_office_coopSharesTransaction',
|
||||
idNameExpression => 'target.reference');
|
||||
call generateRbacIdentityViewFromProjection('hs_office_coopSharesTransaction', 'target.reference');
|
||||
--//
|
||||
|
||||
|
||||
|
||||
@@ -42,8 +42,8 @@ begin
|
||||
|
||||
-- coopassetstransactions cannot be edited nor deleted, just created+viewed
|
||||
call grantPermissionsToRole(
|
||||
getRoleId(hsOfficeMembershipTenant(newHsOfficeMembership), 'fail'),
|
||||
createPermissions(NEW.uuid, array ['view'])
|
||||
getRoleId(hsOfficeMembershipTenant(newHsOfficeMembership)),
|
||||
createPermissions(NEW.uuid, array ['SELECT'])
|
||||
);
|
||||
|
||||
else
|
||||
@@ -68,8 +68,7 @@ execute procedure hsOfficeCoopAssetsTransactionRbacRolesTrigger();
|
||||
-- ============================================================================
|
||||
--changeset hs-office-coopAssetsTransaction-rbac-IDENTITY-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacIdentityView('hs_office_coopAssetsTransaction',
|
||||
idNameExpression => 'target.reference');
|
||||
call generateRbacIdentityViewFromProjection('hs_office_coopAssetsTransaction', 'target.reference');
|
||||
--//
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user