diff --git a/.gitignore b/.gitignore
index d6a2e347..522bf4fa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,7 +4,6 @@
/build/www/**
/src/test/javascript/coverage/
/worktrees/
-TODO-progress.png
######################
# Node
diff --git a/README.md b/README.md
index 04827ba3..4d03a6d3 100644
--- a/README.md
+++ b/README.md
@@ -82,7 +82,7 @@ If you have at least Docker and the Java JDK installed in appropriate versions a
# the following command should return a JSON array with just all packages visible for the admin of the customer yyy:
curl \
- -H 'current-user: superuser-alex@hostsharing.net' -H 'assumed-roles: test_customer#yyy.admin' \
+ -H 'current-user: superuser-alex@hostsharing.net' -H 'assumed-roles: test_customer#yyy:ADMIN' \
http://localhost:8080/api/test/packages
# add a new customer
@@ -380,12 +380,6 @@ You can explore the prototype as follows:
`src/`
The actual source-code, see [Source Code Package Structure](#source-code-package-structure) for details.
-`TODO.md`
- Requirements of initial project. Do not touch!
-
-`TODO-progress.png`
- Generated diagram image of the project progress.
-
`tools/`
Some shell-scripts to useful tasks.
@@ -765,5 +759,4 @@ The output will list the generated files.
## Further Documentation
- the `doc` directory contains architecture concepts and a glossary
-- TODO.md tracks requirements and progress for the contract of the initial project,
- please do not amend anything in this document
+- the `ideas` directory contains unstructured ideas for future development or documentation
diff --git a/build.gradle b/build.gradle
index 6539242e..88c59050 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,15 +1,15 @@
plugins {
id 'java'
- id 'org.springframework.boot' version '3.1.7'
+ id 'org.springframework.boot' version '3.2.4'
id 'io.spring.dependency-management' version '1.1.4'
id 'io.openapiprocessor.openapi-processor' version '2023.2'
- id 'com.github.jk1.dependency-license-report' version '2.5'
- id "org.owasp.dependencycheck" version "9.0.7"
- id "com.diffplug.spotless" version "6.23.3"
+ id 'com.github.jk1.dependency-license-report' version '2.6'
+ id "org.owasp.dependencycheck" version "9.0.10"
+ id "com.diffplug.spotless" version "6.25.0"
id 'jacoco'
id 'info.solidsoft.pitest' version '1.15.0'
id 'se.patrikerdes.use-latest-versions' version '0.2.18'
- id 'com.github.ben-manes.versions' version '0.50.0'
+ id 'com.github.ben-manes.versions' version '0.51.0'
}
group = 'net.hostsharing'
@@ -59,28 +59,16 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.1'
- implementation 'org.springdoc:springdoc-openapi:2.3.0'
- implementation 'org.postgresql:postgresql:42.7.1'
- implementation 'org.liquibase:liquibase-core:4.25.1'
- implementation 'com.vladmihalcea:hibernate-types-60:2.21.1'
- implementation 'io.hypersistence:hypersistence-utils-hibernate-62:3.7.0'
- implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.16.1'
+ implementation 'org.springdoc:springdoc-openapi:2.4.0'
+ implementation 'org.postgresql:postgresql:42.7.3'
+ implementation 'org.liquibase:liquibase-core:4.27.0'
+ implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.7.3'
+ implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.0'
implementation 'org.openapitools:jackson-databind-nullable:0.2.6'
implementation 'org.apache.commons:commons-text:1.11.0'
implementation 'org.modelmapper:modelmapper:3.2.0'
implementation 'org.iban4j:iban4j:3.2.7-RELEASE'
- implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
-
- // fixes vulnerability CVE-2022-1471
- // The dependency usually comes from Spring Boot, just in the wrong version.
- // TODO: Remove this explicit dependency once we are on SpringBoot 3.2.x
- // as well as the related exclude in settings.gradle
- // and the dependency suppression in owasp-dependency-check-suppression.xml.
- implementation('org.yaml:snakeyaml') {
- version {
- strictly('2.2')
- }
- }
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.4.0'
compileOnly 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
diff --git a/doc/hs-office-data-structure.md b/doc/hs-office-data-structure.md
index 960e572b..b84264d0 100644
--- a/doc/hs-office-data-structure.md
+++ b/doc/hs-office-data-structure.md
@@ -10,7 +10,7 @@ classDiagram
namespace Partner {
class partner-MeierGmbH
- class role-MeierGmbH
+ class rel-MeierGmbH
class personDetails-MeierGmbH
class contactData-MeierGmbH
class person-MeierGmbH
@@ -19,28 +19,29 @@ classDiagram
namespace Representatives {
class person-FrankMeier
class contactData-FrankMeier
- class role-MeierGmbH-FrankMeier
+ class rel-MeierGmbH-FrankMeier
}
namespace Debitors {
class debitor-MeierGmbH
class contactData-MeierGmbH-Buha
- class role-MeierGmbH-Buha
+ class rel-MeierGmbH-Buha
}
namespace Operations {
class person-SabineMeier
class contactData-SabineMeier
- class role-MeierGmbH-SabineMeier
+ class rel-MeierGmbH-SabineMeier
}
namespace Enums {
- class RoleType {
+ class RelationType {
<>
UNKNOWN
+ PARTNER
+ DEBITOR
REPRESENTATIVE
- ACCOUNTING
OPERATIONS
}
@@ -64,9 +65,9 @@ classDiagram
class partner-MeierGmbH {
+Numeric partnerNumber: 12345
- +Role partnerRole
+ +Relation partnerRel
}
- partner-MeierGmbH *-- role-MeierGmbH
+ partner-MeierGmbH *-- rel-MeierGmbH
class person-MeierGmbH {
+personType: LEGAL
@@ -90,32 +91,32 @@ classDiagram
+emailAddresses: office@meier-gmbh.de
}
- class role-MeierGmbH {
- +RoleType RoleType PARTNER
+ class rel-MeierGmbH {
+ +RelationType type PARTNER
+Person anchor
+Person holder
- +Contact roleContact
+ +Contact contact
}
- role-MeierGmbH o-- person-HostsharingEG : anchor
- role-MeierGmbH o-- person-MeierGmbH : holder
- role-MeierGmbH o-- contactData-MeierGmbH
+ rel-MeierGmbH o-- person-HostsharingEG : anchor
+ rel-MeierGmbH o-- person-MeierGmbH : holder
+ rel-MeierGmbH o-- contactData-MeierGmbH
%% --- Debitors ---
class debitor-MeierGmbH {
- +Partner partner
- +Numeric[2] debitorNumberSuffix: 00
- +Role billingRole
- +boolean billable: true
- +String vatId: ID123456789
- +String vatCountryCode: DE
- +boolean vatBusiness: true
- +boolean vatReverseCharge: false
+ +Partner partner
+ +Numeric[2] debitorNumberSuffix: 00
+ +Relation debitorRel
+ +boolean billable: true
+ +String vatId: ID123456789
+ +String vatCountryCode: DE
+ +boolean vatBusiness: true
+ +boolean vatReverseCharge: false
+BankAccount refundBankAccount
- +String defaultPrefix: mei
+ +String defaultPrefix: mei
}
debitor-MeierGmbH o-- partner-MeierGmbH
- debitor-MeierGmbH *-- role-MeierGmbH-Buha
+ debitor-MeierGmbH *-- rel-MeierGmbH-Buha
class contactData-MeierGmbH-Buha {
+postalAddress: Hauptstraße 5, 22345 Hamburg
@@ -123,15 +124,15 @@ classDiagram
+emailAddresses: buha@meier-gmbh.de
}
- class role-MeierGmbH-Buha {
- +RoleType RoleType ACCOUNTING
+ class rel-MeierGmbH-Buha {
+ +RelationType type DEBITOR
+Person anchor
+Person holder
- +Contact roleContact
+ +Contact contact
}
- role-MeierGmbH-Buha o-- person-MeierGmbH : anchor
- role-MeierGmbH-Buha o-- person-MeierGmbH : holder
- role-MeierGmbH-Buha o-- contactData-MeierGmbH-Buha
+ rel-MeierGmbH-Buha o-- person-MeierGmbH : anchor
+ rel-MeierGmbH-Buha o-- person-MeierGmbH : holder
+ rel-MeierGmbH-Buha o-- contactData-MeierGmbH-Buha
%% --- Representatives ---
@@ -148,15 +149,15 @@ classDiagram
+emailAddresses: frank.meier@meier-gmbh.de
}
- class role-MeierGmbH-FrankMeier {
- +RoleType RoleType REPRESENTATIVE
+ class rel-MeierGmbH-FrankMeier {
+ +RelationType type REPRESENTATIVE
+Person anchor
+Person holder
- +Contact roleContact
+ +Contact contact
}
- role-MeierGmbH-FrankMeier o-- person-MeierGmbH : anchor
- role-MeierGmbH-FrankMeier o-- person-FrankMeier : holder
- role-MeierGmbH-FrankMeier o-- contactData-FrankMeier
+ rel-MeierGmbH-FrankMeier o-- person-MeierGmbH : anchor
+ rel-MeierGmbH-FrankMeier o-- person-FrankMeier : holder
+ rel-MeierGmbH-FrankMeier o-- contactData-FrankMeier
%% --- Operations ---
@@ -173,14 +174,14 @@ classDiagram
+emailAddresses: sabine.meier@meier-gmbh.de
}
- class role-MeierGmbH-SabineMeier {
- +RoleType RoleType OPERATIONAL
+ class rel-MeierGmbH-SabineMeier {
+ +RelationType type OPERATIONAL
+Person anchor
+Person holder
- +Contact roleContact
+ +Contact contact
}
- role-MeierGmbH-SabineMeier o-- person-MeierGmbH : anchor
- role-MeierGmbH-SabineMeier o-- person-SabineMeier : holder
- role-MeierGmbH-SabineMeier o-- contactData-SabineMeier
+ rel-MeierGmbH-SabineMeier o-- person-MeierGmbH : anchor
+ rel-MeierGmbH-SabineMeier o-- person-SabineMeier : holder
+ rel-MeierGmbH-SabineMeier o-- contactData-SabineMeier
```
diff --git a/doc/ideas/rbac-schema-f.md b/doc/ideas/rbac-schema-f.md
new file mode 100644
index 00000000..f1731d4f
--- /dev/null
+++ b/doc/ideas/rbac-schema-f.md
@@ -0,0 +1,82 @@
+*(this is just a scribbled draft, that's why it's still in German)*
+
+### *Schema-F* für Permissions, Rollen und Grants
+
+Permissions, Rollen und Grants werden in den INSERT/UPDATE/DELETE-Triggern von Geschäftsobjekten erzeugt und gelöscht. Das Löschen erfolgt meistens automatisch über das zugehörige RbacObject, die INSERT- und UPDATE-Trigger müssen jedoch in *pl/pgsql* ausprogrammiert werden.
+
+Das folgende Schema soll dabei unterstützen, die richtigen Permissions, Rollen und Grants festzulegen.
+
+An einigen Stellen ist vom *Initiator* die Rede. Als *Initiator* gilt derjenige User, der die Operation (INSERT oder UPDATE) durchführt bzw. dessen primary assumed Rol. (TODO: bisher gibt es nur assumed roles, das Konzept einer primary assumed Role müsste noch eingeführt werden, derzeit nehmen wir dafür immer den `globalAdmin()`. Bevor Kunden aber selbst Objekte anlegen können, muss das geklärt sein.)
+
+#### Typ Root: Objekte, welche nur eine Spezialisierung bzw. Zusatzdaten für andere Objekte bereitstellen (z.B. Partner für Relations vom Typ Partner oder Partner Details für Partner)
+
+Objektorientiert gedacht, enthalten solche Objekte die Zusatzdaten einer Subklasse; die Daten im Partner erweitern also eine Relation vom Typ `partner`.
+
+- Dann muss dieses Objekt zeitlich nach dem Objekt erzeugt werden, auf dass es sich bezieht, also z.B. zeitlich nach der Relation.
+- Es werden Delete (\*), Edit und View Permissions für dieses Objekt erzeugt.
+- Es werden **keine** Rollen für dieses Objekt erzeugt.
+- Statt eigener Rollen werden die o.g. Permissions passenden Rollen des Hauptobjekts zugewiesen (granted) bzw. aus denen entfernt (revoked).
+ - Handelt es sich um Zusatzdaten zum Zwecke der Spezialisierung, dann z.B. so:
+ - Delete (\*) <-- Owner des Hauptobjektes
+ - Edit <-- **Admin** des Hauptobjektes
+ - View <-- Agent des Hauptobjektes
+ - Handelt es sich um Zusatzdaten, für die sich Edit-Rechte delegieren lassen sollen (wie im Falle der Partner-Details eines Partners), dann z.B. so:
+ - Delete (\*) <-- Owner des Hauptobjektes
+ - Edit <-- **Agent** des Hauptobjektes
+ - View <-- Agent des Hauptobjektes
+- Für die Rollenzuordnung zwischen referenzierten Objekten gilt:
+ - Für Objekte vom Typ Root werden die Rollen des zugehörigen Aggregator-Objektes verwendet.
+ - Gibt es Referenzen auf hierarchisch verbundene Objekte (z.B. Debitor.refundBankAccount) gilt folgende Faustregel:
+ ***Nach oben absteigen, nach unten halten oder aufsteigen.*** An einem fachlich übergeordneten Objekt wird also eine niedrigere Rolle (z.B. Debitor.ADMIN -> Partner.AGENT), einem fachlich untergeordneten Objekt eine gleichwertige Rolle (z.B. Partner.ADMIN -> Debitor.ADMIN) zugewiesen oder sogar aufgestiegen (Debitor.ADMIN -> Package.TENANT).
+ - Für Referenzen zwischen Objekten, die nicht hierarchisch zueinander stehen (z.B. Debitor und Bankverbindung), wird auf beiden seiten abgestiegen (also Debitor.ADMIN -> BankAccount.REFERRER und BankAccount.ADMIN -> Debitor.TENANT).
+
+Anmerkung: Der Typ-Begriff *Root* bezieht sich auf die Rolle im fachlichen Datenmodell. Im Bezug auf den Teilgraphen eines fachlichen Kontexts ist dies auch eine Wurzel im Sinne der Graphentheorie. Aber in anderen fachlichen Kontexten können auch diese Objekte von anderen Teilgraphen referenziert werden und werden dann zum inneren Knoten.
+
+
+#### Typ Aggregator: Objekte, welche weitere Objekte zusammenfassen (z.B. Relation fasst zwei Persons und einen Contact zusammen)
+
+Solche Objekte verweisen üblicherweise auf Objekte vom Typ Leaf und werden oft von Objekten des Typs Root referenziert.
+
+- Es werden i.d.R. folgende Rollen für diese Objekte erzeugt:
+ - Owner, Admin, Agent, Tenent(, Guest?)
+- Es werden Delete (\*), Edit und View Permissions für dieses Objekt erzeugt.
+- Die Permissions werden den Rollen sinnvoll zugewiesen, z.B.:
+ - Owner -> Delete (\*)
+ - Admin --> Edit
+ - Tenant (oder ggf. Guest) --> View
+- Außerdem werden folgende Grants erstellt bzw. entzogen:
+ - Initiator --> Owner
+ - Owner --> Admin
+ - Admin --> Referrer
+ - Admins der referenzierten Objekte werden Agent des Aggregators
+ - Tenants des Aggregators werden Referrer der referenzierten Objekte
+
+### Typ Leaf: Handelt es sich um ein Objekt, welches (außer zur Modellierung separater Permissions) keine Unterobjekte enthält (z.B. Person, Customer)?
+
+Solche Objekte werden üblicherweise von Objekten des Typs Aggregator, manchmal auch von Objekten des Typs Root, referenziert.
+
+- Es werden i.d.R. folgende Rollen für diese Objekte erzeugt:
+ - Owner, Admin, Referrer
+- Es werden Delete (\*), Edit und View Permissions für dieses Objekt erzeugt.
+- Die Permissions werden den Rollen sinnvoll zugewiesen, z.B.:
+ - Delete (\*) <-- Owner
+ - Edit <-- Admin
+ - View <-- Referrer
+- Außerdem werden folgende Grants erstellt bzw. entzogen:
+ - Owner --> Admin
+ - Admin --> Referrer
+
+```mermaid
+flowchart LR
+
+subgraph partnerDetails
+ direction TB
+ style partnerDetails fill:#eee
+
+ perm:partnerDetails.*{{partnerDetails.*}}
+ role:partnerDetails.edit{{partnerDetails.edit}}
+ role:partnerDetails.view{{partnerDetails.view}}
+
+
+end
+```
diff --git a/doc/ideas/simplified-grant-structure.md b/doc/ideas/simplified-grant-structure.md
new file mode 100644
index 00000000..d9b3cf44
--- /dev/null
+++ b/doc/ideas/simplified-grant-structure.md
@@ -0,0 +1,29 @@
+(this is just a scribbled idea, that's why it's still in German)
+
+Ich habe mal wieder vom RBAC-System geträumt 🙈 Ok, im Halbschlaf darüber nachgedacht trifft es wohl besser. Und jetzt frage ich mich, ob wir viel zu kompliziert gedacht haben.
+
+Bislang gingen wir ja davon aus, dass, wenn komplexe Entitäten (z.B. Partner) erzeugt werden, wir wir über den INSERT-Trigger den Rollen der verknüpften Entitäten (z.B. den Rollen der Personendaten des Partners) auch Rechte an den komplexeren Entitäten und umgekehrt geben müssen.
+
+Da die komplexen Entitäten nur mit gewissen verbundenen Entitäten überhaupt sinnvoll nutzbar sind und diese daher über INNSER JOINs mitladen, könnte sonst auch nur jemand diese Entitäten, der auch die SELECT-Permission an den verküpften Entitäten hat.
+
+Vor einigen Wochen hatten wir schon einmal darüber geredet, ob wir dieses Geflecht wirklich komplett durchplanen müssen, also über mehrere Stufen hinweg, oder ob sehr warscheinlich eh dieselben Leuten an den weiter entfernten Entitäten die nötien Rechte haben, weil dahinter dieselben User stehen. Also z.B. dass gewährleistet ist, dass jemand mit ADMIN-Recht an den Personendaten des Partners auch bis in die SEPA-Mandate eines Debitors hineinsehen kann.
+
+Und nun gehe ich noch einen Schritt weiter: Könnte es nicht auch andersherum sein? Also wenn jemand z.B. SELECT-Recht am Partner hat, dass wir davon ausgehen können, dass derjenige auch die Partner-Personen- und Kontaktdaten sehen darf, und zwar implizit durch seine Partner-SELECT-Permission und ohne dass er explizit Rollen für diese Partner-Personen oder Kontaktdaten inne hat?
+
+Im Halbschlaf kam mir nur die Idee, warum wir nicht einfach die komplexen JPA-Entitäten zwar auf die restricted View setzen, wie bisher, aber für die verknüpften Entitäten auf die direkten (bisher "Raw..." genannt) Entitäten gehen. Dann könnte jemand mit einer Rolle, welche die SELECT-Permission auf die komplexe JPA-Entität (z.B.) Partner inne hat, auch die dazugehörige Relation(ship) ["Relation" wurde vor kurzem auf kurz "Relation" umbenannt] und die wiederum dazu gehörigen Personen- und Kontaktdaten lesen, ohne dass in einem INSERT- und UPDATE-Trigger der Partner-Entität die ganzen Grants mit den verknüpften Entäten aufgebaut und aktualisiert werden müssen.
+
+Beim Debitor ist das nämlich selbst mit Generator die Hölle, zumal eben auch Querverbindungen gegranted werden müssen, z.B. von der Debitor-Person zum Sema-Mandat - jedenfalls wenn man nicht Gefahr laufen wollte, dass jemand mit Admin-Rechten an der Partner-Person (also z.B. ein Repräsentant des Partners) die Sepa-Mandate der Debitoren gar nicht mehr sehen kann. Natürlich bräuchte man immer noch die Agent-Rolle am Partner und Debitor (evtl. repräsentiert durch die jeweils zugehörigen Relation - falls dieser Trick überhaupt noch nötig wäre), sowie ein Grant vom Partner-Agent auf den Debitor-Agent und vom Debitor-Agent auf die Sepa-Mandate-Admins, aber eben ohne filigran die ganzen Neben-Entäten (Personen- und Kontaktdaten von Partner und Debitor sowie Bank-Account) in jedem Trigger berücksichtigen zu müssen. Beim Refund-Bank-Account sogar besonders ätzend, weil der optional ist und dadurch zig "if ...refundBankAccountUuid is not null then ..." im Code enstehen (wenn der auch generiert ist).
+
+Mit anderen Worten, um als Repräsentant eines Geschäftspartners auf den Bank-Account der Sepa-Mandate sehen zu dürfen, wird derzeut folgende Grant-Kette durchlaufen (bzw. eben noch nicht, weil es noch nicht funktioniert):
+
+User -> Partner-Holder-Person:ADMIN -> Partner-Relation:AGENT -> Debitor-Relation:AGENT -> Sepa-Mandat:ADMIN -> BankAccount:ADMIN -> BankAccount:SELECT
+
+Daraus würde:
+
+User -> Partner-Relation:AGENT -> Debitor-Relation:AGENT -> Sepa-Mandat:ADMIN -> Sepa-Mandat:SELECT*
+
+(*mit JOIN auf RawBankAccount, also implizitem Leserecht)
+
+Das klingt zunächst nach nur einer marginalen Vereinfachung, die eigentlich Vereinfachung liegt aber im Erzeugen der Grants in den Triggern, denn da sind zudem noch Partner-Anchor-Person, Debitor-Holder- und Anchor-Person, Partner- und Debitor-Contact sowie der RefundBankAccount zu berücksichtigen. Und genau diese Grants würden großteils wegfallen, und durch implizite Persmissions über die JOINs auf die Raw-Tables ersetzt werden. Den refundBankAccound müssten wir dann, analog zu den Sepa-Mandataten, umgedreht modellieren, da den sonst
+
+Man könnte das Ganze auch als "Entwicklung der Rechtestruktur für Hosting-Entitäten auf der obersten Ebene" (Manged Webspace, Managed Server, Cloud Server etc.) sehen, denn die hängen alle unter dem Mega-komplexen Debitor.
diff --git a/doc/rbac.md b/doc/rbac.md
index 06a6ee7e..662bed29 100644
--- a/doc/rbac.md
+++ b/doc/rbac.md
@@ -1,6 +1,6 @@
## *hsadmin-ng*'s Role-Based-Access-Management (RBAC)
-The requirements of *hsadmin-ng* include table-m row- and column-level-security for read and write access to business-objects.
+The requirements of *hsadmin-ng* include table-, row- and column-level-security for read and write access to business-objects.
More precisely, any access has to be controlled according to given rules depending on the accessing users, their roles and the accessed business-object.
Further, roles and business-objects are hierarchical.
@@ -11,7 +11,7 @@ Our implementation is based on Role-Based-Access-Management (RBAC) in conjunctio
As far as possible, we are using the same terms as defined in the RBAC standard, for our function names though, we chose more expressive names.
In RBAC, subjects can be assigned to roles, roles can be hierarchical and eventually have assigned permissions.
-A permission allows a specific operation (e.g. view or edit) on a specific (business-) object.
+A permission allows a specific operation (e.g. SELECT or UPDATE) on a specific (business-) object.
You can find the entity structure as a UML class diagram as follows:
@@ -101,13 +101,12 @@ package RBAC {
RbacPermission *-- RbacObject
enum RbacOperation {
- add-package
- add-domain
- add-domain
+ INSERT:package
+ INSERT:domain
...
- view
- edit
- delete
+ SELECT
+ UPDATE
+ DELETE
}
entity RbacObject {
@@ -172,11 +171,10 @@ An *RbacPermission* allows a specific *RbacOperation* on a specific *RbacObject*
An *RbacOperation* determines, what an *RbacPermission* allows to do.
It can be one of:
-- **'add-...'** - permits creating new instances of specific entity types underneath the object specified by the permission, e.g. "add-package"
-- **'view'** - permits reading the contents of the object specified by the permission
-- **'edit'** - change the contents of the object specified by the permission
-- **'delete'** - delete the object specified by the permission
-- **'\*'**
+- **'INSERT'** - permits inserting new rows related to the row, to which the permission belongs, in the table which is specified an extra column, includes 'SELECT'
+- **'SELECT'** - permits selecting the row specified by the permission, is included in all other permissions
+- **'UPDATE'** - permits updating (only the updatable columns of) the row specified by the permission, includes 'SELECT'
+- **'DELETE'** - permits deleting the row specified by the permission, includes 'SELECT'
This list is extensible according to the needs of the access rule system.
@@ -198,56 +196,60 @@ E.g. if a new package is added, the admin-role of the related customer has to be
There can be global roles like 'administrators'.
Most roles, though, are specific for certain business-objects and automatically generated as such:
- business-object-table#business-object-name.relative-role
+ business-object-table#business-object-name.role-stereotype
Where *business-object-table* is the name of the SQL table of the business object (e.g *customer* or 'package'),
*business-object-name* is generated from an immutable business key(e.g. a prefix like 'xyz' or 'xyz00')
-and the *relative-role*' describes the role relative to the referenced business-object as follows:
+and the *role-stereotype* describes a role relative to a referenced business-object as follows:
#### owner
The owner-role is granted to the subject which created the business object.
-E.g. for a new *customer* it would be granted to 'administrators' and for a new *package* to the 'customer#...admin'.
+E.g. for a new *customer* it would be granted to 'administrators' and for a new *package* to the 'customer#...:ADMIN'.
Whoever has the owner-role assigned can do everything with the related business-object, including deleting (or deactivating) it.
-In most cases, the permissions to other operations than 'delete' are granted through the 'admin' role.
+In most cases, the permissions to other operations than 'DELETE' are granted through the 'admin' role.
By this, all roles ob sub-objects, which are assigned to the 'admin' role, are also granted to the 'owner'.
-#### admin
+#### ADMIN
The admin-role is granted to a role of those subjects who manage the business object.
E.g. a 'package' is manged by the admin of the customer.
-Whoever has the admin-role assigned, can usually edit the related business-object but not deleting (or deactivating) it.
+Whoever has the admin-role assigned, can usually update the related business-object but not delete (or deactivating) it.
-The admin-role also comprises lesser roles, through which the view-permission is granted.
+The admin-role also comprises lesser roles, through which the SELECT-permission is granted.
-#### agent
+#### AGENT
The agent-role is not used in the examples of this document, because it's for more complex cases.
-It's usually granted to those roles and users who represent the related business-object, but are not allowed to edit it.
+It's usually granted to those roles and users who represent the related business-object, but are not allowed to update it.
Other than the tenant-role, it usually offers broader visibility of sub-business-objects (joined entities).
E.g. a package-admin is allowed to see the related debitor-business-object,
but not its banking data.
-#### tenant
+#### TENANT
-The tenant-role is granted to everybody who needs to be able to view the business-object and (probably some) related business-objects.
+The tenant-role is granted to everybody who needs to be able to select the business-object and (probably some) related business-objects.
Usually all owners, admins and tenants of sub-objects get this role granted.
-Some business-objects only have very limited data directly in the main business-object and store more sensitive data in special sub-objects (e.g. 'customer-details') to which tenants of sub-objects of the main-object (e.g. package admins) do not get view permission.
+Some business-objects only have very limited data directly in the main business-object and store more sensitive data in special sub-objects (e.g. 'customer-details') to which tenants of sub-objects of the main-object (e.g. package admins) do not get SELECT permission.
-#### guest
+#### GUEST
+
+(Deprecated)
+
+#### REFERRER
Like the agent-role, the guest-role too is not used in the examples of this document, because it's for more complex cases.
-If the guest-role exists, the view-permission is granted to it, instead of to the tenant-role.
-Other than the tenant-role, the guest-roles does never grant any roles of related objects.
+If the referrer-role exists, the SELECT-permission is granted to it, instead of to the tenant-role.
+Other than the tenant-role, the referrer-roles does never grant any roles of related objects.
-Also, if the guest-role exists, the tenant-role receives the view-permission through the guest-role.
+Also, if the referrer-role exists, the tenant-role receives the SELECT-permission through the referrer-role.
### Referenced Business Objects and Role-Depreciation
@@ -263,7 +265,7 @@ The admin-role of one object could be granted visibility to another object throu
But not in all cases role-depreciation takes place.
E.g. often a tenant-role is granted another tenant-role,
-because it should be again allowed to view sub-objects.
+because it should be again allowed to select sub-objects.
The same for the agent-role, often it is granted another agent-role.
@@ -297,14 +299,14 @@ package RbacRoles {
RbacUsers -[hidden]> RbacRoles
package RbacPermissions {
- object PermCustXyz_View
- object PermCustXyz_Edit
- object PermCustXyz_Delete
- object PermCustXyz_AddPackage
- object PermPackXyz00_View
- object PermPackXyz00_Edit
- object PermPackXyz00_Delete
- object PermPackXyz00_AddUser
+ object PermCustXyz_SELECT
+ object PermCustXyz_UPDATE
+ object PermCustXyz_DELETE
+ object PermCustXyz_INSERT:Package
+ object PermPackXyz00_SELECT
+ object PermPackXyz00_EDIT
+ object PermPackXyz00_DELETE
+ object PermPackXyz00_INSERT:USER
}
RbacRoles -[hidden]> RbacPermissions
@@ -322,23 +324,23 @@ RoleAdministrators o..> RoleCustXyz_Owner
RoleCustXyz_Owner o-> RoleCustXyz_Admin
RoleCustXyz_Admin o-> RolePackXyz00_Owner
-RoleCustXyz_Owner o--> PermCustXyz_Edit
-RoleCustXyz_Owner o--> PermCustXyz_Delete
-RoleCustXyz_Admin o--> PermCustXyz_View
-RoleCustXyz_Admin o--> PermCustXyz_AddPackage
-RolePackXyz00_Owner o--> PermPackXyz00_View
-RolePackXyz00_Owner o--> PermPackXyz00_Edit
-RolePackXyz00_Owner o--> PermPackXyz00_Delete
-RolePackXyz00_Owner o--> PermPackXyz00_AddUser
+RoleCustXyz_Owner o--> PermCustXyz_UPDATE
+RoleCustXyz_Owner o--> PermCustXyz_DELETE
+RoleCustXyz_Admin o--> PermCustXyz_SELECT
+RoleCustXyz_Admin o--> PermCustXyz_INSERT:Package
+RolePackXyz00_Owner o--> PermPackXyz00_SELECT
+RolePackXyz00_Owner o--> PermPackXyz00_UPDATE
+RolePackXyz00_Owner o--> PermPackXyz00_DELETE
+RolePackXyz00_Owner o--> PermPackXyz00_INSERT:User
-PermCustXyz_View o--> CustXyz
-PermCustXyz_Edit o--> CustXyz
-PermCustXyz_Delete o--> CustXyz
-PermCustXyz_AddPackage o--> CustXyz
-PermPackXyz00_View o--> PackXyz00
-PermPackXyz00_Edit o--> PackXyz00
-PermPackXyz00_Delete o--> PackXyz00
-PermPackXyz00_AddUser o--> PackXyz00
+PermCustXyz_SELECT o--> CustXyz
+PermCustXyz_UPDATE o--> CustXyz
+PermCustXyz_DELETE o--> CustXyz
+PermCustXyz_INSERT:Package o--> CustXyz
+PermPackXyz00_SELECT o--> PackXyz00
+PermPackXyz00_UPDATE o--> PackXyz00
+PermPackXyz00_DELETE o--> PackXyz00
+PermPackXyz00_INSERT:User o--> PackXyz00
@enduml
```
@@ -353,12 +355,12 @@ To support the RBAC system, for each business-object-table, some more artifacts
Not yet implemented, but planned are these actions:
-- an `ON DELETE ... DO INSTEAD` rule to allow `SQL DELETE` if applicable for the business-object-table and the user has 'delete' permission,
-- an `ON UPDATE ... DO INSTEAD` rule to allow `SQL UPDATE` if the user has 'edit' right,
-- an `ON INSERT ... DO INSTEAD` rule to allow `SQL INSERT` if the user has 'add-..' right to the parent-business-object.
+- an `ON DELETE ... DO INSTEAD` rule to allow `SQL DELETE` if applicable for the business-object-table and the user has 'DELETE' permission,
+- an `ON UPDATE ... DO INSTEAD` rule to allow `SQL UPDATE` if the user has 'UPDATE' right,
+- an `ON INSERT ... DO INSTEAD` rule to allow `SQL INSERT` if the user has the 'INSERT' right for the parent-business-object.
The restricted view takes the current user from a session property and applies the hierarchy of its roles all the way down to the permissions related to the respective business-object-table.
-This way, each user can only view the data they have 'view'-permission for, only create those they have 'add-...'-permission, only update those they have 'edit'- and only delete those they have 'delete'-permission to.
+This way, each user can only select the data they have 'SELECT'-permission for, only create those they have 'add-...'-permission, only update those they have 'UPDATE'- and only delete those they have 'DELETE'-permission to.
### Current User
@@ -374,7 +376,7 @@ That user is also used for historicization and audit log, but which is a differe
If the session variable `hsadminng.assumedRoles` is set to a non-empty value, its content is interpreted as a list of semicolon-separated role names.
Example:
- SET LOCAL hsadminng.assumedRoles = 'customer#aab.admin;customer#aac.admin';
+ SET LOCAL hsadminng.assumedRoles = 'customer#aab:admin;customer#aac:admin';
In this case, not the current user but the assumed roles are used as a starting point for any further queries.
Roles which are not granted to the current user, directly or indirectly, cannot be assumed.
@@ -387,7 +389,7 @@ A full example is shown here:
BEGIN TRANSACTION;
SET SESSION SESSION AUTHORIZATION restricted;
SET LOCAL hsadminng.currentUser = 'mike@hostsharing.net';
- SET LOCAL hsadminng.assumedRoles = 'customer#aab.admin;customer#aac.admin';
+ SET LOCAL hsadminng.assumedRoles = 'customer#aab:admin;customer#aac:admin';
SELECT c.prefix, p.name as "package", ema.localPart || '@' || dom.name as "email-address"
FROM emailaddress_rv ema
@@ -458,26 +460,26 @@ allow_mixing
entity "BObj customer#xyz" as boCustXyz
together {
- entity "Perm customer#xyz *" as permCustomerXyzAll
- permCustomerXyzAll --> boCustXyz
+ entity "Perm customer#xyz *" as permCustomerXyzDELETE
+ permCustomerXyzDELETE --> boCustXyz
- entity "Perm customer#xyz add-package" as permCustomerXyzAddPack
- permCustomerXyzAddPack --> boCustXyz
+ entity "Perm customer#xyz INSERT:package" as permCustomerXyzINSERT:package
+ permCustomerXyzINSERT:package --> boCustXyz
- entity "Perm customer#xyz view" as permCustomerXyzView
- permCustomerXyzView --> boCustXyz
+ entity "Perm customer#xyz SELECT" as permCustomerXyzSELECT
+ permCustomerXyzSELECT--> boCustXyz
}
-entity "Role customer#xyz.tenant" as roleCustXyzTenant
-roleCustXyzTenant --> permCustomerXyzView
+entity "Role customer#xyz:TENANT" as roleCustXyzTenant
+roleCustXyzTenant --> permCustomerXyzSELECT
-entity "Role customer#xyz.admin" as roleCustXyzAdmin
+entity "Role customer#xyz:ADMIN" as roleCustXyzAdmin
roleCustXyzAdmin --> roleCustXyzTenant
-roleCustXyzAdmin --> permCustomerXyzAddPack
+roleCustXyzAdmin --> permCustomerXyzINSERT:package
-entity "Role customer#xyz.owner" as roleCustXyzOwner
+entity "Role customer#xyz:OWNER" as roleCustXyzOwner
roleCustXyzOwner ..> roleCustXyzAdmin
-roleCustXyzOwner --> permCustomerXyzAll
+roleCustXyzOwner --> permCustomerXyzDELETE
actor "Customer XYZ Admin" as actorCustXyzAdmin
actorCustXyzAdmin --> roleCustXyzAdmin
@@ -487,13 +489,11 @@ roleAdmins --> roleCustXyzOwner
actor "Any Hostmaster" as actorHostmaster
actorHostmaster --> roleAdmins
-
-
@enduml
```
As you can see, there something special:
-From the 'Role customer#xyz.owner' to the 'Role customer#xyz.admin' there is a dashed line, whereas all other lines are solid lines.
+From the 'Role customer#xyz:OWNER' to the 'Role customer#xyz:admin' there is a dashed line, whereas all other lines are solid lines.
Solid lines means, that one role is granted to another and automatically assumed in all queries to the restricted views.
The dashed line means that one role is granted to another but not automatically assumed in queries to the restricted views.
@@ -527,36 +527,36 @@ allow_mixing
entity "BObj package#xyz00" as boPacXyz00
together {
- entity "Perm package#xyz00 *" as permPackageXyzAll
- permPackageXyzAll --> boPacXyz00
+ entity "Perm package#xyz00 *" as permPackageXyzDELETE
+ permPackageXyzDELETE --> boPacXyz00
- entity "Perm package#xyz00 add-domain" as permPacXyz00AddUser
- permPacXyz00AddUser --> boPacXyz00
+ entity "Perm package#xyz00 INSERT:domain" as permPacXyz00INSERT:user
+ permPacXyz00INSERT:user --> boPacXyz00
- entity "Perm package#xyz00 edit" as permPacXyz00Edit
- permPacXyz00Edit --> boPacXyz00
+ entity "Perm package#xyz00 UPDATE" as permPacXyz00UPDATE
+ permPacXyz00UPDATE --> boPacXyz00
- entity "Perm package#xyz00 view" as permPacXyz00View
- permPacXyz00View --> boPacXyz00
+ entity "Perm package#xyz00 SELECT" as permPacXyz00SELECT
+ permPacXyz00SELECT --> boPacXyz00
}
package {
- entity "Role customer#xyz.tenant" as roleCustXyzTenant
- entity "Role customer#xyz.admin" as roleCustXyzAdmin
- entity "Role customer#xyz.owner" as roleCustXyzOwner
+ entity "Role customer#xyz:TENANT" as roleCustXyzTenant
+ entity "Role customer#xyz:ADMIN" as roleCustXyzAdmin
+ entity "Role customer#xyz:OWNER" as roleCustXyzOwner
}
package {
- entity "Role package#xyz00.owner" as rolePacXyz00Owner
- entity "Role package#xyz00.admin" as rolePacXyz00Admin
- entity "Role package#xyz00.tenant" as rolePacXyz00Tenant
+ entity "Role package#xyz00:OWNER" as rolePacXyz00Owner
+ entity "Role package#xyz00:ADMIN" as rolePacXyz00Admin
+ entity "Role package#xyz00:TENANT" as rolePacXyz00Tenant
}
-rolePacXyz00Tenant --> permPacXyz00View
+rolePacXyz00Tenant --> permPacXyz00SELECT
rolePacXyz00Tenant --> roleCustXyzTenant
rolePacXyz00Owner --> rolePacXyz00Admin
-rolePacXyz00Owner --> permPackageXyzAll
+rolePacXyz00Owner --> permPackageXyzDELETE
roleCustXyzAdmin --> rolePacXyz00Owner
roleCustXyzAdmin --> roleCustXyzTenant
@@ -564,8 +564,8 @@ roleCustXyzAdmin --> roleCustXyzTenant
roleCustXyzOwner ..> roleCustXyzAdmin
rolePacXyz00Admin --> rolePacXyz00Tenant
-rolePacXyz00Admin --> permPacXyz00AddUser
-rolePacXyz00Admin --> permPacXyz00Edit
+rolePacXyz00Admin --> permPacXyz00INSERT:user
+rolePacXyz00Admin --> permPacXyz00UPDATE
actor "Package XYZ00 Admin" as actorPacXyzAdmin
actorPacXyzAdmin -l-> rolePacXyz00Admin
@@ -624,10 +624,10 @@ Let's have a look at the two view queries:
WHERE target.uuid IN (
SELECT uuid
FROM queryAccessibleObjectUuidsOfSubjectIds(
- 'view', 'customer', currentSubjectsUuids()));
+ 'SELECT, 'customer', currentSubjectsUuids()));
This view should be automatically updatable.
-Where, for updates, we actually have to check for 'edit' instead of 'view' operation, which makes it a bit more complicated.
+Where, for updates, we actually have to check for 'UPDATE' instead of 'SELECT' operation, which makes it a bit more complicated.
With the larger dataset, the test suite initially needed over 7 seconds with this view query.
At this point the second variant was tried.
@@ -642,7 +642,7 @@ Looks like the query optimizer needed some statistics to find the best path.
SELECT DISTINCT target.*
FROM customer AS target
JOIN queryAccessibleObjectUuidsOfSubjectIds(
- 'view', 'customer', currentSubjectsUuids()) AS allowedObjId
+ 'SELECT, 'customer', currentSubjectsUuids()) AS allowedObjId
ON target.uuid = allowedObjId;
This view cannot is not updatable automatically,
@@ -688,13 +688,13 @@ Otherwise, it would not be possible to assign roles to new users.
All roles are system-defined and cannot be created or modified by any external API.
-Users can view only the roles to which they are assigned.
+Users can view only the roles to which are granted to them.
## RbacGrant
Grant can be `empowered`, this means that the grantee user can grant the granted role to other users
and revoke grants to that role.
-(TODO: access control part not yet implemented)
+(TODO: access control part not yet implemented, currently all accessible roles can be granted to other users)
Grants can be `managed`, which means they are created and deleted by system-defined rules.
If a grant is not managed, it was created by an empowered user and can be deleted by empowered users.
diff --git a/doc/test-concept.md b/doc/test-concept.md
index c8946342..690d1558 100644
--- a/doc/test-concept.md
+++ b/doc/test-concept.md
@@ -87,7 +87,7 @@ Acceptance-Tests run on a fully integrated and deployed system with deployed dou
Acceptance-tests, are blackbox-tests and do not count into test-code-coverage.
-TODO: Complete the Acceptance-Tests test concept.
+TODO.test: Complete the Acceptance-Tests test concept.
#### Performance-Tests
@@ -107,4 +107,4 @@ We define System-Integration-Tests as test in which this system is deployed in a
System-Integration-tests, are blackbox-tests and do not count into test-code-coverage.
-TODO: Complete the System-Integration-Tests test concept.
+TODO.test: Complete the System-Integration-Tests test concept.
diff --git a/etc/owasp-dependency-check-suppression.xml b/etc/owasp-dependency-check-suppression.xml
index 39d77b47..af4269d4 100644
--- a/etc/owasp-dependency-check-suppression.xml
+++ b/etc/owasp-dependency-check-suppression.xml
@@ -1,33 +1,5 @@
-
-
- ^pkg:maven/org\.springframework/spring-web@.*$
- CVE-2016-1000027
-
-
-
- ^pkg:maven/com\.fasterxml\.jackson\.core/jackson\-databind@.*$
- CVE-2022-42003
-
-
-
- ^pkg:maven/org\.eclipse\.angus/angus\-activation@.*$
- cpe:/a:eclipse:eclipse_ide
-
-
-
- ^pkg:maven/jakarta\.activation/jakarta\.activation\-api@.*$
- cpe:/a:eclipse:eclipse_ide
- ^pkg:maven/com\.fasterxml\.jackson\.core/jackson\-databind@.*$
cpe:/a:fasterxml:jackson-databind
-
-
- ^pkg:maven/com\.jayway\.jsonpath/json\-path@.*$
- CVE-2023-51074
- ^pkg:maven/org\.pitest/pitest\-command\-line@.*$
cpe:/a:line:line
-
-
- ^pkg:maven/org\.yaml/snakeyaml@.*$
- CVE-2022-1471
-
diff --git a/settings.gradle b/settings.gradle
index 09d09d6f..d6f3f9eb 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -11,28 +11,4 @@ plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.7.0'
}
-dependencyResolutionManagement {
- components {
- all {
- allVariants {
- withDependencies {
- removeAll {
- // Spring Boot 3.1.x has a transient dependency to snakeyaml 1.3
- // which contains a severe vulnerability.
- // Here we remove this transient dependency and in build.gradle
- // we add an explicit dependency to snakeyaml 2.2,
- // which does not have this vulnerability anymore.
- //
- // TODO: Check Once we are on SpringBoot 3.2.x, check if this exclude
- // is still neccessary. If not:
- // Remove it // as well as the related explicit dependency in build.gradle
- // and the dependency suppression in owasp-dependency-check-suppression.xml.
- it.module in [ 'snakeyaml' ]
- }
- }
- }
- }
- }
-}
-
rootProject.name = 'hsadmin-ng'
diff --git a/sql/historization.sql b/sql/historization.sql
index 2f4087b4..1bd0db44 100644
--- a/sql/historization.sql
+++ b/sql/historization.sql
@@ -18,8 +18,8 @@ CREATE OR REPLACE FUNCTION historicize()
RETURNS trigger
LANGUAGE plpgsql STRICT AS $$
DECLARE
-currentUser VARCHAR(64);
- currentTask varchar;
+ currentUser VARCHAR(63);
+ currentTask VARCHAR(127);
"row" RECORD;
"alive" BOOLEAN;
"sql" varchar;
@@ -37,27 +37,27 @@ END IF;
-- determine task
currentTask = current_setting('hsadminng.currentTask');
- IF (currentTask IS NULL OR length(currentTask) < 12) THEN
- RAISE EXCEPTION 'hsadminng.currentTask (%) must be defined and min 12 characters long, please use "SET LOCAL ...;"', currentTask;
-END IF;
- RAISE NOTICE 'currentTask: %', currentTask;
+ assert currentTask IS NOT NULL AND length(currentTask) >= 12,
+ format('hsadminng.currentTask (%s) must be defined and min 12 characters long, please use "SET LOCAL ...;"', currentTask);
+ assert length(currentTask) <= 127,
+ format('hsadminng.currentTask (%s) must not be longer than 127 characters"', currentTask);
IF (TG_OP = 'INSERT') OR (TG_OP = 'UPDATE') THEN
"row" := NEW;
"alive" := TRUE;
-ELSE -- DELETE or TRUNCATE
- "row" := OLD;
- "alive" := FALSE;
-END IF;
+ ELSE -- DELETE or TRUNCATE
+ "row" := OLD;
+ "alive" := FALSE;
+ END IF;
-sql := format('INSERT INTO tx_history VALUES (txid_current(), now(), %1L, %2L) ON CONFLICT DO NOTHING', currentUser, currentTask);
+ sql := format('INSERT INTO tx_history VALUES (txid_current(), now(), %1L, %2L) ON CONFLICT DO NOTHING', currentUser, currentTask);
RAISE NOTICE 'sql: %', sql;
-EXECUTE sql;
-sql := format('INSERT INTO %3$I_versions VALUES (DEFAULT, txid_current(), %1$L, %2$L, $1.*)', TG_OP, alive, TG_TABLE_NAME);
- RAISE NOTICE 'sql: %', sql;
-EXECUTE sql USING "row";
+ EXECUTE sql;
+ sql := format('INSERT INTO %3$I_versions VALUES (DEFAULT, txid_current(), %1$L, %2$L, $1.*)', TG_OP, alive, TG_TABLE_NAME);
+ RAISE NOTICE 'sql: %', sql;
+ EXECUTE sql USING "row";
-RETURN "row";
+ RETURN "row";
END; $$;
CREATE OR REPLACE PROCEDURE create_historical_view(baseTable varchar)
diff --git a/sql/rbac-tests.sql b/sql/rbac-tests.sql
index 4e179dee..351d1509 100644
--- a/sql/rbac-tests.sql
+++ b/sql/rbac-tests.sql
@@ -3,10 +3,10 @@
-- --------------------------------------------------------
-select isGranted(findRoleId('administrators'), findRoleId('test_package#aaa00.owner'));
-select isGranted(findRoleId('test_package#aaa00.owner'), findRoleId('administrators'));
--- call grantRoleToRole(findRoleId('test_package#aaa00.owner'), findRoleId('administrators'));
--- call grantRoleToRole(findRoleId('administrators'), findRoleId('test_package#aaa00.owner'));
+select isGranted(findRoleId('administrators'), findRoleId('test_package#aaa00:OWNER'));
+select isGranted(findRoleId('test_package#aaa00:OWNER'), findRoleId('administrators'));
+-- call grantRoleToRole(findRoleId('test_package#aaa00:OWNER'), findRoleId('administrators'));
+-- call grantRoleToRole(findRoleId('administrators'), findRoleId('test_package#aaa00:OWNER'));
select count(*)
FROM queryAllPermissionsOfSubjectIdForObjectUuids(findRbacUser('superuser-fran@hostsharing.net'),
@@ -25,7 +25,7 @@ FROM queryAllRbacUsersWithPermissionsFor(findEffectivePermissionId('customer',
select *
FROM queryAllRbacUsersWithPermissionsFor(findEffectivePermissionId('package',
(SELECT uuid FROM RbacObject WHERE objectTable = 'package' LIMIT 1),
- 'delete'));
+ 'DELETE'));
DO LANGUAGE plpgsql
$$
@@ -34,12 +34,12 @@ $$
result bool;
BEGIN
userId = findRbacUser('superuser-alex@hostsharing.net');
- result = (SELECT * FROM isPermissionGrantedToSubject(findEffectivePermissionId('package', 94928, 'add-package'), userId));
+ result = (SELECT * FROM isPermissionGrantedToSubject(findPermissionId('package', 94928, 'add-package'), userId));
IF (result) THEN
RAISE EXCEPTION 'expected permission NOT to be granted, but it is';
end if;
- result = (SELECT * FROM isPermissionGrantedToSubject(findEffectivePermissionId('package', 94928, 'view'), userId));
+ result = (SELECT * FROM isPermissionGrantedToSubject(findPermissionId('package', 94928, 'SELECT'), userId));
IF (NOT result) THEN
RAISE EXCEPTION 'expected permission to be granted, but it is NOT';
end if;
diff --git a/sql/rbac-view-option-experiments.sql b/sql/rbac-view-option-experiments.sql
index d3ef736a..c5c04487 100644
--- a/sql/rbac-view-option-experiments.sql
+++ b/sql/rbac-view-option-experiments.sql
@@ -20,7 +20,7 @@ CREATE POLICY customer_policy ON customer
TO restricted
USING (
-- id=1000
- isPermissionGrantedToSubject(findEffectivePermissionId('test_customer', id, 'view'), currentUserUuid())
+ isPermissionGrantedToSubject(findEffectivePermissionId('test_customer', id, 'SELECT'), currentUserUuid())
);
SET SESSION AUTHORIZATION restricted;
@@ -35,7 +35,7 @@ SELECT * FROM customer;
CREATE OR REPLACE RULE "_RETURN" AS
ON SELECT TO cust_view
DO INSTEAD
- SELECT * FROM customer WHERE isPermissionGrantedToSubject(findEffectivePermissionId('test_customer', id, 'view'), currentUserUuid());
+ SELECT * FROM customer WHERE isPermissionGrantedToSubject(findEffectivePermissionId('test_customer', id, 'SELECT'), currentUserUuid());
SELECT * from cust_view LIMIT 10;
select queryAllPermissionsOfSubjectId(findRbacUser('superuser-alex@hostsharing.net'));
@@ -52,7 +52,7 @@ CREATE OR REPLACE RULE "_RETURN" AS
DO INSTEAD
SELECT c.uuid, c.reference, c.prefix FROM customer AS c
JOIN queryAllPermissionsOfSubjectId(currentUserUuid()) AS p
- ON p.objectTable='test_customer' AND p.objectUuid=c.uuid AND p.op in ('*', 'view');
+ ON p.objectTable='test_customer' AND p.objectUuid=c.uuid;
GRANT ALL PRIVILEGES ON cust_view TO restricted;
SET SESSION SESSION AUTHORIZATION restricted;
@@ -68,7 +68,7 @@ CREATE OR REPLACE VIEW cust_view AS
SELECT c.uuid, c.reference, c.prefix
FROM customer AS c
JOIN queryAllPermissionsOfSubjectId(currentUserUuid()) AS p
- ON p.objectUuid=c.uuid AND p.op in ('*', 'view');
+ ON p.objectUuid=c.uuid;
GRANT ALL PRIVILEGES ON cust_view TO restricted;
SET SESSION SESSION AUTHORIZATION restricted;
@@ -81,9 +81,9 @@ select rr.uuid, rr.type from RbacGrants g
join RbacReference RR on g.ascendantUuid = RR.uuid
where g.descendantUuid in (
select uuid from queryAllPermissionsOfSubjectId(findRbacUser('alex@example.com'))
- where objectTable='test_customer' and op in ('*', 'view'));
+ where objectTable='test_customer');
-call grantRoleToUser(findRoleId('test_customer#aaa.admin'), findRbacUser('aaaaouq@example.com'));
+call grantRoleToUser(findRoleId('test_customer#aaa:ADMIN'), findRbacUser('aaaaouq@example.com'));
select queryAllPermissionsOfSubjectId(findRbacUser('aaaaouq@example.com'));
diff --git a/src/main/java/net/hostsharing/hsadminng/context/Context.java b/src/main/java/net/hostsharing/hsadminng/context/Context.java
index 2730147d..b3dac96b 100644
--- a/src/main/java/net/hostsharing/hsadminng/context/Context.java
+++ b/src/main/java/net/hostsharing/hsadminng/context/Context.java
@@ -15,11 +15,9 @@ import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
-import java.util.function.Function;
import java.util.stream.Collectors;
import static java.util.function.Predicate.not;
-import static net.hostsharing.hsadminng.mapper.PostgresArray.fromPostgresArray;
import static org.springframework.transaction.annotation.Propagation.MANDATORY;
@Service
@@ -55,16 +53,15 @@ public class Context {
final String currentRequest,
final String currentUser,
final String assumedRoles) {
- final var query = em.createNativeQuery(
- """
- call defineContext(
- cast(:currentTask as varchar),
- cast(:currentRequest as varchar),
- cast(:currentUser as varchar),
- cast(:assumedRoles as varchar));
- """);
- query.setParameter("currentTask", shortenToMaxLength(currentTask, 96));
- query.setParameter("currentRequest", shortenToMaxLength(currentRequest, 512)); // TODO.spec: length?
+ final var query = em.createNativeQuery("""
+ call defineContext(
+ cast(:currentTask as varchar(127)),
+ cast(:currentRequest as text),
+ cast(:currentUser as varchar(63)),
+ cast(:assumedRoles as varchar(1023)));
+ """);
+ query.setParameter("currentTask", shortenToMaxLength(currentTask, 127));
+ query.setParameter("currentRequest", currentRequest);
query.setParameter("currentUser", currentUser);
query.setParameter("assumedRoles", assumedRoles != null ? assumedRoles : "");
query.executeUpdate();
@@ -83,14 +80,11 @@ public class Context {
}
public String[] getAssumedRoles() {
- final byte[] result = (byte[]) em.createNativeQuery("select assumedRoles() as roles", String[].class).getSingleResult();
- return fromPostgresArray(result, String.class, Function.identity());
+ return (String[]) em.createNativeQuery("select assumedRoles() as roles", String[].class).getSingleResult();
}
public UUID[] currentSubjectsUuids() {
- final byte[] result = (byte[]) em.createNativeQuery("select currentSubjectsUuids() as uuids", UUID[].class)
- .getSingleResult();
- return fromPostgresArray(result, UUID.class, UUID::fromString);
+ return (UUID[]) em.createNativeQuery("select currentSubjectsUuids() as uuids", UUID[].class).getSingleResult();
}
public static String getCallerMethodNameFromStackFrame(final int skipFrames) {
diff --git a/src/main/java/net/hostsharing/hsadminng/errors/ReferenceNotFoundException.java b/src/main/java/net/hostsharing/hsadminng/errors/ReferenceNotFoundException.java
index e20d1357..deeae9f8 100644
--- a/src/main/java/net/hostsharing/hsadminng/errors/ReferenceNotFoundException.java
+++ b/src/main/java/net/hostsharing/hsadminng/errors/ReferenceNotFoundException.java
@@ -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 ReferenceNotFoundException(final Class entityClass, final UUID uuid, final Throwable exc) {
+ public ReferenceNotFoundException(final Class entityClass, final UUID uuid, final Throwable exc) {
super(exc);
this.entityClass = entityClass;
this.uuid = uuid;
diff --git a/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java b/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java
index 6c36dfb8..5d675484 100644
--- a/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java
+++ b/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java
@@ -11,16 +11,18 @@ import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.lang.Nullable;
import org.springframework.orm.jpa.JpaObjectRetrievalFailureException;
import org.springframework.orm.jpa.JpaSystemException;
+import org.springframework.validation.FieldError;
+import org.springframework.validation.method.ParameterValidationResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
+import org.springframework.web.method.annotation.HandlerMethodValidationException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import jakarta.persistence.EntityNotFoundException;
import jakarta.validation.ValidationException;
-import java.util.NoSuchElementException;
-import java.util.Optional;
+import java.util.*;
import java.util.regex.Pattern;
import static net.hostsharing.hsadminng.errors.CustomErrorResponse.*;
@@ -119,6 +121,28 @@ public class RestResponseEntityExceptionHandler
return errorResponse(request, HttpStatus.BAD_REQUEST, errorList.toString());
}
+ @SuppressWarnings("unchecked,rawtypes")
+
+ @Override
+ protected ResponseEntity handleHandlerMethodValidationException(
+ final HandlerMethodValidationException exc,
+ final HttpHeaders headers,
+ final HttpStatusCode status,
+ final WebRequest request) {
+ final var errorList = exc
+ .getAllValidationResults()
+ .stream()
+ .map(ParameterValidationResult::getResolvableErrors)
+ .flatMap(Collection::stream)
+ .filter(FieldError.class::isInstance)
+ .map(FieldError.class::cast)
+ .map(fieldError -> fieldError.getField() + " " + fieldError.getDefaultMessage() + " but is \""
+ + fieldError.getRejectedValue() + "\"")
+ .toList();
+ return errorResponse(request, HttpStatus.BAD_REQUEST, errorList.toString());
+ }
+
+
private String userReadableEntityClassName(final String exceptionMessage) {
final var regex = "(net.hostsharing.hsadminng.[a-z0-9_.]*.[A-Za-z0-9_$]*Entity) ";
final var pattern = Pattern.compile(regex);
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountEntity.java
index 4d067f68..6542084e 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountEntity.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountEntity.java
@@ -3,7 +3,8 @@ package net.hostsharing.hsadminng.hs.office.bankaccount;
import lombok.*;
import lombok.experimental.FieldNameConstants;
import net.hostsharing.hsadminng.errors.DisplayName;
-import net.hostsharing.hsadminng.persistence.HasUuid;
+import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
+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
@@ -24,11 +30,11 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@AllArgsConstructor
@FieldNameConstants
@DisplayName("BankAccount")
-public class HsOfficeBankAccountEntity implements HasUuid, Stringifyable {
+public class HsOfficeBankAccountEntity implements RbacObject, Stringifyable {
private static Stringify toString = stringify(HsOfficeBankAccountEntity.class, "bankAccount")
+ .withIdProp(HsOfficeBankAccountEntity::getIban)
.withProp(Fields.holder, HsOfficeBankAccountEntity::getHolder)
- .withProp(Fields.iban, HsOfficeBankAccountEntity::getIban)
.withProp(Fields.bic, HsOfficeBankAccountEntity::getBic);
@Id
@@ -50,4 +56,28 @@ public class HsOfficeBankAccountEntity implements HasUuid, Stringifyable {
public String toShortString() {
return holder;
}
+
+ public static RbacView rbac() {
+ return rbacViewFor("bankAccount", HsOfficeBankAccountEntity.class)
+ .withIdentityView(SQL.projection("iban"))
+ .withUpdatableColumns("holder", "iban", "bic")
+
+ .toRole("global", GUEST).grantPermission(INSERT)
+
+ .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("5-hs-office/505-bankaccount/5053-hs-office-bankaccount-rbac");
+ }
}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntity.java
index 69555dc4..1ce3a557 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntity.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntity.java
@@ -3,14 +3,22 @@ package net.hostsharing.hsadminng.hs.office.contact;
import lombok.*;
import lombok.experimental.FieldNameConstants;
import net.hostsharing.hsadminng.errors.DisplayName;
-import net.hostsharing.hsadminng.persistence.HasUuid;
+import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
+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
@@ -22,13 +30,12 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@AllArgsConstructor
@FieldNameConstants
@DisplayName("Contact")
-public class HsOfficeContactEntity implements Stringifyable, HasUuid {
+public class HsOfficeContactEntity implements Stringifyable, RbacObject {
private static Stringify toString = stringify(HsOfficeContactEntity.class, "contact")
.withProp(Fields.label, HsOfficeContactEntity::getLabel)
.withProp(Fields.emailAddresses, HsOfficeContactEntity::getEmailAddresses);
-
@Id
@GeneratedValue(generator = "UUID")
@GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
@@ -36,13 +43,13 @@ public class HsOfficeContactEntity implements Stringifyable, HasUuid {
private String label;
@Column(name = "postaladdress")
- private String postalAddress; // TODO: check if we really want multiple, if so: JSON-Array or Postgres-Array?
+ private String postalAddress; // TODO.spec: check if we really want multiple, if so: JSON-Array or Postgres-Array?
@Column(name = "emailaddresses", columnDefinition = "json")
- private String emailAddresses; // TODO: check if we can really add multiple. format: ["eins@...", "zwei@..."]
+ private String emailAddresses; // TODO.spec: check if we can really add multiple. format: ["eins@...", "zwei@..."]
@Column(name = "phonenumbers", columnDefinition = "json")
- private String phoneNumbers; // TODO: check if we can really add multiple. format: { "office": "+49 40 12345-10", "fax": "+49 40 12345-05" }
+ private String phoneNumbers; // TODO.spec: check if we can really add multiple. format: { "office": "+49 40 12345-10", "fax": "+49 40 12345-05" }
@Override
public String toString() {
@@ -53,4 +60,26 @@ 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);
+ })
+ .toRole(GLOBAL, GUEST).grantPermission(INSERT);
+ }
+
+ public static void main(String[] args) throws IOException {
+ rbac().generateWithBaseFileName("5-hs-office/501-contact/5013-hs-office-contact-rbac");
+ }
}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java
index 946b4626..add8333c 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java
@@ -13,7 +13,6 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
-import jakarta.validation.Valid;
import jakarta.validation.ValidationException;
import java.time.LocalDate;
import java.util.ArrayList;
@@ -59,7 +58,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
public ResponseEntity addCoopAssetsTransaction(
final String currentUser,
final String assumedRoles,
- @Valid final HsOfficeCoopAssetsTransactionInsertResource requestBody) {
+ final HsOfficeCoopAssetsTransactionInsertResource requestBody) {
context.define(currentUser, assumedRoles);
validate(requestBody);
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntity.java
index 2c6fdb1b..47fd03a6 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntity.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntity.java
@@ -1,20 +1,47 @@
package net.hostsharing.hsadminng.hs.office.coopassets;
-import lombok.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
-import net.hostsharing.hsadminng.persistence.HasUuid;
+import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
+import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
+import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.GenericGenerator;
-import jakarta.persistence.*;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+import java.io.IOException;
+import java.io.IOException;
+import java.io.IOException;
import java.math.BigDecimal;
import java.time.LocalDate;
+import java.util.Optional;
import java.util.UUID;
import static java.util.Optional.ofNullable;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.SELECT;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.UPDATE;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.ADMIN;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.AGENT;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.directlyFetchedByDependsOnColumn;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity
@@ -25,16 +52,15 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor
@AllArgsConstructor
@DisplayName("CoopAssetsTransaction")
-public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, HasUuid {
+public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, RbacObject {
private static Stringify stringify = stringify(HsOfficeCoopAssetsTransactionEntity.class)
- .withProp(HsOfficeCoopAssetsTransactionEntity::getMemberNumber)
+ .withIdProp(HsOfficeCoopAssetsTransactionEntity::getTaggedMemberNumber)
.withProp(HsOfficeCoopAssetsTransactionEntity::getValueDate)
.withProp(HsOfficeCoopAssetsTransactionEntity::getTransactionType)
.withProp(HsOfficeCoopAssetsTransactionEntity::getAssetValue)
.withProp(HsOfficeCoopAssetsTransactionEntity::getReference)
.withProp(HsOfficeCoopAssetsTransactionEntity::getComment)
- .withSeparator(", ")
.quotedValues(false);
@Id
@@ -76,8 +102,8 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, HasUu
private String comment;
- public Integer getMemberNumber() {
- return ofNullable(membership).map(HsOfficeMembershipEntity::getMemberNumber).orElse(null);
+ public String getTaggedMemberNumber() {
+ return ofNullable(membership).map(HsOfficeMembershipEntity::toShortString).orElse("M-?????");
}
@Override
@@ -87,6 +113,24 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, HasUu
@Override
public String toShortString() {
- return "%s%+1.2f".formatted(getMemberNumber(), assetValue);
+ return "%s:%+1.2f".formatted(getTaggedMemberNumber(), Optional.ofNullable(assetValue).orElse(BigDecimal.ZERO));
+ }
+
+ public static RbacView rbac() {
+ return rbacViewFor("coopAssetsTransaction", HsOfficeCoopAssetsTransactionEntity.class)
+ .withIdentityView(RbacView.SQL.projection("reference"))
+ .withUpdatableColumns("comment")
+ .importEntityAlias("membership", HsOfficeMembershipEntity.class,
+ dependsOnColumn("membershipUuid"),
+ directlyFetchedByDependsOnColumn(),
+ NOT_NULL)
+
+ .toRole("membership", ADMIN).grantPermission(INSERT)
+ .toRole("membership", ADMIN).grantPermission(UPDATE)
+ .toRole("membership", AGENT).grantPermission(SELECT);
+ }
+
+ public static void main(String[] args) throws IOException {
+ rbac().generateWithBaseFileName("5-hs-office/512-coopassets/5123-hs-office-coopassets-rbac");
}
}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java
index 813d8b92..39dc9002 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java
@@ -13,7 +13,6 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
-import jakarta.validation.Valid;
import jakarta.validation.ValidationException;
import java.time.LocalDate;
import java.util.ArrayList;
@@ -60,7 +59,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
public ResponseEntity addCoopSharesTransaction(
final String currentUser,
final String assumedRoles,
- @Valid final HsOfficeCoopSharesTransactionInsertResource requestBody) {
+ final HsOfficeCoopSharesTransactionInsertResource requestBody) {
context.define(currentUser, assumedRoles);
validate(requestBody);
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntity.java
index c7ba9527..8ab19435 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntity.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntity.java
@@ -1,17 +1,41 @@
package net.hostsharing.hsadminng.hs.office.coopshares;
-import lombok.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
-import net.hostsharing.hsadminng.persistence.HasUuid;
+import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
+import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
+import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
-import jakarta.persistence.*;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+import java.io.IOException;
import java.time.LocalDate;
import java.util.UUID;
import static java.util.Optional.ofNullable;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.SELECT;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.UPDATE;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.ADMIN;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.AGENT;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.directlyFetchedByDependsOnColumn;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity
@@ -22,7 +46,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor
@AllArgsConstructor
@DisplayName("CoopShareTransaction")
-public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, HasUuid {
+public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, RbacObject {
private static Stringify stringify = stringify(HsOfficeCoopSharesTransactionEntity.class)
.withProp(HsOfficeCoopSharesTransactionEntity::getMemberNumberTagged)
@@ -31,7 +55,6 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, HasUu
.withProp(HsOfficeCoopSharesTransactionEntity::getShareCount)
.withProp(HsOfficeCoopSharesTransactionEntity::getReference)
.withProp(HsOfficeCoopSharesTransactionEntity::getComment)
- .withSeparator(", ")
.quotedValues(false);
@Id
@@ -84,4 +107,22 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, HasUu
public String toShortString() {
return "%s%+d".formatted(getMemberNumberTagged(), shareCount);
}
+
+ public static RbacView rbac() {
+ return rbacViewFor("coopSharesTransaction", HsOfficeCoopSharesTransactionEntity.class)
+ .withIdentityView(SQL.projection("reference"))
+ .withUpdatableColumns("comment")
+ .importEntityAlias("membership", HsOfficeMembershipEntity.class,
+ dependsOnColumn("membershipUuid"),
+ directlyFetchedByDependsOnColumn(),
+ NOT_NULL)
+
+ .toRole("membership", ADMIN).grantPermission(INSERT)
+ .toRole("membership", ADMIN).grantPermission(UPDATE)
+ .toRole("membership", AGENT).grantPermission(SELECT);
+ }
+
+ public static void main(String[] args) throws IOException {
+ rbac().generateWithBaseFileName("5-hs-office/511-coopshares/5113-hs-office-coopshares-rbac");
+ }
}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorController.java
index bc4175ca..5455b99b 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorController.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorController.java
@@ -5,7 +5,11 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeDebitors
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorPatchResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorResource;
+import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity;
+import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRepository;
import net.hostsharing.hsadminng.mapper.Mapper;
+import org.apache.commons.lang3.Validate;
+import org.hibernate.Hibernate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
@@ -13,10 +17,13 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import jakarta.persistence.EntityManager;
+import jakarta.persistence.EntityNotFoundException;
import jakarta.persistence.PersistenceContext;
import java.util.List;
import java.util.UUID;
+import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR;
+
@RestController
public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
@@ -30,6 +37,9 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
@Autowired
private HsOfficeDebitorRepository debitorRepo;
+ @Autowired
+ private HsOfficeRelationRepository relRepo;
+
@PersistenceContext
private EntityManager em;
@@ -53,22 +63,44 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
@Override
@Transactional
public ResponseEntity addDebitor(
- final String currentUser,
- final String assumedRoles,
- final HsOfficeDebitorInsertResource body) {
+ String currentUser,
+ String assumedRoles,
+ HsOfficeDebitorInsertResource body) {
context.define(currentUser, assumedRoles);
- final var entityToSave = mapper.map(body, HsOfficeDebitorEntity.class);
+ Validate.isTrue(body.getDebitorRel() == null || body.getDebitorRelUuid() == null,
+ "ERROR: [400] exactly one of debitorRel and debitorRelUuid must be supplied, but found both");
+ Validate.isTrue(body.getDebitorRel() != null || body.getDebitorRelUuid() != null,
+ "ERROR: [400] exactly one of debitorRel and debitorRelUuid must be supplied, but found none");
+ Validate.isTrue(body.getDebitorRel() == null ||
+ body.getDebitorRel().getType() == null || DEBITOR.name().equals(body.getDebitorRel().getType()),
+ "ERROR: [400] debitorRel.type must be '"+DEBITOR.name()+"' or null for default");
+ Validate.isTrue(body.getDebitorRel() == null || body.getDebitorRel().getMark() == null,
+ "ERROR: [400] debitorRel.mark must be null");
- final var saved = debitorRepo.save(entityToSave);
+ final var entityToSave = mapper.map(body, HsOfficeDebitorEntity.class);
+ if ( body.getDebitorRel() != null ) {
+ body.getDebitorRel().setType(DEBITOR.name());
+ final var debitorRel = mapper.map(body.getDebitorRel(), HsOfficeRelationEntity.class);
+ entityToSave.setDebitorRel(relRepo.save(debitorRel));
+ } else {
+ final var debitorRelOptional = relRepo.findByUuid(body.getDebitorRelUuid());
+ debitorRelOptional.ifPresentOrElse(
+ debitorRel -> {entityToSave.setDebitorRel(relRepo.save(debitorRel));},
+ () -> { throw new EntityNotFoundException("ERROR: [400] debitorRelUuid not found: " + body.getDebitorRelUuid());});
+ }
+
+ final var savedEntity = debitorRepo.save(entityToSave);
+ em.flush();
+ em.refresh(savedEntity);
final var uri =
MvcUriComponentsBuilder.fromController(getClass())
.path("/api/hs/office/debitors/{id}")
- .buildAndExpand(saved.getUuid())
+ .buildAndExpand(savedEntity.getUuid())
.toUri();
- final var mapped = mapper.map(saved, HsOfficeDebitorResource.class);
+ final var mapped = mapper.map(savedEntity, HsOfficeDebitorResource.class);
return ResponseEntity.created(uri).body(mapped);
}
@@ -119,6 +151,7 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
new HsOfficeDebitorEntityPatcher(em, current).apply(body);
final var saved = debitorRepo.save(current);
+ Hibernate.initialize(saved);
final var mapped = mapper.map(saved, HsOfficeDebitorResource.class);
return ResponseEntity.ok(mapped);
}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntity.java
index 76480ac0..08c70f66 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntity.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntity.java
@@ -3,40 +3,56 @@ package net.hostsharing.hsadminng.hs.office.debitor;
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.relation.HsOfficeRelationEntity;
+import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
+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 org.hibernate.annotations.JoinFormula;
+import org.hibernate.annotations.NotFound;
+import org.hibernate.annotations.NotFoundAction;
import jakarta.persistence.*;
-import java.util.Optional;
+import jakarta.validation.constraints.Pattern;
+import java.io.IOException;
import java.util.UUID;
+import static jakarta.persistence.CascadeType.DETACH;
+import static jakarta.persistence.CascadeType.MERGE;
+import static jakarta.persistence.CascadeType.PERSIST;
+import static jakarta.persistence.CascadeType.REFRESH;
+import static java.util.Optional.ofNullable;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NULLABLE;
+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.directlyFetchedByDependsOnColumn;
+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
@Table(name = "hs_office_debitor_rv")
@Getter
@Setter
-@Builder
+@Builder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
@DisplayName("Debitor")
-public class HsOfficeDebitorEntity implements HasUuid, Stringifyable {
+public class HsOfficeDebitorEntity implements RbacObject, Stringifyable {
public static final String DEBITOR_NUMBER_TAG = "D-";
+ public static final String TWO_DECIMAL_DIGITS = "^([0-9]{2})$";
- // TODO: I would rather like to generate something matching this example:
- // debitor(1234500: Test AG, tes)
- // maybe remove withSepararator (always use ', ') and add withBusinessIdProp (with ': ' afterwards)?
private static Stringify stringify =
stringify(HsOfficeDebitorEntity.class, "debitor")
- .withProp(e -> DEBITOR_NUMBER_TAG + e.getDebitorNumber())
- .withProp(HsOfficeDebitorEntity::getPartner)
+ .withIdProp(HsOfficeDebitorEntity::toShortString)
+ .withProp(e -> ofNullable(e.getDebitorRel()).map(HsOfficeRelationEntity::toShortString).orElse(null))
.withProp(HsOfficeDebitorEntity::getDefaultPrefix)
- .withSeparator(": ")
.quotedValues(false);
@Id
@@ -45,15 +61,29 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable {
private UUID uuid;
@ManyToOne
- @JoinColumn(name = "partneruuid")
+ @JoinFormula(
+ referencedColumnName = "uuid",
+ value = """
+ (
+ SELECT DISTINCT partner.uuid
+ FROM hs_office_partner_rv partner
+ JOIN hs_office_relation_rv dRel
+ ON dRel.uuid = debitorreluuid AND dRel.type = 'DEBITOR'
+ JOIN hs_office_relation_rv pRel
+ ON pRel.uuid = partner.partnerRelUuid AND pRel.type = 'PARTNER'
+ WHERE pRel.holderUuid = dRel.anchorUuid
+ )
+ """)
+ @NotFound(action = NotFoundAction.IGNORE)
private HsOfficePartnerEntity partner;
- @Column(name = "debitornumbersuffix", columnDefinition = "numeric(2)")
- private Byte debitorNumberSuffix; // TODO maybe rather as a formatted String?
+ @Column(name = "debitornumbersuffix", length = 2)
+ @Pattern(regexp = TWO_DECIMAL_DIGITS)
+ private String debitorNumberSuffix;
- @ManyToOne
- @JoinColumn(name = "billingcontactuuid")
- private HsOfficeContactEntity billingContact; // TODO: migrate to billingPerson
+ @ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = false)
+ @JoinColumn(name = "debitorreluuid", nullable = false)
+ private HsOfficeRelationEntity debitorRel;
@Column(name = "billable", nullable = false)
private Boolean billable; // not a primitive because otherwise the default would be false
@@ -78,14 +108,16 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable {
private String defaultPrefix;
private String getDebitorNumberString() {
- if (partner == null || partner.getPartnerNumber() == null || debitorNumberSuffix == null ) {
- return null;
- }
- return partner.getPartnerNumber() + String.format("%02d", debitorNumberSuffix);
+ return ofNullable(partner)
+ .filter(partner -> debitorNumberSuffix != null)
+ .map(HsOfficePartnerEntity::getPartnerNumber)
+ .map(Object::toString)
+ .map(partnerNumber -> partnerNumber + debitorNumberSuffix)
+ .orElse(null);
}
public Integer getDebitorNumber() {
- return Optional.ofNullable(getDebitorNumberString()).map(Integer::parseInt).orElse(null);
+ return ofNullable(getDebitorNumberString()).map(Integer::parseInt).orElse(null);
}
@Override
@@ -97,4 +129,68 @@ 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 AS uuid,
+ 'D-' || (SELECT partner.partnerNumber
+ FROM hs_office_partner partner
+ JOIN hs_office_relation partnerRel
+ ON partnerRel.uuid = partner.partnerRelUUid AND partnerRel.type = 'PARTNER'
+ JOIN hs_office_relation debitorRel
+ ON debitorRel.anchorUuid = partnerRel.holderUuid AND debitorRel.type = 'DEBITOR'
+ WHERE debitorRel.uuid = debitor.debitorRelUuid)
+ || debitorNumberSuffix as idName
+ FROM hs_office_debitor AS debitor
+ """))
+ .withRestrictedViewOrderBy(SQL.projection("defaultPrefix"))
+ .withUpdatableColumns(
+ "debitorRelUuid",
+ "billable",
+ "refundBankAccountUuid",
+ "vatId",
+ "vatCountryCode",
+ "vatBusiness",
+ "vatReverseCharge",
+ "defaultPrefix" /* TODO.spec: do we want that updatable? */)
+ .toRole("global", ADMIN).grantPermission(INSERT)
+
+ .importRootEntityAliasProxy("debitorRel", HsOfficeRelationEntity.class,
+ directlyFetchedByDependsOnColumn(),
+ dependsOnColumn("debitorRelUuid"))
+ .createPermission(DELETE).grantedTo("debitorRel", OWNER)
+ .createPermission(UPDATE).grantedTo("debitorRel", ADMIN)
+ .createPermission(SELECT).grantedTo("debitorRel", TENANT)
+
+ .importEntityAlias("refundBankAccount", HsOfficeBankAccountEntity.class,
+ dependsOnColumn("refundBankAccountUuid"),
+ directlyFetchedByDependsOnColumn(),
+ NULLABLE)
+ .toRole("refundBankAccount", ADMIN).grantRole("debitorRel", AGENT)
+ .toRole("debitorRel", AGENT).grantRole("refundBankAccount", REFERRER)
+
+ .importEntityAlias("partnerRel", HsOfficeRelationEntity.class,
+ dependsOnColumn("debitorRelUuid"),
+ fetchedBySql("""
+ SELECT ${columns}
+ FROM hs_office_relation AS partnerRel
+ JOIN hs_office_relation AS debitorRel
+ ON debitorRel.type = 'DEBITOR' AND debitorRel.anchorUuid = partnerRel.holderUuid
+ WHERE partnerRel.type = 'PARTNER'
+ AND ${REF}.debitorRelUuid = debitorRel.uuid
+ """),
+ NOT_NULL)
+ .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("5-hs-office/506-debitor/5063-hs-office-debitor-rbac");
+ }
}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntityPatcher.java b/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntityPatcher.java
index 914c8230..cd50abf8 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntityPatcher.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntityPatcher.java
@@ -1,8 +1,8 @@
package net.hostsharing.hsadminng.hs.office.debitor;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
-import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorPatchResource;
+import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity;
import net.hostsharing.hsadminng.mapper.EntityPatcher;
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
@@ -23,9 +23,9 @@ class HsOfficeDebitorEntityPatcher implements EntityPatcher {
- verifyNotNull(newValue, "billingContact");
- entity.setBillingContact(em.getReference(HsOfficeContactEntity.class, newValue));
+ OptionalFromJson.of(resource.getDebitorRelUuid()).ifPresent(newValue -> {
+ verifyNotNull(newValue, "debitorRel");
+ entity.setDebitorRel(em.getReference(HsOfficeRelationEntity.class, newValue));
});
Optional.ofNullable(resource.getBillable()).ifPresent(entity::setBillable);
OptionalFromJson.of(resource.getVatId()).ifPresent(entity::setVatId);
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepository.java
index 64be98b1..737c24ba 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepository.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepository.java
@@ -13,7 +13,10 @@ public interface HsOfficeDebitorRepository extends Repository findDebitorByDebitorNumber(int partnerNumber, byte debitorNumberSuffix);
@@ -24,9 +27,15 @@ public interface HsOfficeDebitorRepository extends Repository> listMemberships(
@@ -58,7 +52,7 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
public ResponseEntity addMembership(
final String currentUser,
final String assumedRoles,
- @Valid final HsOfficeMembershipInsertResource body) {
+ final HsOfficeMembershipInsertResource body) {
context.define(currentUser, assumedRoles);
@@ -121,7 +115,7 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
final var current = membershipRepo.findByUuid(membershipUuid).orElseThrow();
- new HsOfficeMembershipEntityPatcher(em, mapper, current).apply(body);
+ new HsOfficeMembershipEntityPatcher(mapper, current).apply(body);
final var saved = membershipRepo.save(current);
final var mapped = mapper.map(saved, HsOfficeMembershipResource.class, SEPA_MANDATE_ENTITY_TO_RESOURCE_POSTMAPPER);
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntity.java
index 9861f727..c486dc92 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntity.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntity.java
@@ -1,23 +1,38 @@
package net.hostsharing.hsadminng.hs.office.membership;
-import com.vladmihalcea.hibernate.type.range.PostgreSQLRangeType;
-import com.vladmihalcea.hibernate.type.range.Range;
+import io.hypersistence.utils.hibernate.type.range.PostgreSQLRangeType;
+import io.hypersistence.utils.hibernate.type.range.Range;
import lombok.*;
import net.hostsharing.hsadminng.errors.DisplayName;
-import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
-import net.hostsharing.hsadminng.persistence.HasUuid;
+import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity;
+import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
+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.Fetch;
-import org.hibernate.annotations.FetchMode;
import org.hibernate.annotations.Type;
import jakarta.persistence.*;
+import jakarta.validation.constraints.Pattern;
+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.Nullable.NOT_NULL;
+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.RbacUserReference.UserRole.CREATOR;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.ADMIN;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.AGENT;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.OWNER;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.TENANT;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.TENANT;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.TENANT;
+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
@@ -28,17 +43,16 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor
@AllArgsConstructor
@DisplayName("Membership")
-public class HsOfficeMembershipEntity implements HasUuid, Stringifyable {
+public class HsOfficeMembershipEntity implements RbacObject, Stringifyable {
public static final String MEMBER_NUMBER_TAG = "M-";
+ public static final String TWO_DECIMAL_DIGITS = "^([0-9]{2})$";
private static Stringify stringify = stringify(HsOfficeMembershipEntity.class)
.withProp(e -> MEMBER_NUMBER_TAG + e.getMemberNumber())
.withProp(e -> e.getPartner().toShortString())
- .withProp(e -> e.getMainDebitor().toShortString())
.withProp(e -> e.getValidity().asString())
.withProp(HsOfficeMembershipEntity::getReasonForTermination)
- .withSeparator(", ")
.quotedValues(false);
@Id
@@ -49,12 +63,8 @@ public class HsOfficeMembershipEntity implements HasUuid, Stringifyable {
@JoinColumn(name = "partneruuid")
private HsOfficePartnerEntity partner;
- @ManyToOne
- @Fetch(FetchMode.JOIN)
- @JoinColumn(name = "maindebitoruuid")
- private HsOfficeDebitorEntity mainDebitor;
-
@Column(name = "membernumbersuffix", length = 2)
+ @Pattern(regexp = TWO_DECIMAL_DIGITS)
private String memberNumberSuffix;
@Column(name = "validity", columnDefinition = "daterange")
@@ -114,4 +124,45 @@ public class HsOfficeMembershipEntity implements HasUuid, Stringifyable {
setReasonForTermination(HsOfficeReasonForTermination.NONE);
}
}
+
+ public static RbacView rbac() {
+ return rbacViewFor("membership", HsOfficeMembershipEntity.class)
+ .withIdentityView(SQL.query("""
+ SELECT m.uuid AS uuid,
+ 'M-' || p.partnerNumber || m.memberNumberSuffix as idName
+ FROM hs_office_membership AS m
+ JOIN hs_office_partner AS p ON p.uuid = m.partnerUuid
+ """))
+ .withRestrictedViewOrderBy(SQL.projection("validity"))
+ .withUpdatableColumns("validity", "membershipFeeBillable", "reasonForTermination")
+
+ .importEntityAlias("partnerRel", HsOfficeRelationEntity.class,
+ dependsOnColumn("partnerUuid"),
+ fetchedBySql("""
+ SELECT ${columns}
+ FROM hs_office_partner AS partner
+ JOIN hs_office_relation AS partnerRel ON partnerRel.uuid = partner.partnerRelUuid
+ WHERE partner.uuid = ${REF}.partnerUuid
+ """),
+ NOT_NULL)
+ .toRole("global", ADMIN).grantPermission(INSERT)
+
+ .createRole(OWNER, (with) -> {
+ with.owningUser(CREATOR);
+ })
+ .createSubRole(ADMIN, (with) -> {
+ with.incomingSuperRole("partnerRel", ADMIN);
+ with.permission(DELETE);
+ with.permission(UPDATE);
+ })
+ .createSubRole(AGENT, (with) -> {
+ with.incomingSuperRole("partnerRel", AGENT);
+ with.outgoingSubRole("partnerRel", TENANT);
+ with.permission(SELECT);
+ });
+ }
+
+ public static void main(String[] args) throws IOException {
+ rbac().generateWithBaseFileName("5-hs-office/510-membership/5103-hs-office-membership-rbac");
+ }
}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityPatcher.java b/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityPatcher.java
index 59fa6070..89933fe8 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityPatcher.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityPatcher.java
@@ -1,37 +1,26 @@
package net.hostsharing.hsadminng.hs.office.membership;
-import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipPatchResource;
import net.hostsharing.hsadminng.mapper.EntityPatcher;
import net.hostsharing.hsadminng.mapper.Mapper;
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
-import jakarta.persistence.EntityManager;
import java.util.Optional;
-import java.util.UUID;
public class HsOfficeMembershipEntityPatcher implements EntityPatcher {
- private final EntityManager em;
private final Mapper mapper;
private final HsOfficeMembershipEntity entity;
public HsOfficeMembershipEntityPatcher(
- final EntityManager em,
final Mapper mapper,
final HsOfficeMembershipEntity entity) {
- this.em = em;
this.mapper = mapper;
this.entity = entity;
}
@Override
public void apply(final HsOfficeMembershipPatchResource resource) {
- OptionalFromJson.of(resource.getMainDebitorUuid())
- .ifPresent(newValue -> {
- verifyNotNull(newValue, "debitor");
- entity.setMainDebitor(em.getReference(HsOfficeDebitorEntity.class, newValue));
- });
OptionalFromJson.of(resource.getValidTo()).ifPresent(
entity::setValidTo);
Optional.ofNullable(resource.getReasonForTermination())
@@ -40,10 +29,4 @@ public class HsOfficeMembershipEntityPatcher implements EntityPatcher E ref(final Class entityClass, final UUID uuid) {
+ private E ref(final Class entityClass, final UUID uuid) {
try {
return em.getReference(entityClass, uuid);
} catch (final Throwable exc) {
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerDetailsEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerDetailsEntity.java
index 55b30148..6fae8dc0 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerDetailsEntity.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerDetailsEntity.java
@@ -2,14 +2,20 @@ package net.hostsharing.hsadminng.hs.office.partner;
import lombok.*;
import net.hostsharing.hsadminng.errors.DisplayName;
-import net.hostsharing.hsadminng.persistence.HasUuid;
+import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
+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.Permission.*;
+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
@@ -20,7 +26,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor
@AllArgsConstructor
@DisplayName("PartnerDetails")
-public class HsOfficePartnerDetailsEntity implements HasUuid, Stringifyable {
+public class HsOfficePartnerDetailsEntity implements RbacObject, Stringifyable {
private static Stringify stringify = stringify(
HsOfficePartnerDetailsEntity.class,
@@ -31,7 +37,6 @@ public class HsOfficePartnerDetailsEntity implements HasUuid, Stringifyable {
.withProp(HsOfficePartnerDetailsEntity::getBirthday)
.withProp(HsOfficePartnerDetailsEntity::getBirthName)
.withProp(HsOfficePartnerDetailsEntity::getDateOfDeath)
- .withSeparator(", ")
.quotedValues(false);
@Id
@@ -55,6 +60,36 @@ public class HsOfficePartnerDetailsEntity implements HasUuid, Stringifyable {
return registrationNumber != null ? registrationNumber
: birthName != null ? birthName
: birthday != null ? birthday.toString()
- : dateOfDeath != null ? dateOfDeath.toString() : "";
+ : dateOfDeath != null ? dateOfDeath.toString()
+ : "";
+ }
+
+
+ public static RbacView rbac() {
+ return rbacViewFor("partnerDetails", HsOfficePartnerDetailsEntity.class)
+ .withIdentityView(SQL.query("""
+ SELECT partnerDetails.uuid as uuid, partner_iv.idName as idName
+ 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
+ """))
+ .withRestrictedViewOrderBy(SQL.expression("uuid"))
+ .withUpdatableColumns(
+ "registrationOffice",
+ "registrationNumber",
+ "birthPlace",
+ "birthName",
+ "birthday",
+ "dateOfDeath")
+ .toRole("global", ADMIN).grantPermission(INSERT)
+
+ // 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("5-hs-office/504-partner/5044-hs-office-partner-details-rbac");
}
}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntity.java
index 342b601c..43b78fca 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntity.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntity.java
@@ -1,20 +1,40 @@
package net.hostsharing.hsadminng.hs.office.partner;
-import lombok.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
import net.hostsharing.hsadminng.errors.DisplayName;
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.rbacobject.RbacObject;
+import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity;
+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.util.Optional;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+import java.io.IOException;
import java.util.UUID;
+import static jakarta.persistence.CascadeType.*;
+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.directlyFetchedByDependsOnColumn;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
+import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity
@@ -25,12 +45,20 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor
@AllArgsConstructor
@DisplayName("Partner")
-public class HsOfficePartnerEntity implements Stringifyable, HasUuid {
+public class HsOfficePartnerEntity implements Stringifyable, RbacObject {
+
+ public static final String PARTNER_NUMBER_TAG = "P-";
private static Stringify stringify = stringify(HsOfficePartnerEntity.class, "partner")
- .withProp(HsOfficePartnerEntity::getPerson)
- .withProp(HsOfficePartnerEntity::getContact)
- .withSeparator(": ")
+ .withIdProp(HsOfficePartnerEntity::toShortString)
+ .withProp(p -> ofNullable(p.getPartnerRel())
+ .map(HsOfficeRelationEntity::getHolder)
+ .map(HsOfficePersonEntity::toShortString)
+ .orElse(null))
+ .withProp(p -> ofNullable(p.getPartnerRel())
+ .map(HsOfficeRelationEntity::getContact)
+ .map(HsOfficeContactEntity::toShortString)
+ .orElse(null))
.quotedValues(false);
@Id
@@ -40,25 +68,19 @@ public class HsOfficePartnerEntity implements Stringifyable, HasUuid {
@Column(name = "partnernumber", columnDefinition = "numeric(5) not null")
private Integer partnerNumber;
- @ManyToOne
- @JoinColumn(name = "partnerroleuuid", nullable = false)
- private HsOfficeRelationshipEntity partnerRole;
+ @ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = false)
+ @JoinColumn(name = "partnerreluuid", nullable = false)
+ private HsOfficeRelationEntity partnerRel;
- // TODO: remove, is replaced by partnerRole
- @ManyToOne
- @JoinColumn(name = "personuuid", nullable = false)
- private HsOfficePersonEntity person;
-
- // TODO: remove, is replaced by partnerRole
- @ManyToOne
- @JoinColumn(name = "contactuuid", nullable = false)
- private HsOfficeContactEntity contact;
-
- @ManyToOne(cascade = { CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH }, optional = true)
+ @ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = true)
@JoinColumn(name = "detailsuuid")
@NotFound(action = NotFoundAction.IGNORE)
private HsOfficePartnerDetailsEntity details;
+ public String getTaggedPartnerNumber() {
+ return PARTNER_NUMBER_TAG + partnerNumber;
+ }
+
@Override
public String toString() {
return stringify.apply(this);
@@ -66,6 +88,31 @@ public class HsOfficePartnerEntity implements Stringifyable, HasUuid {
@Override
public String toShortString() {
- return Optional.ofNullable(person).map(HsOfficePersonEntity::toShortString).orElse("");
+ return getTaggedPartnerNumber();
+ }
+
+ public static RbacView rbac() {
+ return rbacViewFor("partner", HsOfficePartnerEntity.class)
+ .withIdentityView(SQL.projection("'P-' || partnerNumber"))
+ .withUpdatableColumns("partnerRelUuid")
+ .toRole("global", ADMIN).grantPermission(INSERT)
+
+ .importRootEntityAliasProxy("partnerRel", HsOfficeRelationEntity.class,
+ directlyFetchedByDependsOnColumn(),
+ dependsOnColumn("partnerRelUuid"))
+ .createPermission(DELETE).grantedTo("partnerRel", ADMIN)
+ .createPermission(UPDATE).grantedTo("partnerRel", AGENT)
+ .createPermission(SELECT).grantedTo("partnerRel", TENANT)
+
+ .importSubEntityAlias("partnerDetails", HsOfficePartnerDetailsEntity.class,
+ directlyFetchedByDependsOnColumn(),
+ 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("5-hs-office/504-partner/5043-hs-office-partner-rbac");
}
}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcher.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcher.java
index bc5de4d7..e43009c5 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcher.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcher.java
@@ -1,13 +1,11 @@
package net.hostsharing.hsadminng.hs.office.partner;
-import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerPatchResource;
-import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
+import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity;
import net.hostsharing.hsadminng.mapper.EntityPatcher;
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
import jakarta.persistence.EntityManager;
-import java.util.UUID;
class HsOfficePartnerEntityPatcher implements EntityPatcher {
private final EntityManager em;
@@ -21,19 +19,15 @@ class HsOfficePartnerEntityPatcher implements EntityPatcher {
- verifyNotNull(newValue, "contact");
- entity.setContact(em.getReference(HsOfficeContactEntity.class, newValue));
- });
- OptionalFromJson.of(resource.getPersonUuid()).ifPresent(newValue -> {
- verifyNotNull(newValue, "person");
- entity.setPerson(em.getReference(HsOfficePersonEntity.class, newValue));
+ OptionalFromJson.of(resource.getPartnerRelUuid()).ifPresent(newValue -> {
+ verifyNotNull(newValue, "partnerRel");
+ entity.setPartnerRel(em.getReference(HsOfficeRelationEntity.class, newValue));
});
new HsOfficePartnerDetailsEntityPatcher(em, entity.getDetails()).apply(resource.getDetails());
}
- private void verifyNotNull(final UUID newValue, final String propertyName) {
+ private void verifyNotNull(final Object newValue, final String propertyName) {
if (newValue == null) {
throw new IllegalArgumentException("property '" + propertyName + "' must not be null");
}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepository.java
index dfbd1667..6594cb1b 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepository.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepository.java
@@ -11,10 +11,13 @@ public interface HsOfficePartnerRepository extends Repository findByUuid(UUID id);
+ List findAll(); // TODO.impl: move to a repo in test sources
+
@Query("""
SELECT partner FROM HsOfficePartnerEntity partner
- JOIN HsOfficeContactEntity contact ON contact.uuid = partner.contact.uuid
- JOIN HsOfficePersonEntity person ON person.uuid = partner.person.uuid
+ JOIN HsOfficeRelationEntity rel ON rel.uuid = partner.partnerRel.uuid
+ JOIN HsOfficeContactEntity contact ON contact.uuid = rel.contact.uuid
+ JOIN HsOfficePersonEntity person ON person.uuid = rel.holder.uuid
WHERE :name is null
OR partner.details.birthName like concat(cast(:name as text), '%')
OR contact.label like concat(cast(:name as text), '%')
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonEntity.java
index f36ce946..5f2ad6ec 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonEntity.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonEntity.java
@@ -3,14 +3,22 @@ package net.hostsharing.hsadminng.hs.office.person;
import lombok.*;
import lombok.experimental.FieldNameConstants;
import net.hostsharing.hsadminng.errors.DisplayName;
-import net.hostsharing.hsadminng.persistence.HasUuid;
+import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
+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
@@ -22,7 +30,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@AllArgsConstructor
@FieldNameConstants
@DisplayName("Person")
-public class HsOfficePersonEntity implements HasUuid, Stringifyable {
+public class HsOfficePersonEntity implements RbacObject, Stringifyable {
private static Stringify toString = stringify(HsOfficePersonEntity.class, "person")
.withProp(Fields.personType, HsOfficePersonEntity::getPersonType)
@@ -64,4 +72,28 @@ public class HsOfficePersonEntity implements HasUuid, Stringifyable {
return personType + " " +
(!StringUtils.isEmpty(tradeName) ? tradeName : (StringUtils.isEmpty(salutation) ? "" : salutation + " ") + (familyName + ", " + givenName));
}
+
+ public static RbacView rbac() {
+ return rbacViewFor("person", HsOfficePersonEntity.class)
+ .withIdentityView(SQL.projection("concat(tradeName, familyName, givenName)"))
+ .withUpdatableColumns("personType", "tradeName", "givenName", "familyName")
+ .toRole("global", GUEST).grantPermission(INSERT)
+
+ .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("5-hs-office/502-person/5023-hs-office-person-rbac");
+ }
}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationController.java
similarity index 51%
rename from src/main/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipController.java
rename to src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationController.java
index 98c6bccf..e1f80148 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipController.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationController.java
@@ -1,8 +1,8 @@
-package net.hostsharing.hsadminng.hs.office.relationship;
+package net.hostsharing.hsadminng.hs.office.relation;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRepository;
-import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeRelationshipsApi;
+import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeRelationsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository;
import net.hostsharing.hsadminng.mapper.Mapper;
@@ -22,7 +22,7 @@ import java.util.function.BiConsumer;
@RestController
-public class HsOfficeRelationshipController implements HsOfficeRelationshipsApi {
+public class HsOfficeRelationController implements HsOfficeRelationsApi {
@Autowired
private Context context;
@@ -31,10 +31,10 @@ public class HsOfficeRelationshipController implements HsOfficeRelationshipsApi
private Mapper mapper;
@Autowired
- private HsOfficeRelationshipRepository relationshipRepo;
+ private HsOfficeRelationRepository relationRepo;
@Autowired
- private HsOfficePersonRepository relHolderRepo;
+ private HsOfficePersonRepository holderRepo;
@Autowired
private HsOfficeContactRepository contactRepo;
@@ -44,79 +44,80 @@ public class HsOfficeRelationshipController implements HsOfficeRelationshipsApi
@Override
@Transactional(readOnly = true)
- public ResponseEntity> listRelationships(
+ public ResponseEntity> listRelations(
final String currentUser,
final String assumedRoles,
final UUID personUuid,
- final HsOfficeRelationshipTypeResource relationshipType) {
+ final HsOfficeRelationTypeResource relationType) {
context.define(currentUser, assumedRoles);
- final var entities = relationshipRepo.findRelationshipRelatedToPersonUuidAndRelationshipType(personUuid,
- mapper.map(relationshipType, HsOfficeRelationshipType.class));
+ final var entities = relationRepo.findRelationRelatedToPersonUuidAndRelationType(personUuid,
+ mapper.map(relationType, HsOfficeRelationType.class));
- final var resources = mapper.mapList(entities, HsOfficeRelationshipResource.class,
- RELATIONSHIP_ENTITY_TO_RESOURCE_POSTMAPPER);
+ final var resources = mapper.mapList(entities, HsOfficeRelationResource.class,
+ RELATION_ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(resources);
}
@Override
@Transactional
- public ResponseEntity addRelationship(
+ public ResponseEntity addRelation(
final String currentUser,
final String assumedRoles,
- final HsOfficeRelationshipInsertResource body) {
+ final HsOfficeRelationInsertResource body) {
context.define(currentUser, assumedRoles);
- final var entityToSave = new HsOfficeRelationshipEntity();
- entityToSave.setRelType(HsOfficeRelationshipType.valueOf(body.getRelType()));
- entityToSave.setRelAnchor(relHolderRepo.findByUuid(body.getRelAnchorUuid()).orElseThrow(
- () -> new NoSuchElementException("cannot find relAnchorUuid " + body.getRelAnchorUuid())
+ final var entityToSave = new HsOfficeRelationEntity();
+ entityToSave.setType(HsOfficeRelationType.valueOf(body.getType()));
+ entityToSave.setMark(body.getMark());
+ entityToSave.setAnchor(holderRepo.findByUuid(body.getAnchorUuid()).orElseThrow(
+ () -> new NoSuchElementException("cannot find anchorUuid " + body.getAnchorUuid())
));
- entityToSave.setRelHolder(relHolderRepo.findByUuid(body.getRelHolderUuid()).orElseThrow(
- () -> new NoSuchElementException("cannot find relHolderUuid " + body.getRelHolderUuid())
+ entityToSave.setHolder(holderRepo.findByUuid(body.getHolderUuid()).orElseThrow(
+ () -> new NoSuchElementException("cannot find holderUuid " + body.getHolderUuid())
));
entityToSave.setContact(contactRepo.findByUuid(body.getContactUuid()).orElseThrow(
() -> new NoSuchElementException("cannot find contactUuid " + body.getContactUuid())
));
- final var saved = relationshipRepo.save(entityToSave);
+ final var saved = relationRepo.save(entityToSave);
final var uri =
MvcUriComponentsBuilder.fromController(getClass())
- .path("/api/hs/office/relationships/{id}")
+ .path("/api/hs/office/relations/{id}")
.buildAndExpand(saved.getUuid())
.toUri();
- final var mapped = mapper.map(saved, HsOfficeRelationshipResource.class,
- RELATIONSHIP_ENTITY_TO_RESOURCE_POSTMAPPER);
+ final var mapped = mapper.map(saved, HsOfficeRelationResource.class,
+ RELATION_ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.created(uri).body(mapped);
}
@Override
@Transactional(readOnly = true)
- public ResponseEntity getRelationshipByUuid(
+ public ResponseEntity getRelationByUuid(
final String currentUser,
final String assumedRoles,
- final UUID relationshipUuid) {
+ final UUID relationUuid) {
context.define(currentUser, assumedRoles);
- final var result = relationshipRepo.findByUuid(relationshipUuid);
+ final var result = relationRepo.findByUuid(relationUuid);
if (result.isEmpty()) {
return ResponseEntity.notFound().build();
}
- return ResponseEntity.ok(mapper.map(result.get(), HsOfficeRelationshipResource.class, RELATIONSHIP_ENTITY_TO_RESOURCE_POSTMAPPER));
+ return ResponseEntity.ok(mapper.map(result.get(), HsOfficeRelationResource.class, RELATION_ENTITY_TO_RESOURCE_POSTMAPPER));
}
@Override
@Transactional
- public ResponseEntity deleteRelationshipByUuid(
+ public ResponseEntity deleteRelationByUuid(
final String currentUser,
final String assumedRoles,
- final UUID relationshipUuid) {
+ final UUID relationUuid) {
context.define(currentUser, assumedRoles);
- final var result = relationshipRepo.deleteByUuid(relationshipUuid);
+ final var result = relationRepo.deleteByUuid(relationUuid);
if (result == 0) {
return ResponseEntity.notFound().build();
}
@@ -126,27 +127,27 @@ public class HsOfficeRelationshipController implements HsOfficeRelationshipsApi
@Override
@Transactional
- public ResponseEntity patchRelationship(
+ public ResponseEntity patchRelation(
final String currentUser,
final String assumedRoles,
- final UUID relationshipUuid,
- final HsOfficeRelationshipPatchResource body) {
+ final UUID relationUuid,
+ final HsOfficeRelationPatchResource body) {
context.define(currentUser, assumedRoles);
- final var current = relationshipRepo.findByUuid(relationshipUuid).orElseThrow();
+ final var current = relationRepo.findByUuid(relationUuid).orElseThrow();
- new HsOfficeRelationshipEntityPatcher(em, current).apply(body);
+ new HsOfficeRelationEntityPatcher(em, current).apply(body);
- final var saved = relationshipRepo.save(current);
- final var mapped = mapper.map(saved, HsOfficeRelationshipResource.class);
+ final var saved = relationRepo.save(current);
+ final var mapped = mapper.map(saved, HsOfficeRelationResource.class);
return ResponseEntity.ok(mapped);
}
- final BiConsumer RELATIONSHIP_ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
- resource.setRelAnchor(mapper.map(entity.getRelAnchor(), HsOfficePersonResource.class));
- resource.setRelHolder(mapper.map(entity.getRelHolder(), HsOfficePersonResource.class));
+ final BiConsumer RELATION_ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
+ resource.setAnchor(mapper.map(entity.getAnchor(), HsOfficePersonResource.class));
+ resource.setHolder(mapper.map(entity.getHolder(), HsOfficePersonResource.class));
resource.setContact(mapper.map(entity.getContact(), HsOfficeContactResource.class));
};
}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntity.java
new file mode 100644
index 00000000..8d6c6fe8
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntity.java
@@ -0,0 +1,135 @@
+package net.hostsharing.hsadminng.hs.office.relation;
+
+import lombok.*;
+import lombok.experimental.FieldNameConstants;
+import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
+import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
+import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
+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.Nullable.NOT_NULL;
+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.directlyFetchedByDependsOnColumn;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
+import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
+
+@Entity
+@Table(name = "hs_office_relation_rv")
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@FieldNameConstants
+public class HsOfficeRelationEntity implements RbacObject, Stringifyable {
+
+ private static Stringify toString = stringify(HsOfficeRelationEntity.class, "rel")
+ .withProp(Fields.anchor, HsOfficeRelationEntity::getAnchor)
+ .withProp(Fields.type, HsOfficeRelationEntity::getType)
+ .withProp(Fields.mark, HsOfficeRelationEntity::getMark)
+ .withProp(Fields.holder, HsOfficeRelationEntity::getHolder)
+ .withProp(Fields.contact, HsOfficeRelationEntity::getContact);
+
+ private static Stringify toShortString = stringify(HsOfficeRelationEntity.class, "rel")
+ .withProp(Fields.anchor, HsOfficeRelationEntity::getAnchor)
+ .withProp(Fields.type, HsOfficeRelationEntity::getType)
+ .withProp(Fields.holder, HsOfficeRelationEntity::getHolder);
+
+ @Id
+ @GeneratedValue
+ private UUID uuid;
+
+ @ManyToOne
+ @JoinColumn(name = "anchoruuid")
+ private HsOfficePersonEntity anchor;
+
+ @ManyToOne
+ @JoinColumn(name = "holderuuid")
+ private HsOfficePersonEntity holder;
+
+ @ManyToOne
+ @JoinColumn(name = "contactuuid")
+ private HsOfficeContactEntity contact;
+
+ @Column(name = "type")
+ @Enumerated(EnumType.STRING)
+ private HsOfficeRelationType type;
+
+ @Column(name = "mark")
+ private String mark;
+
+ @Override
+ public String toString() {
+ return toString.apply(this);
+ }
+
+ @Override
+ public String toShortString() {
+ return toShortString.apply(this);
+ }
+
+ public static RbacView rbac() {
+ return rbacViewFor("relation", HsOfficeRelationEntity.class)
+ .withIdentityView(SQL.projection("""
+ (select idName from hs_office_person_iv p where p.uuid = anchorUuid)
+ || '-with-' || target.type || '-'
+ || (select idName from hs_office_person_iv p where p.uuid = holderUuid)
+ """))
+ .withRestrictedViewOrderBy(SQL.expression(
+ "(select idName from hs_office_person_iv p where p.uuid = target.holderUuid)"))
+ .withUpdatableColumns("contactUuid")
+ .importEntityAlias("anchorPerson", HsOfficePersonEntity.class,
+ dependsOnColumn("anchorUuid"),
+ directlyFetchedByDependsOnColumn(),
+ NOT_NULL)
+ .importEntityAlias("holderPerson", HsOfficePersonEntity.class,
+ dependsOnColumn("holderUuid"),
+ directlyFetchedByDependsOnColumn(),
+ NOT_NULL)
+ .importEntityAlias("contact", HsOfficeContactEntity.class,
+ dependsOnColumn("contactUuid"),
+ directlyFetchedByDependsOnColumn(),
+ NOT_NULL)
+ .createRole(OWNER, (with) -> {
+ with.owningUser(CREATOR);
+ with.incomingSuperRole(GLOBAL, ADMIN);
+ // TODO: if type=REPRESENTATIIVE
+ // with.incomingSuperRole("holderPerson", ADMIN);
+ with.permission(DELETE);
+ })
+ .createSubRole(ADMIN, (with) -> {
+ with.incomingSuperRole("anchorPerson", ADMIN);
+ // TODO: if type=REPRESENTATIIVE
+ // with.outgoingSuperRole("anchorPerson", OWNER);
+ 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);
+ })
+
+ .toRole("anchorPerson", ADMIN).grantPermission(INSERT);
+ }
+
+ public static void main(String[] args) throws IOException {
+ rbac().generateWithBaseFileName("5-hs-office/503-relation/5033-hs-office-relation-rbac");
+ }
+}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipEntityPatcher.java b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityPatcher.java
similarity index 67%
rename from src/main/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipEntityPatcher.java
rename to src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityPatcher.java
index fa080ba2..aeaae5ea 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipEntityPatcher.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityPatcher.java
@@ -1,25 +1,25 @@
-package net.hostsharing.hsadminng.hs.office.relationship;
+package net.hostsharing.hsadminng.hs.office.relation;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
-import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeRelationshipPatchResource;
+import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeRelationPatchResource;
import net.hostsharing.hsadminng.mapper.EntityPatcher;
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
import jakarta.persistence.EntityManager;
import java.util.UUID;
-class HsOfficeRelationshipEntityPatcher implements EntityPatcher {
+class HsOfficeRelationEntityPatcher implements EntityPatcher {
private final EntityManager em;
- private final HsOfficeRelationshipEntity entity;
+ private final HsOfficeRelationEntity entity;
- HsOfficeRelationshipEntityPatcher(final EntityManager em, final HsOfficeRelationshipEntity entity) {
+ HsOfficeRelationEntityPatcher(final EntityManager em, final HsOfficeRelationEntity entity) {
this.em = em;
this.entity = entity;
}
@Override
- public void apply(final HsOfficeRelationshipPatchResource resource) {
+ public void apply(final HsOfficeRelationPatchResource resource) {
OptionalFromJson.of(resource.getContactUuid()).ifPresent(newValue -> {
verifyNotNull(newValue, "contact");
entity.setContact(em.getReference(HsOfficeContactEntity.class, newValue));
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepository.java
new file mode 100644
index 00000000..95bac3a2
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepository.java
@@ -0,0 +1,37 @@
+package net.hostsharing.hsadminng.hs.office.relation;
+
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.Repository;
+
+import jakarta.validation.constraints.NotNull;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+public interface HsOfficeRelationRepository extends Repository {
+
+ Optional findByUuid(UUID id);
+
+ default List findRelationRelatedToPersonUuidAndRelationType(@NotNull UUID personUuid, HsOfficeRelationType relationType) {
+ return findRelationRelatedToPersonUuidAndRelationTypeString(personUuid, relationType.toString());
+ }
+
+ @Query(value = """
+ SELECT p.* FROM hs_office_relation_rv AS p
+ WHERE p.anchorUuid = :personUuid OR p.holderUuid = :personUuid
+ """, nativeQuery = true)
+ List findRelationRelatedToPersonUuid(@NotNull UUID personUuid);
+
+ @Query(value = """
+ SELECT p.* FROM hs_office_relation_rv AS p
+ WHERE (:relationType IS NULL OR p.type = cast(:relationType AS HsOfficeRelationType))
+ AND ( p.anchorUuid = :personUuid OR p.holderUuid = :personUuid)
+ """, nativeQuery = true)
+ List findRelationRelatedToPersonUuidAndRelationTypeString(@NotNull UUID personUuid, String relationType);
+
+ HsOfficeRelationEntity save(final HsOfficeRelationEntity entity);
+
+ long count();
+
+ int deleteByUuid(UUID uuid);
+}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipType.java b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationType.java
similarity index 50%
rename from src/main/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipType.java
rename to src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationType.java
index 2b9fe60c..035c9b55 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipType.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationType.java
@@ -1,12 +1,12 @@
-package net.hostsharing.hsadminng.hs.office.relationship;
+package net.hostsharing.hsadminng.hs.office.relation;
-public enum HsOfficeRelationshipType {
+public enum HsOfficeRelationType {
UNKNOWN,
PARTNER,
EX_PARTNER,
REPRESENTATIVE,
VIP_CONTACT,
- ACCOUNTING,
+ DEBITOR,
OPERATIONS,
SUBSCRIBER
}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipEntity.java
deleted file mode 100644
index 704f2760..00000000
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipEntity.java
+++ /dev/null
@@ -1,70 +0,0 @@
-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.stringify.Stringify;
-import net.hostsharing.hsadminng.stringify.Stringifyable;
-
-import jakarta.persistence.*;
-import java.util.UUID;
-
-import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
-
-@Entity
-@Table(name = "hs_office_relationship_rv")
-@Getter
-@Setter
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor
-@FieldNameConstants
-public class HsOfficeRelationshipEntity implements HasUuid, Stringifyable {
-
- private static Stringify toString = stringify(HsOfficeRelationshipEntity.class, "rel")
- .withProp(Fields.relAnchor, HsOfficeRelationshipEntity::getRelAnchor)
- .withProp(Fields.relType, HsOfficeRelationshipEntity::getRelType)
- .withProp(Fields.relMark, HsOfficeRelationshipEntity::getRelMark)
- .withProp(Fields.relHolder, HsOfficeRelationshipEntity::getRelHolder)
- .withProp(Fields.contact, HsOfficeRelationshipEntity::getContact);
-
- private static Stringify toShortString = stringify(HsOfficeRelationshipEntity.class, "rel")
- .withProp(Fields.relAnchor, HsOfficeRelationshipEntity::getRelAnchor)
- .withProp(Fields.relType, HsOfficeRelationshipEntity::getRelType)
- .withProp(Fields.relHolder, HsOfficeRelationshipEntity::getRelHolder);
-
- @Id
- @GeneratedValue
- private UUID uuid;
-
- @ManyToOne
- @JoinColumn(name = "relanchoruuid")
- private HsOfficePersonEntity relAnchor;
-
- @ManyToOne
- @JoinColumn(name = "relholderuuid")
- private HsOfficePersonEntity relHolder;
-
- @ManyToOne
- @JoinColumn(name = "contactuuid")
- private HsOfficeContactEntity contact;
-
- @Column(name = "reltype")
- @Enumerated(EnumType.STRING)
- private HsOfficeRelationshipType relType;
-
- @Column(name = "relmark")
- private String relMark;
-
- @Override
- public String toString() {
- return toString.apply(this);
- }
-
- @Override
- public String toShortString() {
- return toShortString.apply(this);
- }
-}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipRepository.java
deleted file mode 100644
index d34caa8c..00000000
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipRepository.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package net.hostsharing.hsadminng.hs.office.relationship;
-
-import org.springframework.data.jpa.repository.Query;
-import org.springframework.data.repository.Repository;
-
-import jakarta.validation.constraints.NotNull;
-import java.util.List;
-import java.util.Optional;
-import java.util.UUID;
-
-public interface HsOfficeRelationshipRepository extends Repository {
-
- Optional findByUuid(UUID id);
-
- default List findRelationshipRelatedToPersonUuidAndRelationshipType(@NotNull UUID personUuid, HsOfficeRelationshipType relationshipType) {
- return findRelationshipRelatedToPersonUuidAndRelationshipTypeString(personUuid, relationshipType.toString());
- }
-
- @Query(value = """
- SELECT p.* FROM hs_office_relationship_rv AS p
- WHERE p.relAnchorUuid = :personUuid OR p.relHolderUuid = :personUuid
- """, nativeQuery = true)
- List findRelationshipRelatedToPersonUuid(@NotNull UUID personUuid);
-
- @Query(value = """
- SELECT p.* FROM hs_office_relationship_rv AS p
- WHERE (:relationshipType IS NULL OR p.relType = cast(:relationshipType AS HsOfficeRelationshipType))
- AND ( p.relAnchorUuid = :personUuid OR p.relHolderUuid = :personUuid)
- """, nativeQuery = true)
- List findRelationshipRelatedToPersonUuidAndRelationshipTypeString(@NotNull UUID personUuid, String relationshipType);
-
- HsOfficeRelationshipEntity save(final HsOfficeRelationshipEntity entity);
-
- long count();
-
- int deleteByUuid(UUID uuid);
-}
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateController.java
index 581cd577..115b8948 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateController.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateController.java
@@ -14,7 +14,6 @@ import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBui
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
-import jakarta.validation.Valid;
import java.util.List;
import java.util.UUID;
import java.util.function.BiConsumer;
@@ -57,7 +56,7 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
public ResponseEntity addSepaMandate(
final String currentUser,
final String assumedRoles,
- @Valid final HsOfficeSepaMandateInsertResource body) {
+ final HsOfficeSepaMandateInsertResource body) {
context.define(currentUser, assumedRoles);
@@ -132,6 +131,7 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
if (entity.getValidity().hasUpperBound()) {
resource.setValidTo(entity.getValidity().upper().minusDays(1));
}
+ resource.getDebitor().setDebitorNumber(entity.getDebitor().getDebitorNumber());
};
final BiConsumer SEPA_MANDATE_RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntity.java
index baed26aa..ac831295 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntity.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntity.java
@@ -1,21 +1,32 @@
package net.hostsharing.hsadminng.hs.office.sepamandate;
-import com.vladmihalcea.hibernate.type.range.PostgreSQLRangeType;
-import com.vladmihalcea.hibernate.type.range.Range;
+import io.hypersistence.utils.hibernate.type.range.PostgreSQLRangeType;
+import io.hypersistence.utils.hibernate.type.range.Range;
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.persistence.HasUuid;
+import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity;
+import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
+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.Nullable.NOT_NULL;
+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
@@ -26,14 +37,13 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor
@AllArgsConstructor
@DisplayName("SEPA-Mandate")
-public class HsOfficeSepaMandateEntity implements Stringifyable, HasUuid {
+public class HsOfficeSepaMandateEntity implements Stringifyable, RbacObject {
private static Stringify stringify = stringify(HsOfficeSepaMandateEntity.class)
.withProp(e -> e.getBankAccount().getIban())
.withProp(HsOfficeSepaMandateEntity::getReference)
.withProp(HsOfficeSepaMandateEntity::getAgreement)
.withProp(e -> e.getValidity().asString())
- .withSeparator(", ")
.quotedValues(false);
@Id
@@ -84,4 +94,53 @@ public class HsOfficeSepaMandateEntity implements Stringifyable, HasUuid {
return reference;
}
+ public static RbacView rbac() {
+ return rbacViewFor("sepaMandate", HsOfficeSepaMandateEntity.class)
+ .withIdentityView(query("""
+ select sm.uuid as uuid, ba.iban || '-' || sm.validity as idName
+ from hs_office_sepamandate sm
+ join hs_office_bankaccount ba on ba.uuid = sm.bankAccountUuid
+ """))
+ .withRestrictedViewOrderBy(expression("validity"))
+ .withUpdatableColumns("reference", "agreement", "validity")
+
+ .importEntityAlias("debitorRel", HsOfficeRelationEntity.class,
+ dependsOnColumn("debitorUuid"),
+ fetchedBySql("""
+ SELECT ${columns}
+ FROM hs_office_relation debitorRel
+ JOIN hs_office_debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid
+ WHERE debitor.uuid = ${REF}.debitorUuid
+ """),
+ NOT_NULL)
+ .importEntityAlias("bankAccount", HsOfficeBankAccountEntity.class,
+ dependsOnColumn("bankAccountUuid"),
+ directlyFetchedByDependsOnColumn(),
+ NOT_NULL)
+
+ .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);
+ })
+
+ .toRole("debitorRel", ADMIN).grantPermission(INSERT);
+ }
+
+ public static void main(String[] args) throws IOException {
+ rbac().generateWithBaseFileName("5-hs-office/507-sepamandate/5073-hs-office-sepamandate-rbac");
+ }
}
diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/PostgresArray.java b/src/main/java/net/hostsharing/hsadminng/mapper/PostgresArray.java
deleted file mode 100644
index e1e1d056..00000000
--- a/src/main/java/net/hostsharing/hsadminng/mapper/PostgresArray.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package net.hostsharing.hsadminng.mapper;
-
-import lombok.experimental.UtilityClass;
-import org.postgresql.util.PGtokenizer;
-
-import java.lang.reflect.Array;
-import java.nio.charset.StandardCharsets;
-import java.util.function.Function;
-
-@UtilityClass
-public class PostgresArray {
-
- /**
- * Converts a byte[], as returned for a Postgres-array by native queries, to a Java array.
- *
- *
This example code worked with Hibernate 5 (Spring Boot 3.0.x):
- *
With Hibernate 6 (Spring Boot 3.1.x), this utility method can be used like such:
- *
- * final byte[] result = (byte[]) em.createNativeQuery("select * from currentSubjectsUuids() as uuids", UUID[].class)
- * .getSingleResult();
- * return fromPostgresArray(result, UUID.class, UUID::fromString);
- *
- *
- *
- * @param pgArray the byte[] returned by a native query containing as rendered for a Postgres array
- * @param elementClass the class of a single element of the Java array to be returned
- * @param itemParser converts a string element to the specified elementClass
- * @return a Java array containing the data from pgArray
- * @param type of a single element of the Java array
- */
- public static T[] fromPostgresArray(final byte[] pgArray, final Class elementClass, final Function itemParser) {
- final var pgArrayLiteral = new String(pgArray, StandardCharsets.UTF_8);
- if (pgArrayLiteral.length() == 2) {
- return newGenericArray(elementClass, 0);
- }
- final PGtokenizer tokenizer = new PGtokenizer(pgArrayLiteral.substring(1, pgArrayLiteral.length()-1), ',');
- tokenizer.remove("\"", "\"");
- final T[] array = newGenericArray(elementClass, tokenizer.getSize()); // Create a new array of the specified type and length
- for ( int n = 0; n < tokenizer.getSize(); ++n ) {
- final String token = tokenizer.getToken(n);
- if ( !"NULL".equals(token) ) {
- array[n] = itemParser.apply(token.trim().replace("\\\"", "\""));
- }
- }
- return array;
- }
-
- @SuppressWarnings("unchecked")
- private static T[] newGenericArray(final Class elementClass, final int length) {
- return (T[]) Array.newInstance(elementClass, length);
- }
-
-}
diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/PostgresDateRange.java b/src/main/java/net/hostsharing/hsadminng/mapper/PostgresDateRange.java
index c360db1a..db6ad189 100644
--- a/src/main/java/net/hostsharing/hsadminng/mapper/PostgresDateRange.java
+++ b/src/main/java/net/hostsharing/hsadminng/mapper/PostgresDateRange.java
@@ -1,6 +1,6 @@
package net.hostsharing.hsadminng.mapper;
-import com.vladmihalcea.hibernate.type.range.Range;
+import io.hypersistence.utils.hibernate.type.range.Range;
import lombok.experimental.UtilityClass;
import java.time.LocalDate;
diff --git a/src/main/java/net/hostsharing/hsadminng/persistence/HasUuid.java b/src/main/java/net/hostsharing/hsadminng/persistence/HasUuid.java
deleted file mode 100644
index 1f3ead14..00000000
--- a/src/main/java/net/hostsharing/hsadminng/persistence/HasUuid.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package net.hostsharing.hsadminng.persistence;
-
-import java.util.UUID;
-
-public interface HasUuid {
- UUID getUuid();
-}
diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java
new file mode 100644
index 00000000..7ef34252
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java
@@ -0,0 +1,260 @@
+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.PostgresTriggerReference.NEW;
+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);
+ generateGrantInsertRoleToExistingObjects(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 generateGrantInsertRoleToExistingObjects(final StringWriter plPgSql) {
+ getOptionalInsertSuperRole().ifPresent( superRoleDef -> {
+ plPgSql.writeLn("""
+ /*
+ Creates INSERT INTO ${rawSubTableName} permissions for the related ${rawSuperTableName} rows.
+ */
+ do language plpgsql $$
+ declare
+ row ${rawSuperTableName};
+ begin
+ call defineContext('create INSERT INTO ${rawSubTableName} permissions for the related ${rawSuperTableName} rows');
+
+ FOR row IN SELECT * FROM ${rawSuperTableName}
+ LOOP
+ call grantPermissionToRole(
+ createPermission(row.uuid, 'INSERT', '${rawSubTableName}'),
+ ${rawSuperRoleDescriptor});
+ END LOOP;
+ END;
+ $$;
+ """,
+ with("rawSubTableName", rbacDef.getRootEntityAlias().getRawTableName()),
+ with("rawSuperTableName", superRoleDef.getEntityAlias().getRawTableName()),
+ with("rawSuperRoleDescriptor", toRoleDescriptor(superRoleDef, "row"))
+ );
+ });
+ }
+
+ 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(
+ createPermission(NEW.uuid, 'INSERT', '${rawSubTableName}'),
+ ${rawSuperRoleDescriptor});
+ return NEW;
+ end; $$;
+
+ -- z_... is to put it at the end of after insert triggers, to make sure the roles exist
+ create trigger z_${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", toRoleDescriptor(superRoleDef, NEW.name()))
+ );
+ });
+ }
+
+ private void generateInsertCheckTrigger(final StringWriter plPgSql) {
+ getOptionalInsertGrant().ifPresentOrElse(g -> {
+ if (g.getSuperRoleDef().getEntityAlias().isGlobal()) {
+ switch (g.getSuperRoleDef().getRole()) {
+ case ADMIN -> {
+ generateInsertPermissionTriggerAllowOnlyGlobalAdmin(plPgSql);
+ }
+ case GUEST -> {
+ // no permission check trigger generated, as anybody can insert rows into this table
+ }
+ default -> {
+ throw new IllegalArgumentException(
+ "invalid global role for INSERT permission: " + g.getSuperRoleDef().getRole());
+ }
+ }
+ } else {
+ if (g.getSuperRoleDef().getEntityAlias().isFetchedByDirectForeignKey()) {
+ generateInsertPermissionTriggerAllowByRoleOfDirectForeignKey(plPgSql, g);
+ } else {
+ generateInsertPermissionTriggerAllowByRoleOfIndirectForeignKey(plPgSql, g);
+ }
+ }
+ },
+ () -> {
+ System.err.println("WARNING: no explicit INSERT grant for " + rbacDef.getRootEntityAlias().simpleName() + " => implicitly grant INSERT to global:ADMIN");
+ generateInsertPermissionTriggerAllowOnlyGlobalAdmin(plPgSql);
+ });
+ }
+
+ private void generateInsertPermissionTriggerAllowByRoleOfDirectForeignKey(final StringWriter plPgSql, final RbacView.RbacGrantDefinition g) {
+ plPgSql.writeLn("""
+ /**
+ Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable},
+ where the check is performed by a direct role.
+
+ A direct role is a role depending on a foreign key directly available in the NEW row.
+ */
+ 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; $$;
+
+ 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()));
+ }
+
+ private void generateInsertPermissionTriggerAllowByRoleOfIndirectForeignKey(
+ final StringWriter plPgSql,
+ final RbacView.RbacGrantDefinition g) {
+ plPgSql.writeLn("""
+ /**
+ Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable},
+ where the check is performed by an indirect role.
+
+ An indirect role is a role which depends on an object uuid which is not a direct foreign key
+ of the source entity, but needs to be fetched via joined tables.
+ */
+ create or replace function ${rawSubTable}_insert_permission_check_tf()
+ returns trigger
+ language plpgsql as $$
+
+ declare
+ superRoleObjectUuid uuid;
+
+ begin
+ """,
+ with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()));
+ plPgSql.chopEmptyLines();
+ plPgSql.indented(2, () -> {
+ plPgSql.writeLn(
+ "superRoleObjectUuid := (" + g.getSuperRoleDef().getEntityAlias().fetchSql().sql + ");\n" +
+ "assert superRoleObjectUuid is not null, 'superRoleObjectUuid must not be null';",
+ with("columns", g.getSuperRoleDef().getEntityAlias().aliasName() + ".uuid"),
+ with("ref", NEW.name()));
+ });
+ plPgSql.writeLn();
+ plPgSql.writeLn("""
+ if ( not hasInsertPermission(superRoleObjectUuid, 'INSERT', '${rawSubTable}') ) then
+ raise exception
+ '[403] insert into ${rawSubTable} not allowed for current subjects % (%)',
+ currentSubjects(), currentSubjectsUuids();
+ end if;
+ return NEW;
+ end; $$;
+
+ create trigger ${rawSubTable}_insert_permission_check_tg
+ before insert on ${rawSubTable}
+ for each row
+ execute procedure ${rawSubTable}_insert_permission_check_tf();
+
+ """,
+ with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()));
+ }
+
+ private void generateInsertPermissionTriggerAllowOnlyGlobalAdmin(final StringWriter plPgSql) {
+ plPgSql.writeLn("""
+ /**
+ Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable},
+ where only global-admin has that permission.
+ */
+ 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; $$;
+
+ create trigger ${rawSubTable}_insert_permission_check_tg
+ before insert on ${rawSubTable}
+ for each row
+ when ( not isGlobalAdmin() )
+ execute procedure ${rawSubTable}_insert_permission_missing_tf();
+ """,
+ with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()));
+ }
+
+ private Stream getInsertGrants() {
+ return rbacDef.getGrantDefs().stream()
+ .filter(g -> g.grantType() == PERM_TO_ROLE)
+ .filter(g -> g.getPermDef().toCreate && g.getPermDef().getPermission() == INSERT);
+ }
+
+ private Optional getOptionalInsertGrant() {
+ return getInsertGrants()
+ .reduce(singleton());
+ }
+
+ private Optional getOptionalInsertSuperRole() {
+ return getInsertGrants()
+ .map(RbacView.RbacGrantDefinition::getSuperRoleDef)
+ .reduce(singleton());
+ }
+
+ private static BinaryOperator 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().name());
+ }
+
+
+ private String toRoleDescriptor(final RbacView.RbacRoleDefinition roleDef, final String ref) {
+ final var functionName = toVar(roleDef);
+ if (roleDef.getEntityAlias().isGlobal()) {
+ return functionName + "()";
+ }
+ return functionName + "(" + ref + ")";
+ }
+}
diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/PostgresTriggerReference.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/PostgresTriggerReference.java
new file mode 100644
index 00000000..4fb5cb61
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/PostgresTriggerReference.java
@@ -0,0 +1,5 @@
+package net.hostsharing.hsadminng.rbac.rbacdef;
+
+public enum PostgresTriggerReference {
+ NEW, OLD
+}
diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacIdentityViewGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacIdentityViewGenerator.java
new file mode 100644
index 00000000..066acba2
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacIdentityViewGenerator.java
@@ -0,0 +1,47 @@
+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 generateRbacIdentityViewFromQuery('${rawTableName}',
+ $idName$
+ ${identityViewSqlPart}
+ $idName$);
+ """;
+ default -> throw new IllegalStateException("illegal SQL part given");
+ },
+ with("identityViewSqlPart", StringWriter.indented(2, rbacDef.getIdentityViewSqlQuery().sql)),
+ with("rawTableName", rawTableName));
+
+ plPgSql.writeLn("--//");
+ }
+}
diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacObjectGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacObjectGenerator.java
new file mode 100644
index 00000000..a7377301
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacObjectGenerator.java
@@ -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));
+ }
+}
diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacRestrictedViewGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacRestrictedViewGenerator.java
new file mode 100644
index 00000000..b5757865
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacRestrictedViewGenerator.java
@@ -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 rawTableName;
+
+ public RbacRestrictedViewGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) {
+ this.rbacDef = rbacDef;
+ this.liquibaseTagPrefix = liquibaseTagPrefix;
+ this.rawTableName = rbacDef.getRootEntityAlias().getRawTableName();
+ }
+
+ void generateTo(final StringWriter plPgSql) {
+ plPgSql.writeLn("""
+ -- ============================================================================
+ --changeset ${liquibaseTagPrefix}-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
+ -- ----------------------------------------------------------------------------
+ call generateRbacRestrictedView('${rawTableName}',
+ $orderBy$
+ ${orderBy}
+ $orderBy$,
+ $updates$
+ ${updates}
+ $updates$);
+ --//
+
+ """,
+ with("liquibaseTagPrefix", liquibaseTagPrefix),
+ with("orderBy", indented(2, rbacDef.getOrderBySqlExpression().sql)),
+ with("updates", indented(2, rbacDef.getUpdatableColumns().stream()
+ .map(c -> c + " = new." + c)
+ .collect(joining(",\n")))),
+ with("rawTableName", rawTableName));
+ }
+}
diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacRoleDescriptorsGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacRoleDescriptorsGenerator.java
new file mode 100644
index 00000000..dab3ab01
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacRoleDescriptorsGenerator.java
@@ -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));
+ }
+}
diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java
new file mode 100644
index 00000000..cb048455
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java
@@ -0,0 +1,1087 @@
+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.relation.HsOfficeRelationEntity;
+import net.hostsharing.hsadminng.hs.office.sepamandate.HsOfficeSepaMandateEntity;
+import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
+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.Nullable.NOT_NULL;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.Part.AUTO_FETCH;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.directlyFetchedByDependsOnColumn;
+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 userDefs = new LinkedHashSet<>();
+ private final Set roleDefs = new LinkedHashSet<>();
+ private final Set permDefs = new LinkedHashSet<>();
+ private final Map 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 updatableColumns = new LinkedHashSet<>();
+ private final Set grantDefs = new LinkedHashSet<>();
+
+ private SQL identityViewSqlQuery;
+ private SQL orderBySqlExpression;
+ private EntityAlias rootEntityAliasProxy;
+ private RbacRoleDefinition previousRoleDef;
+
+ /** Crates an RBAC definition template for the given entity class and defining the given alias.
+ *
+ * @param alias
+ * an alias name for this entity/table, which can be used in further grants
+ *
+ * @param entityClass
+ * the Java class for which this RBAC definition is to be defined
+ * (the class to which the calling method belongs)
+ *
+ * @return
+ * the newly created RBAC definition template
+ *
+ * @param
+ * a JPA entity class extending RbacObject
+ */
+ public static RbacView rbacViewFor(final String alias, final Class 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"));
+ }
+
+ /**
+ * Specifies, which columns of the restricted view are updatable at all.
+ *
+ * @param columnNames
+ * A list of the updatable columns.
+ *
+ * @return
+ * the `this` instance itself to allow chained calls.
+ */
+ public RbacView withUpdatableColumns(final String... columnNames) {
+ Collections.addAll(updatableColumns, columnNames);
+ verifyVersionColumnExists();
+ return this;
+ }
+
+ /** Specifies the SQL query which creates the identity view for this entity.
+ *
+ *
An identity view is a view which maps an objectUuid to an idName.
+ * The idName should be a human-readable representation of the row, but as short as possible.
+ * The idName must only consist of letters (A-Z, a-z), digits (0-9), dash (-), dot (.) and unserscore '_'.
+ * It's used to create the object-specific-role-names like test_customer#abc:ADMIN - here 'abc' is the idName.
+ * The idName not necessarily unique in a table, but it should be avoided.
+ *
+ *
+ * @param sqlExpression
+ * Either specify an SQL projection (the part between SELECT and FROM), e.g. `SQL.projection("columnName")
+ * or the whole SELECT query returning the uuid and idName columns,
+ * e.g. `SQL.query("SELECT ... AS uuid, ... AS idName FROM ... JOIN ...").
+ * Only add really important columns, just enough to create a short human-readable representation.
+ *
+ * @return
+ * the `this` instance itself to allow chained calls.
+ */
+ public RbacView withIdentityView(final SQL sqlExpression) {
+ this.identityViewSqlQuery = sqlExpression;
+ return this;
+ }
+
+ /**
+ * Specifies a ORDER BY clause for the generated restricted view.
+ *
+ *
A restricted view is generated, no matter if the order was specified or not.
+ *
+ * @param orderBySqlExpression
+ * That's the part behind `ORDER BY`, e.g. `SQL.expression("prefix").
+ *
+ * @return
+ * the `this` instance itself to allow chained calls.
+ */
+ public RbacView withRestrictedViewOrderBy(final SQL orderBySqlExpression) {
+ this.orderBySqlExpression = orderBySqlExpression;
+ return this;
+ }
+
+ /**
+ * Specifies that the given role (OWNER, ADMIN, ...) is to be created for new/updated roles in this table.
+ *
+ * @param role
+ * OWNER, ADMIN, AGENT etc.
+ * @param with
+ * a lambda which receives the created role to create grants and permissions to and from the newly created role,
+ * e.g. the owning user, incoming superroles, outgoing subroles
+ * @return
+ * the `this` instance itself to allow chained calls.
+ */
+ public RbacView createRole(final Role role, final Consumer with) {
+ final RbacRoleDefinition newRoleDef = findRbacRole(rootEntityAlias, role).toCreate();
+ with.accept(newRoleDef);
+ previousRoleDef = newRoleDef;
+ return this;
+ }
+
+ /**
+ * Specifies that the given role (OWNER, ADMIN, ...) is to be created for new/updated roles in this table,
+ * which is becomes sub-role of the previously created role.
+ *
+ * @param role
+ * OWNER, ADMIN, AGENT etc.
+ * @return
+ * the `this` instance itself to allow chained calls.
+ */
+ public RbacView createSubRole(final Role role) {
+ final RbacRoleDefinition newRoleDef = findRbacRole(rootEntityAlias, role).toCreate();
+ findOrCreateGrantDef(newRoleDef, previousRoleDef).toCreate();
+ previousRoleDef = newRoleDef;
+ return this;
+ }
+
+
+ /**
+ * Specifies that the given role (OWNER, ADMIN, ...) is to be created for new/updated roles in this table,
+ * which is becomes sub-role of the previously created role.
+ *
+ * @param role
+ * OWNER, ADMIN, AGENT etc.
+ * @param with
+ * a lambda which receives the created role to create grants and permissions to and from the newly created role,
+ * e.g. the owning user, incoming superroles, outgoing subroles
+ * @return
+ * the `this` instance itself to allow chained calls.
+ */
+ public RbacView createSubRole(final Role role, final Consumer with) {
+ final RbacRoleDefinition newRoleDef = findRbacRole(rootEntityAlias, role).toCreate();
+ findOrCreateGrantDef(newRoleDef, previousRoleDef).toCreate();
+ with.accept(newRoleDef);
+ previousRoleDef = newRoleDef;
+ return this;
+ }
+
+ /**
+ * Specifies that the given permission is to be created for each new row in the target table.
+ *
+ *
Grants to permissions created by this method have to be specified separately,
+ * often it's easier to read to use createRole/createSubRole and use with.permission(...).
+ *
+ * @param permission
+ * e.g. INSERT, SELECT, UPDATE, DELETE
+ *
+ * @return
+ * the newly created permission definition
+ */
+ public RbacPermissionDefinition createPermission(final Permission permission) {
+ return createPermission(rootEntityAlias, permission);
+ }
+
+ /**
+ * Specifies that the given permission is to be created for each new row in the target table,
+ * but for another table, e.g. a table with details data with different access rights.
+ *
+ *
Grants to permissions created by this method have to be specified separately,
+ * often it's easier to read to use createRole/createSubRole and use with.permission(...).
+ *
+ * @param entityAliasName
+ * A previously defined entity alias name.
+ *
+ * @param permission
+ * e.g. INSERT, SELECT, UPDATE, DELETE
+ *
+ * @return
+ * the newly created permission definition
+ */
+ 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 RbacView declarePlaceholderEntityAliases(final String... aliasNames) {
+ for (String alias : aliasNames) {
+ entityAliases.put(alias, new EntityAlias(alias));
+ }
+ return this;
+ }
+
+ /**
+ * Imports the RBAC template from the given entity class and defines an alias name for it.
+ * This method is especially for proxy-entities, if the root entity does not have its own
+ * roles, a proxy-entity can be specified and its roles can be used instead.
+ *
+ * @param aliasName
+ * An alias name for the entity class. The same entity class can be imported multiple times,
+ * if multiple references to its table exist, then distinct alias names habe to be defined.
+ *
+ * @param entityClass
+ * A JPA entity class extending RbacObject which also implements an `rbac` method returning
+ * its RBAC specification.
+ *
+ * @param fetchSql
+ * An SQL SELECT statement which fetches the referenced row. Use `${REF}` to speficiy the
+ * newly created or updated row (will be replaced by NEW/OLD from the trigger method).
+ *
+ * @param dependsOnColum
+ * The column, usually containing an uuid, on which this other table depends.
+ *
+ * @return
+ * the newly created permission definition
+ *
+ * @param
+ * a JPA entity class extending RbacObject
+ */
+ public RbacView importRootEntityAliasProxy(
+ final String aliasName,
+ final Class extends RbacObject> 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, NOT_NULL);
+ return this;
+ }
+
+ /**
+ * Imports the RBAC template from the given entity class and defines an alias name for it.
+ * This method is especially to declare sub-entities, e.g. details to a main object.
+ *
+ * @see {@link}
+ *
+ * @return
+ * the newly created permission definition
+ *
+ * @param
+ * a JPA entity class extending RbacObject
+ */
+ public RbacView importSubEntityAlias(
+ final String aliasName, final Class extends RbacObject> entityClass,
+ final SQL fetchSql, final Column dependsOnColum) {
+ importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, true, NOT_NULL);
+ return this;
+ }
+
+ /**
+ * Imports the RBAC template from the given entity class and defines an anlias name for it.
+ *
+ * @param aliasName
+ * An alias name for the entity class. The same entity class can be imported multiple times,
+ * if multiple references to its table exist, then distinct alias names habe to be defined.
+ *
+ * @param entityClass
+ * A JPA entity class extending RbacObject which also implements an `rbac` method returning
+ * its RBAC specification.
+ *
+ * @param fetchSql
+ * An SQL SELECT statement which fetches the referenced row. Use `${REF}` to speficiy the
+ * newly created or updated row (will be replaced by NEW/OLD from the trigger method).
+ *
+ * @param dependsOnColum
+ * The column, usually containing an uuid, on which this other table depends.
+ *
+ * @param nullable
+ * Specifies whether the dependsOnColum is nullable or not.
+ *
+ * @return
+ * the newly created permission definition
+ *
+ * @param
+ * a JPA entity class extending RbacObject
+ */
+ public RbacView importEntityAlias(
+ final String aliasName, final Class extends RbacObject> entityClass,
+ final Column dependsOnColum, final SQL fetchSql, final Nullable nullable) {
+ importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, false, nullable);
+ return this;
+ }
+
+ // TODO: remove once it's not used in HsOffice...Entity anymore
+ public RbacView importEntityAlias(
+ final String aliasName, final Class extends RbacObject> entityClass,
+ final Column dependsOnColum) {
+ importEntityAliasImpl(aliasName, entityClass, directlyFetchedByDependsOnColumn(), dependsOnColum, false, null);
+ return this;
+ }
+
+ private EntityAlias importEntityAliasImpl(
+ final String aliasName, final Class extends RbacObject> entityClass,
+ final SQL fetchSql, final Column dependsOnColum, boolean asSubEntity, final Nullable nullable) {
+ final var entityAlias = new EntityAlias(aliasName, entityClass, fetchSql, dependsOnColum, asSubEntity, nullable);
+ 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);
+ }
+ }
+
+ /**
+ * Starts declaring a grant to a given role.
+ *
+ * @param entityAlias
+ * A previously speciried entity alias name.
+ * @param role
+ * OWNER, ADMIN, AGENT, ...
+ * @return
+ * a grant builder
+ */
+ 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 Permission perm) {
+ final var forTable = rootEntityAlias.getRawTableName();
+ findOrCreateGrantDef(findRbacPerm(rootEntityAlias, perm, forTable), superRoleDef).toCreate();
+ return RbacView.this;
+ }
+
+ }
+
+ public enum Nullable {
+ NOT_NULL, // DEFAULT
+ NULLABLE
+ }
+
+ @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);
+ }
+
+ /**
+ * Grants the permission under definition to the given role.
+ *
+ * @param entityAlias
+ * A previously declared entity alias name.
+ * @param role
+ * OWNER, ADMIN, ...
+ * @return
+ * The RbacView specification to which this permission definition belongs.
+ */
+ 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;
+ }
+
+ /**
+ * Specifies which user becomes the owner of newly created objects.
+ * @param userRole
+ * GLOBAL_ADMIN, CREATOR, ...
+ * @return
+ * The grant definition for further chained calls.
+ */
+ public RbacGrantDefinition owningUser(final RbacUserReference.UserRole userRole) {
+ return grantRoleToUser(this, findUserRef(userRole));
+ }
+
+ /**
+ * Specifies which permission is to be created for newly created objects.
+ * @param permission
+ * INSERT, SELECT, ...
+ * @return
+ * The grant definition for further chained calls.
+ */
+ public RbacGrantDefinition permission(final Permission permission) {
+ return grantPermissionToRole(createPermission(entityAlias, permission), this);
+ }
+
+ /**
+ * Specifies in incoming super role which gets granted the role under definition.
+ *
+ *
Incoming means an incoming grant arrow in our grant-diagrams.
+ * Super-role means that it's the role to which another role is granted.
+ * Both means actually the same, just in different aspects.
+ *
+ * @param entityAlias
+ * A previously declared entity alias name.
+ * @param role
+ * OWNER, ADMIN, ...
+ * @return
+ * The grant definition for further chained calls.
+ */
+ public RbacGrantDefinition incomingSuperRole(final String entityAlias, final Role role) {
+ final var incomingSuperRole = findRbacRole(entityAlias, role);
+ return grantSubRoleToSuperRole(this, incomingSuperRole);
+ }
+
+ /**
+ * Specifies in outgoing sub role which gets granted the role under definition.
+ *
+ *
Outgoing means an outgoing grant arrow in our grant-diagrams.
+ * Sub-role means which is granted to another role.
+ * Both means actually the same, just in different aspects.
+ *
+ * @param entityAlias
+ * A previously declared entity alias name.
+ * @param role
+ * OWNER, ADMIN, ...
+ * @return
+ * The grant definition for further chained calls.
+ */
+ 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, Nullable nullable) {
+
+ public EntityAlias(final String aliasName) {
+ this(aliasName, null, null, null, false, null);
+ }
+
+ public EntityAlias(final String aliasName, final Class extends RbacObject> entityClass) {
+ this(aliasName, entityClass, null, null, false, null);
+ }
+
+ 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);
+ };
+ }
+
+ boolean isFetchedByDirectForeignKey() {
+ return fetchSql != null && fetchSql.part == AUTO_FETCH;
+ }
+
+ 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 enum Role {
+
+ OWNER,
+ ADMIN,
+ AGENT,
+ TENANT,
+ REFERRER,
+
+ @Deprecated
+ GUEST;
+
+ @Override
+ public String toString() {
+ return ":" + name();
+ }
+ }
+
+ public enum Permission {
+ INSERT,
+ DELETE,
+ UPDATE,
+ SELECT;
+
+ @Override
+ public String toString() {
+ return ":" + name();
+ }
+ }
+
+ 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 and `${columns}` for the projection.
+ *
+ *
The query must define the entity alias name of the fetched table
+ * as its alias for, so it can be used in the generated projection (the columns between
+ * `SELECT` and `FROM`.
+ *
+ *
`${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`.
+ *
+ *
`${columns}` is going to be replaced by the columns which are needed for the query,
+ * e.g. `*` or `uuid`.
+ *
+ * @param sql an SQL SELECT expression (not ending with ';)
+ * @return the wrapped SQL expression
+ */
+ public static SQL fetchedBySql(final String sql) {
+ if ( !sql.startsWith("SELECT ${columns}") ) {
+ throw new IllegalArgumentException("SQL SELECT expression must start with 'SELECT ${columns}', but is: " + 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 directlyFetchedByDependsOnColumn() {
+ return new SQL(null, 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 outerAliasNames;
+
+ AliasNameMapper(final RbacView importedRbacView, final String outerAliasName, final Set 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;
+ }
+ }
+
+ private static void generateRbacView(final Class extends RbacObject> 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("WARNING: no main method in: " + c.getName() + " => no RBAC rules generated");
+ }
+ }
+
+ /**
+ * This main method generates the RbacViews (PostgreSQL+diagram) for all given entity classes.
+ */
+ public static void main(String[] args) {
+ Stream.of(
+ TestCustomerEntity.class,
+ TestPackageEntity.class,
+ TestDomainEntity.class,
+ HsOfficePersonEntity.class,
+ HsOfficePartnerEntity.class,
+ HsOfficePartnerDetailsEntity.class,
+ HsOfficeBankAccountEntity.class,
+ HsOfficeDebitorEntity.class,
+ HsOfficeRelationEntity.class,
+ HsOfficeCoopAssetsTransactionEntity.class,
+ HsOfficeContactEntity.class,
+ HsOfficeSepaMandateEntity.class,
+ HsOfficeCoopSharesTransactionEntity.class,
+ HsOfficeMembershipEntity.class
+ ).forEach(RbacView::generateRbacView);
+ }
+}
diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchartGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchartGenerator.java
new file mode 100644
index 00000000..c6e775c9
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchartGenerator.java
@@ -0,0 +1,162 @@
+package net.hostsharing.hsadminng.rbac.rbacdef;
+
+import lombok.SneakyThrows;
+import org.apache.commons.lang3.StringUtils;
+
+import java.nio.file.*;
+
+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, do not amend manually.
+
+ ```mermaid
+ %{flowchart}
+ ```
+ """
+ .replace("%{entityAlias}", rbacDef.getRootEntityAlias().aliasName())
+ .replace("%{flowchart}", flowchart.toString()),
+ StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+ System.out.println("Markdown-File: " + path.toAbsolutePath());
+ }
+}
diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java
new file mode 100644
index 00000000..5a3b2be8
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java
@@ -0,0 +1,53 @@
+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 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}, do not amend manually.
+ """,
+ with("generator", getClass().getSimpleName()),
+ 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()
+ .replace("\n\n\n", "\n\n")
+ .replace("-- ====", "\n-- ====")
+ .replace("\n\n--//", "\n--//");
+ }
+
+ @SneakyThrows
+ public void generateToChangeLog(final Path outputPath) {
+ Files.writeString(
+ outputPath,
+ toString(),
+ StandardOpenOption.CREATE,
+ StandardOpenOption.TRUNCATE_EXISTING);
+ System.out.println(outputPath.toAbsolutePath());
+ }
+}
diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java
new file mode 100644
index 00000000..484415f2
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java
@@ -0,0 +1,573 @@
+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.Optional.ofNullable;
+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 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);");
+ plPgSql.writeLn();
+ generateCreateRolesAndGrantsAfterInsert(plPgSql);
+ plPgSql.ensureSingleEmptyLine();
+ plPgSql.writeLn("call leaveTriggerForObjectUuid(NEW.uuid);");
+ });
+ plPgSql.writeLn("end; $$;");
+ plPgSql.writeLn();
+ }
+
+
+ private void generateSimplifiedUpdateTriggerFunction(final StringWriter plPgSql) {
+
+ final var updateConditions = updatableEntityAliases()
+ .map(RbacView.EntityAlias::dependsOnColumName)
+ .distinct()
+ .map(columnName -> "NEW." + columnName + " is distinct from OLD." + columnName)
+ .collect(joining( "\n or "));
+ 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 $$
+ begin
+
+ if ${updateConditions} then
+ delete from rbacgrants g where g.grantedbytriggerof = OLD.uuid;
+ call buildRbacSystemFor${simpleEntityName}(NEW);
+ end if;
+ end; $$;
+ """,
+ with("simpleEntityName", simpleEntityName),
+ with("rawTableName", rawTableName),
+ with("updateConditions", updateConditions));
+ }
+
+ 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(() -> {
+ referencedEntityAliases()
+ .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);");
+ plPgSql.writeLn();
+ generateUpdateRolesAndGrantsAfterUpdate(plPgSql);
+ plPgSql.ensureSingleEmptyLine();
+ plPgSql.writeLn("call leaveTriggerForObjectUuid(NEW.uuid);");
+ });
+ plPgSql.writeLn("end; $$;");
+ plPgSql.writeLn();
+ }
+
+ private boolean hasAnyUpdatableEntityAliases() {
+ return updatableEntityAliases().anyMatch(e -> true);
+ }
+
+ private boolean hasAnyUpdatableAndNullableEntityAliases() {
+ return updatableEntityAliases()
+ .filter(ea -> ea.nullable() == RbacView.Nullable.NULLABLE)
+ .anyMatch(e -> true);
+ }
+
+ private void generateCreateRolesAndGrantsAfterInsert(final StringWriter plPgSql) {
+ referencedEntityAliases()
+ .forEach((ea) -> {
+ generateFetchedVars(plPgSql, ea, NEW);
+ plPgSql.writeLn();
+ });
+
+ 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 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 updatableEntityAliases() {
+ return referencedEntityAliases()
+ .filter(ea -> rbacDef.getUpdatableColumns().contains(ea.dependsOnColum().column));
+ }
+
+ private void generateUpdateRolesAndGrantsAfterUpdate(final StringWriter plPgSql) {
+ plPgSql.ensureSingleEmptyLine();
+
+ referencedEntityAliases()
+ .forEach((ea) -> {
+ generateFetchedVars(plPgSql, ea, OLD);
+ generateFetchedVars(plPgSql, ea, NEW);
+ plPgSql.writeLn();
+ });
+
+ 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 void generateFetchedVars(
+ final StringWriter plPgSql,
+ final RbacView.EntityAlias ea,
+ final PostgresTriggerReference old) {
+ plPgSql.writeLn(
+ ea.fetchSql().sql + " INTO " + entityRefVar(old, ea) + ";",
+ with("columns", ea.aliasName() + ".*"),
+ with("ref", old.name()));
+ if (ea.nullable() == RbacView.Nullable.NOT_NULL) {
+ plPgSql.writeLn(
+ "assert ${entityRefVar}.uuid is not null, format('${entityRefVar} must not be null for ${REF}.${dependsOnColumn} = %s', ${REF}.${dependsOnColumn});",
+ with("entityRefVar", entityRefVar(old, ea)),
+ with("dependsOnColumn", ea.dependsOnColumName()),
+ with("ref", old.name()));
+ plPgSql.writeLn();
+ }
+ }
+
+ private void updateGrantsDependingOn(final StringWriter plPgSql, final String columnName) {
+ rbacDef.getGrantDefs().stream()
+ .filter(RbacView.RbacGrantDefinition::isToCreate)
+ .filter(g -> g.dependsOnColumn(columnName))
+ .filter(g -> !isInsertPermissionGrant(g))
+ .forEach(g -> {
+ plPgSql.ensureSingleEmptyLine();
+ plPgSql.writeLn(generateRevoke(g));
+ plPgSql.writeLn(generateGrant(g));
+ plPgSql.writeLn();
+ });
+ }
+
+ private static Boolean isInsertPermissionGrant(final RbacView.RbacGrantDefinition g) {
+ final var isInsertPermissionGrant = ofNullable(g.getPermDef()).map(RbacPermissionDefinition::getPermission).map(p -> p == INSERT).orElse(false);
+ return isInsertPermissionGrant;
+ }
+
+ 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}", getPerm(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 getPerm(final PostgresTriggerReference ref, final RbacPermissionDefinition permDef) {
+ return permRef("getPermissionId", 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.name());
+ }
+
+ 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().name())
+ + "(" + 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.name())));
+
+ generatePermissionsForRole(plPgSql, role);
+
+ generateIncomingSuperRolesForRole(plPgSql, role);
+
+ generateOutgoingSubRolesForRole(plPgSql, role);
+
+ generateUserGrantsForRole(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::name)
+ .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()))
+ .sorted().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()))
+ .sorted().toList();
+ plPgSql.indented(() ->
+ plPgSql.writeLn("outgoingSubRoles => array[" + joinArrayElements(arrayElements, 1) + "],\n"));
+ rbacGrants.removeAll(outgoingGrants);
+ }
+ }
+
+ private String joinArrayElements(final List arrayElements, final int singleLineLimit) {
+ return arrayElements.size() <= singleLineLimit
+ ? String.join(", ", arrayElements)
+ : arrayElements.stream().collect(joining(",\n\t", "\n\t", ""));
+ }
+
+ private Set 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 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 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 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");
+ if ( hasAnyUpdatableAndNullableEntityAliases() ) {
+ generateSimplifiedUpdateTriggerFunction(plPgSql);
+ } else {
+ 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().name());
+ }
+
+ private static String toTriggerReference(
+ final PostgresTriggerReference triggerRef,
+ final RbacView.EntityAlias entityAlias) {
+ return triggerRef.name().toLowerCase() + capitalize(entityAlias.aliasName());
+ }
+}
diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/StringWriter.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/StringWriter.java
new file mode 100644
index 00000000..fe4b0548
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/StringWriter.java
@@ -0,0 +1,121 @@
+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 indent(int levels) {
+ indentLevel += levels;
+ }
+
+ void unindent(int levels) {
+ indentLevel -= levels;
+ }
+
+ void indented(final Runnable indented) {
+ indent();
+ indented.run();
+ unindent();
+ }
+
+ void indented(int levels, final Runnable indented) {
+ indent(levels);
+ indented.run();
+ unindent(levels);
+ }
+
+ 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 int indentLevel, final String text) {
+ final var indentation = StringUtils.repeat(" ", indentLevel);
+ final var indented = stream(text.split("\n"))
+ .map(line -> line.trim().isBlank() ? "" : indentation + line)
+ .collect(joining("\n"));
+ return indented;
+ }
+
+ private String indented(final String text) {
+ if ( indentLevel == 0) {
+ return text;
+ }
+ return indented(indentLevel, text);
+ }
+
+ 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) {
+ 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;
+ }
+ }
+}
diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/package-info.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/package-info.java
new file mode 100644
index 00000000..2a193f2f
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/package-info.java
@@ -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.
diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantEntity.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantEntity.java
similarity index 89%
rename from src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantEntity.java
rename to src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantEntity.java
index 6dc8d1ce..f7b3cdf4 100644
--- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantEntity.java
+++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantEntity.java
@@ -1,13 +1,13 @@
package net.hostsharing.hsadminng.rbac.rbacgrant;
import lombok.*;
-import org.jetbrains.annotations.NotNull;
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;
@@ -20,7 +20,7 @@ import java.util.UUID;
@Immutable
@NoArgsConstructor
@AllArgsConstructor
-public class RawRbacGrantEntity {
+public class RawRbacGrantEntity implements Comparable {
@Id
private UUID uuid;
@@ -64,4 +64,9 @@ public class RawRbacGrantEntity {
// 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);
+ }
}
diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantRepository.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantRepository.java
similarity index 67%
rename from src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantRepository.java
rename to src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantRepository.java
index c7ac60ab..37828bdf 100644
--- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantRepository.java
+++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantRepository.java
@@ -8,4 +8,8 @@ import java.util.UUID;
public interface RawRbacGrantRepository extends Repository {
List findAll();
+
+ List findByAscendingUuid(UUID ascendingUuid);
+
+ List findByDescendantUuid(UUID refUuid);
}
diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantController.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantController.java
index 29bdc2d8..9dfaea74 100644
--- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantController.java
+++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantController.java
@@ -94,4 +94,17 @@ public class RbacGrantController implements RbacGrantsApi {
return ResponseEntity.noContent().build();
}
+
+// TODO: implement an endpoint to create a Mermaid flowchart with all grants of a given user
+// @GetMapping(
+// path = "/api/rbac/users/{userUuid}/grants",
+// produces = {"text/vnd.mermaid"})
+// @Transactional(readOnly = true)
+// public ResponseEntity allGrantsOfUserAsMermaid(
+// @RequestHeader(name = "current-user") String currentUser,
+// @RequestHeader(name = "assumed-roles", required = false) String assumedRoles) {
+// final var graph = RbacGrantsDiagramService.allGrantsToUser(currentUser);
+// return ResponseEntity.ok(graph);
+// }
+
}
diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantEntity.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantEntity.java
index a3abf528..c2f2d524 100644
--- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantEntity.java
+++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantEntity.java
@@ -59,9 +59,9 @@ public class RbacGrantEntity {
}
public String toDisplay() {
- return "{ grant role " + grantedRoleIdName +
- " to user " + granteeUserName +
- " by role " + grantedByRoleIdName +
+ return "{ grant role:" + grantedRoleIdName +
+ " to user:" + granteeUserName +
+ " by role:" + grantedByRoleIdName +
(assumed ? " and assume" : "") +
" }";
}
diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java
new file mode 100644
index 00000000..f8746eb5
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java
@@ -0,0 +1,228 @@
+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 {
+
+ private static final int GRANT_LIMIT = 500;
+
+ 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;
+
+ public static final EnumSet ALL = EnumSet.allOf(Include.class);
+ public static final EnumSet ALL_TEST_ENTITY_RELATED = EnumSet.of(USERS, DETAILS, NOT_ASSUMED, TEST_ENTITIES, PERMISSIONS);
+ public static final EnumSet ALL_NON_TEST_ENTITY_RELATED = EnumSet.of(USERS, DETAILS, NOT_ASSUMED, NON_TEST_ENTITIES, PERMISSIONS);
+ }
+
+ @Autowired
+ private Context context;
+
+ @Autowired
+ private RawRbacGrantRepository rawGrantRepo;
+
+ @PersistenceContext
+ private EntityManager em;
+
+ public String allGrantsToCurrentUser(final EnumSet includes) {
+ final var graph = new LimitedHashSet();
+ for ( UUID subjectUuid: context.currentSubjectsUuids() ) {
+ traverseGrantsTo(graph, subjectUuid, includes);
+ }
+ return toMermaidFlowchart(graph, includes);
+ }
+
+ private void traverseGrantsTo(final Set graph, final UUID refUuid, final EnumSet 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 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 LimitedHashSet();
+ traverseGrantsFrom(graph, refUuid, includes);
+ return toMermaidFlowchart(graph, includes);
+ }
+
+ private void traverseGrantsFrom(final Set graph, final UUID refUuid, final EnumSet 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 graph, final EnumSet 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 " + cleanId(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 -> cleanId(g.getAscendantIdName())
+ + " -->" + (g.isAssumed() ? " " : "|XX| ")
+ + cleanId(g.getDescendantIdName()))
+ .sorted()
+ .collect(joining("\n"));
+
+ final var avoidCroppedNodeLabels = "%%{init:{'flowchart':{'htmlLabels':false}}}%%\n\n";
+ return (includes.contains(DETAILS) ? avoidCroppedNodeLabels : "")
+ + (graph.size() >= GRANT_LIMIT ? "%% too many grants, graph is cropped\n" : "")
+ + "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 "[" + cleanId(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 cleanId(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 cleanId(final String idName) {
+ return idName.replaceAll("@.*", "")
+ .replace("[", "").replace("]", "").replace("(", "").replace(")", "").replace(",", "");
+ }
+
+
+ class LimitedHashSet extends HashSet {
+
+ @Override
+ public boolean add(final T t) {
+ if (size() < GRANT_LIMIT ) {
+ return super.add(t);
+ } else {
+ return false;
+ }
+ }
+ }
+
+}
+
+record Node(String idName, UUID uuid) {
+
+}
diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacobject/RbacObject.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacobject/RbacObject.java
new file mode 100644
index 00000000..4d7646d1
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacobject/RbacObject.java
@@ -0,0 +1,8 @@
+package net.hostsharing.hsadminng.rbac.rbacobject;
+
+
+import java.util.UUID;
+
+public interface RbacObject {
+ UUID getUuid();
+}
diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleEntity.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleEntity.java
index 26528c8a..fa21785a 100644
--- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleEntity.java
+++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleEntity.java
@@ -34,6 +34,6 @@ public class RbacRoleEntity {
@Enumerated(EnumType.STRING)
private RbacRoleType roleType;
- @Formula("objectTable||'#'||objectIdName||'.'||roleType")
+ @Formula("objectTable||'#'||objectIdName||':'||roleType")
private String roleName;
}
diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleType.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleType.java
index 153344fa..e78e8836 100644
--- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleType.java
+++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleType.java
@@ -1,5 +1,5 @@
package net.hostsharing.hsadminng.rbac.rbacrole;
public enum RbacRoleType {
- owner, admin, agent, tenant, guest
+ OWNER, ADMIN, AGENT, TENANT, GUEST, REFERRER
}
diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserPermission.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserPermission.java
index ba251885..f29503c3 100644
--- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserPermission.java
+++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserPermission.java
@@ -8,8 +8,8 @@ public interface RbacUserPermission {
String getRoleName();
UUID getPermissionUuid();
String getOp();
+ String getOpTableName();
String getObjectTable();
String getObjectIdName();
UUID getObjectUuid();
-
}
diff --git a/src/main/java/net/hostsharing/hsadminng/stringify/Stringify.java b/src/main/java/net/hostsharing/hsadminng/stringify/Stringify.java
index 076f6209..8cdf433b 100644
--- a/src/main/java/net/hostsharing/hsadminng/stringify/Stringify.java
+++ b/src/main/java/net/hostsharing/hsadminng/stringify/Stringify.java
@@ -16,6 +16,7 @@ public final class Stringify {
private final Class clazz;
private final String name;
+ private Function idProp;
private final List> props = new ArrayList<>();
private String separator = ", ";
private Boolean quotedValues = null;
@@ -42,6 +43,11 @@ public final class Stringify {
}
}
+ public Stringify withIdProp(final Function getter) {
+ idProp = getter;
+ return this;
+ }
+
public Stringify withProp(final String propName, final Function getter) {
props.add(new Property<>(propName, getter));
return this;
@@ -64,7 +70,9 @@ public final class Stringify {
})
.map(propVal -> propName(propVal, "=") + optionallyQuoted(propVal))
.collect(Collectors.joining(separator));
- return name + "(" + propValues + ")";
+ return idProp != null
+ ? name + "(" + idProp.apply(object) + ": " + propValues + ")"
+ : name + "(" + propValues + ")";
}
public Stringify withSeparator(final String separator) {
diff --git a/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerController.java b/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerController.java
index 1bd000ba..67607c83 100644
--- a/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerController.java
+++ b/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerController.java
@@ -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> 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}")
diff --git a/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java b/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java
index 1f2bb0e1..19340440 100644
--- a/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java
+++ b/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java
@@ -4,17 +4,27 @@ import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
+import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
+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 RbacObject {
@Id
@GeneratedValue
@@ -25,4 +35,28 @@ 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")
+ .toRole("global", ADMIN).grantPermission(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("2-test/201-test-customer/2013-test-customer-rbac");
+ }
}
diff --git a/src/main/java/net/hostsharing/hsadminng/test/dom/TestDomainEntity.java b/src/main/java/net/hostsharing/hsadminng/test/dom/TestDomainEntity.java
new file mode 100644
index 00000000..b6d659c5
--- /dev/null
+++ b/src/main/java/net/hostsharing/hsadminng/test/dom/TestDomainEntity.java
@@ -0,0 +1,72 @@
+package net.hostsharing.hsadminng.test.dom;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
+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.Nullable.NOT_NULL;
+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.directlyFetchedByDependsOnColumn;
+import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
+
+@Entity
+@Table(name = "test_domain_rv")
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+public class TestDomainEntity implements RbacObject {
+
+ @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"),
+ directlyFetchedByDependsOnColumn(),
+ NOT_NULL)
+ .toRole("package", ADMIN).grantPermission(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("2-test/203-test-domain/2033-test-domain-rbac");
+ }
+}
diff --git a/src/main/java/net/hostsharing/hsadminng/test/pac/TestPackageEntity.java b/src/main/java/net/hostsharing/hsadminng/test/pac/TestPackageEntity.java
index 8687666f..e8430863 100644
--- a/src/main/java/net/hostsharing/hsadminng/test/pac/TestPackageEntity.java
+++ b/src/main/java/net/hostsharing/hsadminng/test/pac/TestPackageEntity.java
@@ -4,18 +4,29 @@ import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
+import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
+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.Nullable.NOT_NULL;
+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 RbacObject {
@Id
@GeneratedValue
@@ -31,4 +42,32 @@ 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"),
+ directlyFetchedByDependsOnColumn(),
+ NOT_NULL)
+ .toRole("customer", ADMIN).grantPermission(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("2-test/202-test-package/2023-test-package-rbac");
+ }
}
diff --git a/src/main/resources/api-definition/hs-office/api-mappings.yaml b/src/main/resources/api-definition/hs-office/api-mappings.yaml
index 11778eb0..2403e1e4 100644
--- a/src/main/resources/api-definition/hs-office/api-mappings.yaml
+++ b/src/main/resources/api-definition/hs-office/api-mappings.yaml
@@ -23,7 +23,7 @@ map:
null: org.openapitools.jackson.nullable.JsonNullable
/api/hs/office/persons/{personUUID}:
null: org.openapitools.jackson.nullable.JsonNullable
- /api/hs/office/relationships/{relationshipUUID}:
+ /api/hs/office/relations/{relationUUID}:
null: org.openapitools.jackson.nullable.JsonNullable
/api/hs/office/bankaccounts/{bankAccountUUID}:
null: org.openapitools.jackson.nullable.JsonNullable
diff --git a/src/main/resources/api-definition/hs-office/hs-office-debitor-schemas.yaml b/src/main/resources/api-definition/hs-office/hs-office-debitor-schemas.yaml
index 26736fac..dcf3df93 100644
--- a/src/main/resources/api-definition/hs-office/hs-office-debitor-schemas.yaml
+++ b/src/main/resources/api-definition/hs-office/hs-office-debitor-schemas.yaml
@@ -9,6 +9,8 @@ components:
uuid:
type: string
format: uuid
+ debitorRel:
+ $ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelation'
debitorNumber:
type: integer
format: int32
@@ -21,8 +23,6 @@ components:
maximum: 99
partner:
$ref: './hs-office-partner-schemas.yaml#/components/schemas/HsOfficePartner'
- billingContact:
- $ref: './hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContact'
billable:
type: boolean
vatId:
@@ -43,7 +43,7 @@ components:
HsOfficeDebitorPatch:
type: object
properties:
- billingContactUuid:
+ debitorRelUuid:
type: string
format: uuid
nullable: true
@@ -75,14 +75,11 @@ components:
HsOfficeDebitorInsert:
type: object
properties:
- partnerUuid:
+ debitorRel:
+ $ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationInsert'
+ debitorRelUuid:
type: string
format: uuid
- nullable: false
- billingContactUuid:
- type: string
- format: uuid
- nullable: false
debitorNumberSuffix:
type: integer
format: int8
@@ -105,9 +102,7 @@ components:
defaultPrefix:
type: string
pattern: '^[a-z]{3}$'
-
required:
- - partnerUuid
- - billingContactUuid
+ - debitorNumberSuffix
- defaultPrefix
- billable
diff --git a/src/main/resources/api-definition/hs-office/hs-office-membership-schemas.yaml b/src/main/resources/api-definition/hs-office/hs-office-membership-schemas.yaml
index 163f6f34..02fba043 100644
--- a/src/main/resources/api-definition/hs-office/hs-office-membership-schemas.yaml
+++ b/src/main/resources/api-definition/hs-office/hs-office-membership-schemas.yaml
@@ -46,10 +46,6 @@ components:
HsOfficeMembershipPatch:
type: object
properties:
- mainDebitorUuid:
- type: string
- format: uuid
- nullable: true
validTo:
type: string
format: date
@@ -69,10 +65,6 @@ components:
type: string
format: uuid
nullable: false
- mainDebitorUuid:
- type: string
- format: uuid
- nullable: false
memberNumberSuffix:
type: string
minLength: 2
@@ -95,7 +87,6 @@ components:
required:
- partnerUuid
- memberNumberSuffix
- - mainDebitorUuid
- validFrom
- membershipFeeBillable
additionalProperties: false
diff --git a/src/main/resources/api-definition/hs-office/hs-office-partner-schemas.yaml b/src/main/resources/api-definition/hs-office/hs-office-partner-schemas.yaml
index a473bd49..89b22241 100644
--- a/src/main/resources/api-definition/hs-office/hs-office-partner-schemas.yaml
+++ b/src/main/resources/api-definition/hs-office/hs-office-partner-schemas.yaml
@@ -14,10 +14,8 @@ components:
format: int8
minimum: 10000
maximum: 99999
- person:
- $ref: './hs-office-person-schemas.yaml#/components/schemas/HsOfficePerson'
- contact:
- $ref: './hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContact'
+ partnerRel:
+ $ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelation'
details:
$ref: '#/components/schemas/HsOfficePartnerDetails'
@@ -52,11 +50,7 @@ components:
HsOfficePartnerPatch:
type: object
properties:
- personUuid:
- type: string
- format: uuid
- nullable: true
- contactUuid:
+ partnerRelUuid:
type: string
format: uuid
nullable: true
@@ -96,38 +90,31 @@ components:
format: int8
minimum: 10000
maximum: 99999
- partnerRole:
- $ref: '#/components/schemas/HsOfficePartnerRoleInsert'
- personUuid:
- type: string
- format: uuid
- contactUuid:
- type: string
- format: uuid
+ partnerRel:
+ $ref: '#/components/schemas/HsOfficePartnerRelInsert'
details:
$ref: '#/components/schemas/HsOfficePartnerDetailsInsert'
required:
- partnerNumber
- - personUuid
- - contactUuid
+ - partnerRel
- details
- HsOfficePartnerRoleInsert:
+ HsOfficePartnerRelInsert:
type: object
nullable: false
properties:
- relAnchorUuid:
+ anchorUuid:
type: string
format: uuid
- relHolderUuid:
+ holderUuid:
type: string
format: uuid
contactUuid:
type: string
format: uuid
required:
- - relAnchorUuid
- - relHolderUuid
+ - anchorUuid
+ - holderUuid
- relContactUuid
HsOfficePartnerDetailsInsert:
diff --git a/src/main/resources/api-definition/hs-office/hs-office-relationship-schemas.yaml b/src/main/resources/api-definition/hs-office/hs-office-relation-schemas.yaml
similarity index 72%
rename from src/main/resources/api-definition/hs-office/hs-office-relationship-schemas.yaml
rename to src/main/resources/api-definition/hs-office/hs-office-relation-schemas.yaml
index af5e5f86..7b316b40 100644
--- a/src/main/resources/api-definition/hs-office/hs-office-relationship-schemas.yaml
+++ b/src/main/resources/api-definition/hs-office/hs-office-relation-schemas.yaml
@@ -3,37 +3,37 @@ components:
schemas:
- HsOfficeRelationshipType:
+ HsOfficeRelationType:
type: string
enum:
- UNKNOWN
- PARTNER
- EX_PARTNER
- - REPRESENTATIVE,
+ - DEBITOR
+ - REPRESENTATIVE
- VIP_CONTACT
- - ACCOUNTING,
- OPERATIONS
- SUBSCRIBER
- HsOfficeRelationship:
+ HsOfficeRelation:
type: object
properties:
uuid:
type: string
format: uuid
- relAnchor:
+ anchor:
$ref: './hs-office-person-schemas.yaml#/components/schemas/HsOfficePerson'
- relHolder:
+ holder:
$ref: './hs-office-person-schemas.yaml#/components/schemas/HsOfficePerson'
- relType:
+ type:
type: string
- relMark:
+ mark:
type: string
nullable: true
contact:
$ref: './hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContact'
- HsOfficeRelationshipPatch:
+ HsOfficeRelationPatch:
type: object
properties:
contactUuid:
@@ -41,25 +41,26 @@ components:
format: uuid
nullable: true
- HsOfficeRelationshipInsert:
+ HsOfficeRelationInsert:
type: object
properties:
- relAnchorUuid:
+ anchorUuid:
type: string
format: uuid
- relHolderUuid:
+ holderUuid:
type: string
format: uuid
- relType:
+ type:
type: string
nullable: true
- relMark:
+ mark:
type: string
+ nullable: true
contactUuid:
type: string
format: uuid
required:
- - relAnchorUuid
- - relHolderUuid
- - relType
- - relContactUuid
+ - anchorUuid
+ - holderUuid
+ - type
+ - contactUuid
diff --git a/src/main/resources/api-definition/hs-office/hs-office-relationships-with-uuid.yaml b/src/main/resources/api-definition/hs-office/hs-office-relations-with-uuid.yaml
similarity index 64%
rename from src/main/resources/api-definition/hs-office/hs-office-relationships-with-uuid.yaml
rename to src/main/resources/api-definition/hs-office/hs-office-relations-with-uuid.yaml
index d3b9605e..83b9cf3e 100644
--- a/src/main/resources/api-definition/hs-office/hs-office-relationships-with-uuid.yaml
+++ b/src/main/resources/api-definition/hs-office/hs-office-relations-with-uuid.yaml
@@ -1,25 +1,25 @@
get:
tags:
- - hs-office-relationships
- description: 'Fetch a single person relationship by its uuid, if visible for the current subject.'
- operationId: getRelationshipByUuid
+ - hs-office-relations
+ description: 'Fetch a single person relation by its uuid, if visible for the current subject.'
+ operationId: getRelationByUuid
parameters:
- $ref: './auth.yaml#/components/parameters/currentUser'
- $ref: './auth.yaml#/components/parameters/assumedRoles'
- - name: relationshipUUID
+ - name: relationUUID
in: path
required: true
schema:
type: string
format: uuid
- description: UUID of the relationship to fetch.
+ description: UUID of the relation to fetch.
responses:
"200":
description: OK
content:
'application/json':
schema:
- $ref: './hs-office-relationship-schemas.yaml#/components/schemas/HsOfficeRelationship'
+ $ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelation'
"401":
$ref: './error-responses.yaml#/components/responses/Unauthorized'
@@ -28,13 +28,13 @@ get:
patch:
tags:
- - hs-office-relationships
- description: 'Updates a single person relationship by its uuid, if permitted for the current subject.'
- operationId: patchRelationship
+ - hs-office-relations
+ description: 'Updates a single person relation by its uuid, if permitted for the current subject.'
+ operationId: patchRelation
parameters:
- $ref: './auth.yaml#/components/parameters/currentUser'
- $ref: './auth.yaml#/components/parameters/assumedRoles'
- - name: relationshipUUID
+ - name: relationUUID
in: path
required: true
schema:
@@ -44,14 +44,14 @@ patch:
content:
'application/json':
schema:
- $ref: './hs-office-relationship-schemas.yaml#/components/schemas/HsOfficeRelationshipPatch'
+ $ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationPatch'
responses:
"200":
description: OK
content:
'application/json':
schema:
- $ref: './hs-office-relationship-schemas.yaml#/components/schemas/HsOfficeRelationship'
+ $ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelation'
"401":
$ref: './error-responses.yaml#/components/responses/Unauthorized'
"403":
@@ -59,19 +59,19 @@ patch:
delete:
tags:
- - hs-office-relationships
- description: 'Delete a single person relationship by its uuid, if permitted for the current subject.'
- operationId: deleteRelationshipByUuid
+ - hs-office-relations
+ description: 'Delete a single person relation by its uuid, if permitted for the current subject.'
+ operationId: deleteRelationByUuid
parameters:
- $ref: './auth.yaml#/components/parameters/currentUser'
- $ref: './auth.yaml#/components/parameters/assumedRoles'
- - name: relationshipUUID
+ - name: relationUUID
in: path
required: true
schema:
type: string
format: uuid
- description: UUID of the relationship to delete.
+ description: UUID of the relation to delete.
responses:
"204":
description: No Content
diff --git a/src/main/resources/api-definition/hs-office/hs-office-relationships.yaml b/src/main/resources/api-definition/hs-office/hs-office-relations.yaml
similarity index 61%
rename from src/main/resources/api-definition/hs-office/hs-office-relationships.yaml
rename to src/main/resources/api-definition/hs-office/hs-office-relations.yaml
index 2d7ed2fd..0c98075f 100644
--- a/src/main/resources/api-definition/hs-office/hs-office-relationships.yaml
+++ b/src/main/resources/api-definition/hs-office/hs-office-relations.yaml
@@ -1,9 +1,9 @@
get:
- summary: Returns a list of (optionally filtered) person relationships for a given person.
- description: Returns the list of (optionally filtered) person relationships of a given person and which are visible to the current user or any of it's assumed roles.
+ summary: Returns a list of (optionally filtered) person relations for a given person.
+ description: Returns the list of (optionally filtered) person relations of a given person and which are visible to the current user or any of it's assumed roles.
tags:
- - hs-office-relationships
- operationId: listRelationships
+ - hs-office-relations
+ operationId: listRelations
parameters:
- $ref: './auth.yaml#/components/parameters/currentUser'
- $ref: './auth.yaml#/components/parameters/assumedRoles'
@@ -13,13 +13,13 @@ get:
schema:
type: string
format: uuid
- description: Prefix of name properties from relHolder or contact to filter the results.
- - name: relationshipType
+ description: Prefix of name properties from holder or contact to filter the results.
+ - name: relationType
in: query
required: false
schema:
- $ref: './hs-office-relationship-schemas.yaml#/components/schemas/HsOfficeRelationshipType'
- description: Prefix of name properties from relHolder or contact to filter the results.
+ $ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationType'
+ description: Prefix of name properties from holder or contact to filter the results.
responses:
"200":
description: OK
@@ -28,17 +28,17 @@ get:
schema:
type: array
items:
- $ref: './hs-office-relationship-schemas.yaml#/components/schemas/HsOfficeRelationship'
+ $ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelation'
"401":
$ref: './error-responses.yaml#/components/responses/Unauthorized'
"403":
$ref: './error-responses.yaml#/components/responses/Forbidden'
post:
- summary: Adds a new person relationship.
+ summary: Adds a new person relation.
tags:
- - hs-office-relationships
- operationId: addRelationship
+ - hs-office-relations
+ operationId: addRelation
parameters:
- $ref: './auth.yaml#/components/parameters/currentUser'
- $ref: './auth.yaml#/components/parameters/assumedRoles'
@@ -46,7 +46,7 @@ post:
content:
'application/json':
schema:
- $ref: './hs-office-relationship-schemas.yaml#/components/schemas/HsOfficeRelationshipInsert'
+ $ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationInsert'
required: true
responses:
"201":
@@ -54,7 +54,7 @@ post:
content:
'application/json':
schema:
- $ref: './hs-office-relationship-schemas.yaml#/components/schemas/HsOfficeRelationship'
+ $ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelation'
"401":
$ref: './error-responses.yaml#/components/responses/Unauthorized'
"403":
diff --git a/src/main/resources/api-definition/hs-office/hs-office.yaml b/src/main/resources/api-definition/hs-office/hs-office.yaml
index f3110867..3bbc5c34 100644
--- a/src/main/resources/api-definition/hs-office/hs-office.yaml
+++ b/src/main/resources/api-definition/hs-office/hs-office.yaml
@@ -35,13 +35,13 @@ paths:
$ref: "./hs-office-persons-with-uuid.yaml"
- # Relationships
+ # Relations
- /api/hs/office/relationships:
- $ref: "./hs-office-relationships.yaml"
+ /api/hs/office/relations:
+ $ref: "./hs-office-relations.yaml"
- /api/hs/office/relationships/{relationshipUUID}:
- $ref: "./hs-office-relationships-with-uuid.yaml"
+ /api/hs/office/relations/{relationUUID}:
+ $ref: "./hs-office-relations-with-uuid.yaml"
# BankAccounts
diff --git a/src/main/resources/api-definition/rbac/rbac-role-schemas.yaml b/src/main/resources/api-definition/rbac/rbac-role-schemas.yaml
index 589c00b8..4e5b5f4d 100644
--- a/src/main/resources/api-definition/rbac/rbac-role-schemas.yaml
+++ b/src/main/resources/api-definition/rbac/rbac-role-schemas.yaml
@@ -19,8 +19,11 @@ components:
roleType:
type: string
enum:
- - owner
- - admin
- - tenant
+ - OWNER
+ - ADMIN
+ - AGENT
+ - TENANT
+ - REFERRER
+ - GUEST
roleName:
type: string
diff --git a/src/main/resources/db/changelog/000-template.sql b/src/main/resources/db/changelog/0-basis/000-template.sql
similarity index 100%
rename from src/main/resources/db/changelog/000-template.sql
rename to src/main/resources/db/changelog/0-basis/000-template.sql
diff --git a/src/main/resources/db/changelog/001-last-row-count.sql b/src/main/resources/db/changelog/0-basis/001-last-row-count.sql
similarity index 100%
rename from src/main/resources/db/changelog/001-last-row-count.sql
rename to src/main/resources/db/changelog/0-basis/001-last-row-count.sql
diff --git a/src/main/resources/db/changelog/002-int-to-var.sql b/src/main/resources/db/changelog/0-basis/002-int-to-var.sql
similarity index 100%
rename from src/main/resources/db/changelog/002-int-to-var.sql
rename to src/main/resources/db/changelog/0-basis/002-int-to-var.sql
diff --git a/src/main/resources/db/changelog/003-random-in-range.sql b/src/main/resources/db/changelog/0-basis/003-random-in-range.sql
similarity index 100%
rename from src/main/resources/db/changelog/003-random-in-range.sql
rename to src/main/resources/db/changelog/0-basis/003-random-in-range.sql
diff --git a/src/main/resources/db/changelog/004-jsonb-changes-delta.sql b/src/main/resources/db/changelog/0-basis/004-jsonb-changes-delta.sql
similarity index 100%
rename from src/main/resources/db/changelog/004-jsonb-changes-delta.sql
rename to src/main/resources/db/changelog/0-basis/004-jsonb-changes-delta.sql
diff --git a/src/main/resources/db/changelog/005-uuid-ossp-extension.sql b/src/main/resources/db/changelog/0-basis/005-uuid-ossp-extension.sql
similarity index 100%
rename from src/main/resources/db/changelog/005-uuid-ossp-extension.sql
rename to src/main/resources/db/changelog/0-basis/005-uuid-ossp-extension.sql
diff --git a/src/main/resources/db/changelog/006-numeric-hash-functions.sql b/src/main/resources/db/changelog/0-basis/006-numeric-hash-functions.sql
similarity index 86%
rename from src/main/resources/db/changelog/006-numeric-hash-functions.sql
rename to src/main/resources/db/changelog/0-basis/006-numeric-hash-functions.sql
index 5e2e2814..13d31931 100644
--- a/src/main/resources/db/changelog/006-numeric-hash-functions.sql
+++ b/src/main/resources/db/changelog/0-basis/006-numeric-hash-functions.sql
@@ -3,7 +3,7 @@
-- ============================================================================
-- NUMERIC-HASH-FUNCTIONS
---changeset hash:1 endDelimiter:--//
+--changeset numeric-hash-functions:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
create function bigIntHash(text) returns bigint as $$
diff --git a/src/main/resources/db/changelog/0-basis/007-table-columns.sql b/src/main/resources/db/changelog/0-basis/007-table-columns.sql
new file mode 100644
index 00000000..588defba
--- /dev/null
+++ b/src/main/resources/db/changelog/0-basis/007-table-columns.sql
@@ -0,0 +1,20 @@
+--liquibase formatted sql
+
+
+-- ============================================================================
+-- TABLE-COLUMNS-FUNCTION
+--changeset table-columns-function:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+create or replace function columnsNames( tableName text )
+ returns text
+ stable
+ language 'plpgsql' as $$
+declare columns text[];
+begin
+ columns := (select array(select column_name::text
+ from information_schema.columns
+ where table_name = tableName));
+ return array_to_string(columns, ', ');
+end; $$
+--//
diff --git a/src/main/resources/db/changelog/009-check-environment.sql b/src/main/resources/db/changelog/0-basis/009-check-environment.sql
similarity index 100%
rename from src/main/resources/db/changelog/009-check-environment.sql
rename to src/main/resources/db/changelog/0-basis/009-check-environment.sql
diff --git a/src/main/resources/db/changelog/010-context.sql b/src/main/resources/db/changelog/0-basis/010-context.sql
similarity index 83%
rename from src/main/resources/db/changelog/010-context.sql
rename to src/main/resources/db/changelog/0-basis/010-context.sql
index 4820cf9c..8ea73f45 100644
--- a/src/main/resources/db/changelog/010-context.sql
+++ b/src/main/resources/db/changelog/0-basis/010-context.sql
@@ -10,10 +10,10 @@
This function will be overwritten by later changesets.
*/
create procedure contextDefined(
- currentTask varchar,
- currentRequest varchar,
- currentUser varchar,
- assumedRoles varchar
+ currentTask varchar(127),
+ currentRequest text,
+ currentUser varchar(63),
+ assumedRoles varchar(1023)
)
language plpgsql as $$
begin
@@ -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(127),
+ currentRequest text = null,
+ currentUser varchar(63) = null,
+ assumedRoles varchar(1023) = null
)
language plpgsql as $$
begin
+ currentTask := coalesce(currentTask, '');
+ assert length(currentTask) <= 127, FORMAT('currentTask must not be longer than 127 characters: "%s"', currentTask);
+ assert length(currentTask) >= 12, FORMAT('currentTask must be at least 12 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) <= 1023, FORMAT('assumedRoles must not be longer than 1023 characters: "%s"', assumedRoles);
execute format('set local hsadminng.assumedRoles to %L', assumedRoles);
call contextDefined(currentTask, currentRequest, currentUser, assumedRoles);
@@ -54,11 +59,11 @@ end; $$;
Raises exception if not set.
*/
create or replace function currentTask()
- returns varchar(96)
+ returns varchar(127)
stable -- leakproof
language plpgsql as $$
declare
- currentTask varchar(96);
+ currentTask varchar(127);
begin
begin
currentTask := current_setting('hsadminng.currentTask');
@@ -82,11 +87,11 @@ end; $$;
Raises exception if not set.
*/
create or replace function currentRequest()
- returns varchar(512)
+ returns text
stable -- leakproof
language plpgsql as $$
declare
- currentRequest varchar(512);
+ currentRequest text;
begin
begin
currentRequest := current_setting('hsadminng.currentRequest');
@@ -130,22 +135,11 @@ end; $$;
or empty array, if not set.
*/
create or replace function assumedRoles()
- returns varchar(63)[]
+ returns varchar(1023)[]
stable -- leakproof
language plpgsql as $$
-declare
- currentSubject varchar(63);
begin
- begin
- currentSubject := current_setting('hsadminng.assumedRoles');
- exception
- when others then
- return array []::varchar[];
- end;
- if (currentSubject = '') then
- return array []::varchar[];
- end if;
- return string_to_array(currentSubject, ';');
+ return string_to_array(current_setting('hsadminng.assumedRoles', true), ';');
end; $$;
create or replace function cleanIdentifier(rawIdentifier varchar)
@@ -155,7 +149,7 @@ create or replace function cleanIdentifier(rawIdentifier varchar)
declare
cleanIdentifier varchar;
begin
- cleanIdentifier := regexp_replace(rawIdentifier, '[^A-Za-z0-9\-._:]+', '', 'g');
+ cleanIdentifier := regexp_replace(rawIdentifier, '[^A-Za-z0-9\-._]+', '', 'g');
return cleanIdentifier;
end; $$;
@@ -213,17 +207,17 @@ begin
end ; $$;
create or replace function currentSubjects()
- returns varchar(63)[]
+ returns varchar(1023)[]
stable -- leakproof
language plpgsql as $$
declare
- assumedRoles varchar(63)[];
+ assumedRoles varchar(1023)[];
begin
assumedRoles := assumedRoles();
if array_length(assumedRoles, 1) > 0 then
- return assumedRoles();
+ return assumedRoles;
else
- return array [currentUser()]::varchar(63)[];
+ return array [currentUser()]::varchar(1023)[];
end if;
end; $$;
diff --git a/src/main/resources/db/changelog/020-audit-log.sql b/src/main/resources/db/changelog/0-basis/020-audit-log.sql
similarity index 96%
rename from src/main/resources/db/changelog/020-audit-log.sql
rename to src/main/resources/db/changelog/0-basis/020-audit-log.sql
index 173e5741..4c2826e3 100644
--- a/src/main/resources/db/changelog/020-audit-log.sql
+++ b/src/main/resources/db/changelog/0-basis/020-audit-log.sql
@@ -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
- currentTask varchar(96) not null,
- currentRequest varchar(512) not null
+ assumedRoles varchar(1023) not null, -- not the uuids, because roles can be deleted
+ currentTask varchar(127) not null,
+ currentRequest text not null
);
create index on tx_context using brin (txTimestamp);
diff --git a/src/main/resources/db/changelog/057-rbac-role-builder.sql b/src/main/resources/db/changelog/057-rbac-role-builder.sql
deleted file mode 100644
index 81a81590..00000000
--- a/src/main/resources/db/changelog/057-rbac-role-builder.sql
+++ /dev/null
@@ -1,88 +0,0 @@
---liquibase formatted sql
-
--- ============================================================================
--- PERMISSIONS
---changeset rbac-role-builder-to-uuids:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-
-create or replace function toPermissionUuids(forObjectUuid uuid, permitOps RbacOp[])
- returns uuid[]
- language plpgsql
- strict as $$
-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
---changeset rbac-role-builder-create-role:1 endDelimiter:--//
--- -----------------------------------------------------------------
-
-create or replace function createRoleWithGrants(
- roleDescriptor RbacRoleDescriptor,
- permissions RbacOp[] = array[]::RbacOp[],
- incomingSuperRoles RbacRoleDescriptor[] = array[]::RbacRoleDescriptor[],
- outgoingSubRoles RbacRoleDescriptor[] = array[]::RbacRoleDescriptor[],
- userUuids uuid[] = array[]::uuid[],
- grantedByRole RbacRoleDescriptor = null
-)
- returns uuid
- called on null input
- language plpgsql as $$
-declare
- roleUuid uuid;
- superRoleUuid uuid;
- subRoleUuid uuid;
- userUuid uuid;
- grantedByRoleUuid uuid;
-begin
- roleUuid := createRole(roleDescriptor);
-
- if cardinality(permissions) >0 then
- call grantPermissionsToRole(roleUuid, toPermissionUuids(roleDescriptor.objectuuid, permissions));
- end if;
-
- foreach superRoleUuid in array toRoleUuids(incomingSuperRoles)
- loop
- call grantRoleToRole(roleUuid, superRoleUuid);
- end loop;
-
- foreach subRoleUuid in array toRoleUuids(outgoingSubRoles)
- loop
- call grantRoleToRole(subRoleUuid, roleUuid);
- 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';
- end if;
- grantedByRoleUuid := getRoleId(grantedByRole, 'fail');
- foreach userUuid in array userUuids
- loop
- call grantRoleToUserUnchecked(grantedByRoleUuid, roleUuid, userUuid);
- end loop;
- end if;
-
- return roleUuid;
-end; $$;
---//
-
diff --git a/src/main/resources/db/changelog/050-rbac-base.sql b/src/main/resources/db/changelog/1-rbac/1050-rbac-base.sql
similarity index 76%
rename from src/main/resources/db/changelog/050-rbac-base.sql
rename to src/main/resources/db/changelog/1-rbac/1050-rbac-base.sql
index fe2f30ae..6a3387fb 100644
--- a/src/main/resources/db/changelog/050-rbac-base.sql
+++ b/src/main/resources/db/changelog/1-rbac/1050-rbac-base.sql
@@ -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;
-$$;
-
--//
-- ============================================================================
@@ -187,7 +164,7 @@ end; $$;
*/
-create type RbacRoleType as enum ('owner', 'admin', 'agent', 'tenant', 'guest');
+create type RbacRoleType as enum ('OWNER', 'ADMIN', 'AGENT', 'TENANT', 'GUEST', 'REFERRER');
create table RbacRole
(
@@ -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)
@@ -254,7 +249,7 @@ declare
roleUuid uuid;
begin
-- TODO.refact: extract function toRbacRoleDescriptor(roleIdName varchar) + find other occurrences
- roleParts = overlay(roleIdName placing '#' from length(roleIdName) + 1 - strpos(reverse(roleIdName), '.'));
+ roleParts = overlay(roleIdName placing '#' from length(roleIdName) + 1 - strpos(reverse(roleIdName), ':'));
objectTableFromRoleIdName = split_part(roleParts, '#', 1);
objectNameFromRoleIdName = split_part(roleParts, '#', 2);
roleTypeFromRoleIdName = split_part(roleParts, '#', 3);
@@ -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,68 @@ 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,
- unique (objectUuid, op)
+ uuid uuid primary key references RbacReference (uuid) on delete cascade,
+ objectUuid uuid not null references RbacObject,
+ op RbacOp not null,
+ opTableName varchar(60)
);
+ALTER TABLE RbacPermission
+ ADD CONSTRAINT RbacPermission_uc UNIQUE NULLS NOT DISTINCT (objectUuid, op, opTableName);
+
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 is not distinct from 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 +428,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 +448,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,25 +469,46 @@ 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)
+create or replace function getPermissionId(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null)
returns uuid
- returns null on null input
stable -- leakproof
language plpgsql as $$
declare
- permissionId uuid;
+ permissionUuid uuid;
begin
- permissionId := findPermissionId(forObjectUuid, forOp);
- if permissionId is null and forOp <> '*' then
- permissionId := findPermissionId(forObjectUuid, '*');
- end if;
- return permissionId;
-end $$;
-
+ select uuid into permissionUuid
+ from RbacPermission p
+ where p.objectUuid = forObjectUuid
+ and p.op = forOp
+ and forOpTableName is null or p.opTableName = forOpTableName;
+ assert permissionUuid is not null,
+ format('permission %s %s for object UUID %s cannot be found', forOp, forOpTableName, forObjectUuid);
+ return permissionUuid;
+end; $$;
--//
+
+-- ============================================================================
+--changeset rbac-base-duplicate-role-grant-exception:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+create or replace procedure raiseDuplicateRoleGrantException(subRoleId uuid, superRoleId uuid)
+ language plpgsql as $$
+declare
+ subRoleIdName text;
+ superRoleIdName text;
+begin
+ select roleIdName from rbacRole_ev where uuid=subRoleId into subRoleIdName;
+ select roleIdName from rbacRole_ev where uuid=superRoleId into superRoleIdName;
+ raise exception '[400] Duplicate role grant detected: role % (%) already granted to % (%)', subRoleId, subRoleIdName, superRoleId, superRoleIdName;
+end;
+$$;
+--//
+
+
-- ============================================================================
--changeset rbac-base-GRANTS:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
@@ -552,6 +603,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 +629,27 @@ select exists(
);
$$;
+create or replace procedure grantPermissionToRole(permissionUuid uuid, roleUuid 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(permissionUuid uuid, roleDesc RbacRoleDescriptor)
+ language plpgsql as $$
+begin
+ call grantPermissionToRole(permissionUuid, findRoleId(roleDesc));
+end;
+$$;
+
+-- TODO: deprecated, remove and use grantPermissionToRole(...)
create or replace procedure grantPermissionsToRole(roleUuid uuid, permissionIds uuid[])
language plpgsql as $$
begin
@@ -591,7 +675,7 @@ begin
perform assertReferenceType('subRoleId (descendant)', subRoleId, 'RbacRole');
if isGranted(subRoleId, superRoleId) then
- raise exception '[400] Cyclic role grant detected between % and %', subRoleId, superRoleId;
+ call raiseDuplicateRoleGrantException(subRoleId, superRoleId);
end if;
insert
@@ -607,6 +691,11 @@ declare
superRoleId uuid;
subRoleId uuid;
begin
+ -- TODO: maybe separate method grantRoleToRoleIfNotNull(...) for NULLABLE references
+ if superRole.objectUuid is null or subRole.objectuuid is null then
+ return;
+ end if;
+
superRoleId := findRoleId(superRole);
subRoleId := findRoleId(subRole);
@@ -614,7 +703,7 @@ begin
perform assertReferenceType('subRoleId (descendant)', subRoleId, 'RbacRole');
if isGranted(subRoleId, superRoleId) then
- raise exception '[400] Cyclic role grant detected between % and %', subRoleId, superRoleId;
+ call raiseDuplicateRoleGrantException(subRoleId, superRoleId);
end if;
insert
@@ -629,6 +718,7 @@ declare
superRoleId uuid;
subRoleId uuid;
begin
+ if ( superRoleId is null ) then return; end if;
superRoleId := findRoleId(superRole);
if ( subRoleId is null ) then return; end if;
subRoleId := findRoleId(subRole);
@@ -637,7 +727,7 @@ begin
perform assertReferenceType('subRoleId (descendant)', subRoleId, 'RbacRole');
if isGranted(subRoleId, superRoleId) then
- raise exception '[400] Cyclic role grant detected between % and %', subRoleId, superRoleId;
+ call raiseDuplicateRoleGrantException(subRoleId, superRoleId);
end if;
insert
@@ -661,11 +751,39 @@ begin
if (isGranted(superRoleId, subRoleId)) then
delete from RbacGrants where ascendantUuid = superRoleId and descendantUuid = subRoleId;
else
- raise exception 'cannot revoke role % (%) from % (% because it is not granted',
+ raise exception 'cannot revoke role % (%) from % (%) because it is not granted',
subRole, subRoleId, superRole, superRoleId;
end if;
end; $$;
+create or replace procedure revokePermissionFromRole(permissionId UUID, superRole RbacRoleDescriptor)
+ language plpgsql as $$
+declare
+ superRoleId uuid;
+ permissionOp text;
+ objectTable text;
+ objectUuid uuid;
+begin
+ superRoleId := findRoleId(superRole);
+
+ perform assertReferenceType('superRoleId (ascendant)', superRoleId, 'RbacRole');
+ perform assertReferenceType('permission (descendant)', permissionId, 'RbacPermission');
+
+ if (isGranted(superRoleId, permissionId)) then
+ delete from RbacGrants where ascendantUuid = superRoleId and descendantUuid = permissionId;
+ else
+ select p.op, o.objectTable, o.uuid
+ from rbacGrants g
+ join rbacPermission p on p.uuid=g.descendantUuid
+ join rbacobject o on o.uuid=p.objectUuid
+ where g.uuid=permissionId
+ into permissionOp, objectTable, objectUuid;
+
+ raise exception 'cannot revoke permission % (% on %#% (%) from % (%)) because it is not granted',
+ permissionId, permissionOp, objectTable, objectUuid, permissionId, superRole, superRoleId;
+ end if;
+end; $$;
+
-- ============================================================================
--changeset rbac-base-QUERY-ACCESSIBLE-OBJECT-UUIDS:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
@@ -697,7 +815,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 +907,5 @@ do $$
create role restricted;
grant all privileges on all tables in schema public to restricted;
end if;
- end $$
+ end $$;
--//
-
diff --git a/src/main/resources/db/changelog/051-rbac-user-grant.sql b/src/main/resources/db/changelog/1-rbac/1051-rbac-user-grant.sql
similarity index 73%
rename from src/main/resources/db/changelog/051-rbac-user-grant.sql
rename to src/main/resources/db/changelog/1-rbac/1051-rbac-user-grant.sql
index 23dcbdd4..a82865c8 100644
--- a/src/main/resources/db/changelog/051-rbac-user-grant.sql
+++ b/src/main/resources/db/changelog/1-rbac/1051-rbac-user-grant.sql
@@ -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; $$;
+--//
diff --git a/src/main/resources/db/changelog/054-rbac-context.sql b/src/main/resources/db/changelog/1-rbac/1054-rbac-context.sql
similarity index 92%
rename from src/main/resources/db/changelog/054-rbac-context.sql
rename to src/main/resources/db/changelog/1-rbac/1054-rbac-context.sql
index ede86057..ab3a9bd5 100644
--- a/src/main/resources/db/changelog/054-rbac-context.sql
+++ b/src/main/resources/db/changelog/1-rbac/1054-rbac-context.sql
@@ -50,20 +50,23 @@ begin
foreach roleName in array string_to_array(assumedRoles, ';')
loop
- roleNameParts = overlay(roleName placing '#' from length(roleName) + 1 - strpos(reverse(roleName), '.'));
+ roleNameParts = overlay(roleName placing '#' from length(roleName) + 1 - strpos(reverse(roleName), ':'));
objectTableToAssume = split_part(roleNameParts, '#', 1);
objectNameToAssume = split_part(roleNameParts, '#', 2);
roleTypeToAssume = split_part(roleNameParts, '#', 3);
objectUuidToAssume = findObjectUuidByIdName(objectTableToAssume, objectNameToAssume);
+ if objectUuidToAssume is null then
+ raise exception '[401] object % cannot be found in table %', objectNameToAssume, objectTableToAssume;
+ end if;
- select uuid as roleuuidToAssume
+ select uuid
from RbacRole r
where r.objectUuid = objectUuidToAssume
and r.roleType = roleTypeToAssume
into roleUuidToAssume;
if roleUuidToAssume is null then
- raise exception '[403] role % not accessible for user %', roleName, currentSubjects();
+ raise exception '[403] role % does not exist or is not accessible for user %', roleName, currentUser();
end if;
if not isGranted(currentUserUuid, roleUuidToAssume) then
raise exception '[403] user % has no permission to assume role %', currentUser(), roleName;
@@ -82,10 +85,10 @@ end; $$;
This function will be overwritten by later changesets.
*/
create or replace procedure contextDefined(
- currentTask varchar,
- currentRequest varchar,
- currentUser varchar,
- assumedRoles varchar
+ currentTask varchar(127),
+ currentRequest text,
+ currentUser varchar(63),
+ assumedRoles varchar(1023)
)
language plpgsql as $$
declare
diff --git a/src/main/resources/db/changelog/055-rbac-views.sql b/src/main/resources/db/changelog/1-rbac/1055-rbac-views.sql
similarity index 85%
rename from src/main/resources/db/changelog/055-rbac-views.sql
rename to src/main/resources/db/changelog/1-rbac/1055-rbac-views.sql
index b1757c56..a8570f6c 100644
--- a/src/main/resources/db/changelog/055-rbac-views.sql
+++ b/src/main/resources/db/changelog/1-rbac/1055-rbac-views.sql
@@ -9,7 +9,7 @@
*/
drop view if exists rbacrole_ev;
create or replace view rbacrole_ev as
-select (objectTable || '#' || objectIdName || '.' || roleType) as roleIdName, *
+select (objectTable || '#' || objectIdName || ':' || roleType) as roleIdName, *
-- @formatter:off
from (
select r.*,
@@ -40,7 +40,7 @@ select *
where isGranted(currentSubjectsUuids(), r.uuid)
) as unordered
-- @formatter:on
- order by objectTable || '#' || objectIdName || '.' || roleType;
+ order by objectTable || '#' || objectIdName || ':' || roleType;
grant all privileges on rbacrole_rv to ${HSADMINNG_POSTGRES_RESTRICTED_USERNAME};
--//
@@ -57,12 +57,13 @@ create or replace view rbacgrants_ev as
-- @formatter:off
select x.grantUuid as uuid,
x.grantedByTriggerOf as grantedByTriggerOf,
- go.objectTable || '#' || findIdNameByObjectUuid(go.objectTable, go.uuid) || '.' || r.roletype as grantedByRoleIdName,
+ go.objectTable || '#' || findIdNameByObjectUuid(go.objectTable, go.uuid) || ':' || r.roletype as grantedByRoleIdName,
x.ascendingIdName as ascendantIdName,
x.descendingIdName as descendantIdName,
x.grantedByRoleUuid,
x.ascendantUuid as ascendantUuid,
x.descendantUuid as descendantUuid,
+ x.op as permOp, x.optablename as permOpTableName,
x.assumed
from (
select g.uuid as grantUuid,
@@ -70,17 +71,21 @@ create or replace view rbacgrants_ev as
g.grantedbyroleuuid, g.ascendantuuid, g.descendantuuid, g.assumed,
coalesce(
- 'user ' || au.name,
- 'role ' || aro.objectTable || '#' || findIdNameByObjectUuid(aro.objectTable, aro.uuid) || '.' || ar.roletype
+ 'user:' || au.name,
+ 'role:' || aro.objectTable || '#' || findIdNameByObjectUuid(aro.objectTable, aro.uuid) || ':' || ar.roletype
) as ascendingIdName,
aro.objectTable, aro.uuid,
-
- coalesce(
- 'role ' || dro.objectTable || '#' || findIdNameByObjectUuid(dro.objectTable, dro.uuid) || '.' || dr.roletype,
- 'perm ' || dp.op || ' on ' || dpo.objecttable || '#' || findIdNameByObjectUuid(dpo.objectTable, dpo.uuid)
+ ( case
+ when dro is not null
+ then ('role:' || dro.objectTable || '#' || findIdNameByObjectUuid(dro.objectTable, dro.uuid) || ':' || dr.roletype)
+ when dp.op = 'INSERT'
+ then 'perm:' || dpo.objecttable || '#' || findIdNameByObjectUuid(dpo.objectTable, dpo.uuid) || ':' || dp.op || '>' || dp.opTableName
+ else 'perm:' || dpo.objecttable || '#' || findIdNameByObjectUuid(dpo.objectTable, dpo.uuid) || ':' || dp.op
+ end
) as descendingIdName,
- dro.objectTable, dro.uuid
- from rbacgrants as g
+ dro.objectTable, dro.uuid,
+ dp.op, dp.optablename
+ from rbacgrants as g
left outer join rbacrole as ar on ar.uuid = g.ascendantUuid
left outer join rbacobject as aro on aro.uuid = ar.objectuuid
@@ -110,8 +115,8 @@ create or replace view rbacgrants_ev as
drop view if exists rbacgrants_rv;
create or replace view rbacgrants_rv as
-- @formatter:off
-select o.objectTable || '#' || findIdNameByObjectUuid(o.objectTable, o.uuid) || '.' || r.roletype as grantedByRoleIdName,
- g.objectTable || '#' || g.objectIdName || '.' || g.roletype as grantedRoleIdName, g.userName, g.assumed,
+select o.objectTable || '#' || findIdNameByObjectUuid(o.objectTable, o.uuid) || ':' || r.roletype as grantedByRoleIdName,
+ g.objectTable || '#' || g.objectIdName || ':' || g.roletype as grantedRoleIdName, g.userName, g.assumed,
g.grantedByRoleUuid, g.descendantUuid as grantedRoleUuid, g.ascendantUuid as userUuid,
g.objectTable, g.objectUuid, g.objectIdName, g.roleType as grantedRoleType
from (
@@ -322,7 +327,7 @@ execute function deleteRbacUser();
drop view if exists RbacOwnGrantedPermissions_rv;
create or replace view RbacOwnGrantedPermissions_rv as
select r.uuid as roleuuid, p.uuid as permissionUuid,
- (r.objecttable || '#' || r.objectidname || '.' || r.roletype) as roleName, p.op,
+ (r.objecttable || ':' || r.objectidname || ':' || r.roletype) as roleName, p.op,
o.objecttable, r.objectidname, o.uuid as objectuuid
from rbacrole_rv r
join rbacgrants g on g.ascendantuuid = r.uuid
@@ -337,11 +342,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
@@ -356,12 +359,14 @@ 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.roleObjectTable || '#' || xp.roleObjectIdName || ':' || xp.roleType) as roleName,
+ 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 +378,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;
+$$;
--//
diff --git a/src/main/resources/db/changelog/056-rbac-trigger-context.sql b/src/main/resources/db/changelog/1-rbac/1056-rbac-trigger-context.sql
similarity index 100%
rename from src/main/resources/db/changelog/056-rbac-trigger-context.sql
rename to src/main/resources/db/changelog/1-rbac/1056-rbac-trigger-context.sql
diff --git a/src/main/resources/db/changelog/1-rbac/1057-rbac-role-builder.sql b/src/main/resources/db/changelog/1-rbac/1057-rbac-role-builder.sql
new file mode 100644
index 00000000..57ba3cb7
--- /dev/null
+++ b/src/main/resources/db/changelog/1-rbac/1057-rbac-role-builder.sql
@@ -0,0 +1,67 @@
+--liquibase formatted sql
+
+
+-- =================================================================
+-- CREATE ROLE
+--changeset rbac-role-builder-create-role:1 endDelimiter:--//
+-- -----------------------------------------------------------------
+
+create or replace function createRoleWithGrants(
+ roleDescriptor RbacRoleDescriptor,
+ permissions RbacOp[] = array[]::RbacOp[],
+ incomingSuperRoles RbacRoleDescriptor[] = array[]::RbacRoleDescriptor[],
+ outgoingSubRoles RbacRoleDescriptor[] = array[]::RbacRoleDescriptor[],
+ userUuids uuid[] = array[]::uuid[],
+ grantedByRole RbacRoleDescriptor = null
+)
+ returns uuid
+ called on null input
+ language plpgsql as $$
+declare
+ roleUuid uuid;
+ permission RbacOp;
+ permissionUuid uuid;
+ subRoleDesc RbacRoleDescriptor;
+ superRoleDesc RbacRoleDescriptor;
+ subRoleUuid uuid;
+ superRoleUuid uuid;
+ userUuid uuid;
+ userGrantsByRoleUuid uuid;
+begin
+ roleUuid := createRole(roleDescriptor);
+
+ foreach permission in array permissions
+ loop
+ permissionUuid := createPermission(roleDescriptor.objectuuid, permission);
+ call grantPermissionToRole(permissionUuid, roleUuid);
+ end loop;
+
+ foreach superRoleDesc in array array_remove(incomingSuperRoles, null)
+ loop
+ superRoleUuid := getRoleId(superRoleDesc);
+ call grantRoleToRole(roleUuid, superRoleUuid, superRoleDesc.assumed);
+ end loop;
+
+ foreach subRoleDesc in array array_remove(outgoingSubRoles, null)
+ loop
+ subRoleUuid := getRoleId(subRoleDesc);
+ call grantRoleToRole(subRoleUuid, roleUuid, subRoleDesc.assumed);
+ end loop;
+
+ if cardinality(userUuids) > 0 then
+ -- direct grants to users need a grantedByRole which can revoke the grant
+ if grantedByRole is null then
+ userGrantsByRoleUuid := roleUuid; -- TODO.spec: or do we want to require an explicit userGrantsByRoleUuid?
+ else
+ userGrantsByRoleUuid := getRoleId(grantedByRole);
+ end if;
+ foreach userUuid in array userUuids
+ loop
+ call grantRoleToUserUnchecked(userGrantsByRoleUuid, roleUuid, userUuid);
+ end loop;
+ end if;
+
+ return roleUuid;
+end; $$;
+--//
+
diff --git a/src/main/resources/db/changelog/058-rbac-generators.sql b/src/main/resources/db/changelog/1-rbac/1058-rbac-generators.sql
similarity index 71%
rename from src/main/resources/db/changelog/058-rbac-generators.sql
rename to src/main/resources/db/changelog/1-rbac/1058-rbac-generators.sql
index fa198308..958d3afe 100644
--- a/src/main/resources/db/changelog/058-rbac-generators.sql
+++ b/src/main/resources/db/changelog/1-rbac/1058-rbac-generators.sql
@@ -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,59 @@ 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)
+ -- TODO: remove guest role
+ 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$;
+
+ create or replace function %1$sReferrer(entity %2$s)
+ returns RbacRoleDescriptor
+ language plpgsql
+ strict as $f$
+ begin
+ return roleDescriptor('%2$s', entity.uuid, 'REFERRER');
end; $f$;
$sql$, prefix, targetTable);
@@ -92,7 +100,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 +109,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 +136,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; $$;
--//
@@ -137,21 +157,25 @@ end; $$;
--changeset rbac-generators-RESTRICTED-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
-create or replace procedure generateRbacRestrictedView(targetTable text, orderBy text, columnUpdates text = null)
+create or replace procedure generateRbacRestrictedView(targetTable text, orderBy text, columnUpdates text = null, columnNames text = '*')
language plpgsql as $$
declare
sql text;
+ newColumns text;
begin
targetTable := lower(targetTable);
+ if columnNames = '*' then
+ columnNames := columnsNames(targetTable);
+ end if;
/*
- 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
@@ -164,20 +188,21 @@ begin
/**
Instead of insert trigger function for the restricted view.
*/
+ newColumns := 'new.' || replace(columnNames, ',', ', new.');
sql := format($sql$
- create or replace function %1$sInsert()
- returns trigger
- language plpgsql as $f$
- declare
- newTargetRow %1$s;
- begin
- insert
- into %1$s
- values (new.*)
- returning * into newTargetRow;
- return newTargetRow;
- end; $f$;
- $sql$, targetTable);
+ create or replace function %1$sInsert()
+ returns trigger
+ language plpgsql as $f$
+ declare
+ newTargetRow %1$s;
+ begin
+ insert
+ into %1$s (%2$s)
+ values (%3$s)
+ returning * into newTargetRow;
+ return newTargetRow;
+ end; $f$;
+ $sql$, targetTable, columnNames, newColumns);
execute sql;
/*
@@ -200,7 +225,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 +248,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 +256,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;
diff --git a/src/main/resources/db/changelog/059-rbac-statistics.sql b/src/main/resources/db/changelog/1-rbac/1059-rbac-statistics.sql
similarity index 100%
rename from src/main/resources/db/changelog/059-rbac-statistics.sql
rename to src/main/resources/db/changelog/1-rbac/1059-rbac-statistics.sql
diff --git a/src/main/resources/db/changelog/080-rbac-global.sql b/src/main/resources/db/changelog/1-rbac/1080-rbac-global.sql
similarity index 80%
rename from src/main/resources/db/changelog/080-rbac-global.sql
rename to src/main/resources/db/changelog/1-rbac/1080-rbac-global.sql
index 034400fa..c28a464d 100644
--- a/src/main/resources/db/changelog/080-rbac-global.sql
+++ b/src/main/resources/db/changelog/1-rbac/1080-rbac-global.sql
@@ -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,18 +109,41 @@ 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;
-call defineContext('creating global admin role', null, null, null);
-select createRole(globalAdmin());
+ call defineContext('creating role:global#global:ADMIN', null, null, null);
+ select createRole(globalAdmin());
commit;
+--//
+
+
+-- ============================================================================
+--changeset rbac-global-GUEST-ROLE:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+/*
+ A global guest role.
+ */
+create or replace function globalGuest(assumed boolean = true)
+ returns RbacRoleDescriptor
+ returns null on null input
+ stable -- leakproof
+ language sql as $$
+select 'global', (select uuid from RbacObject where objectTable = 'global'), 'GUEST'::RbacRoleType, assumed;
+$$;
+
+begin transaction;
+ call defineContext('creating role:global#global:guest', null, null, null);
+ select createRole(globalGuest());
+commit;
+--//
+
-- ============================================================================
--changeset rbac-global-ADMIN-USERS:1 context:dev,tc endDelimiter:--//
diff --git a/src/main/resources/db/changelog/113-test-customer-rbac.sql b/src/main/resources/db/changelog/113-test-customer-rbac.sql
deleted file mode 100644
index d7682cc1..00000000
--- a/src/main/resources/db/changelog/113-test-customer-rbac.sql
+++ /dev/null
@@ -1,145 +0,0 @@
---liquibase formatted sql
-
--- ============================================================================
---changeset test-customer-rbac-OBJECT:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRelatedRbacObject('test_customer');
---//
-
-
--- ============================================================================
---changeset test-customer-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacRoleDescriptors('testCustomer', 'test_customer');
---//
-
-
--- ============================================================================
---changeset test-customer-rbac-ROLES-CREATION:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-
-/*
- Creates the roles and their assignments for a new customer 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;
-
- call enterTriggerForObjectUuid(NEW.uuid);
-
- -- the owner role with full access for Hostsharing administrators
- testCustomerOwnerUuid = createRoleWithGrants(
- testCustomerOwner(NEW),
- permissions => array['*'],
- incomingSuperRoles => array[globalAdmin()]
- );
-
- -- the admin role for the customer's admins, who can view and add products
- customerAdminUuid = 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()
- );
-
- -- 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']
- );
-
- call leaveTriggerForObjectUuid(NEW.uuid);
- return NEW;
-end; $$;
-
-/*
- An AFTER INSERT TRIGGER which creates the role structure for a new customer.
- */
-
-drop trigger if exists createRbacRolesForTestCustomer_Trigger on test_customer;
-create trigger createRbacRolesForTestCustomer_Trigger
- after insert
- on test_customer
- for each row
-execute procedure createRbacRolesForTestCustomer();
---//
-
-
--- ============================================================================
---changeset test-customer-rbac-IDENTITY-VIEW:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacIdentityView('test_customer', $idName$
- target.prefix
- $idName$);
---//
-
-
--- ============================================================================
---changeset test-customer-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacRestrictedView('test_customer', 'target.prefix',
- $updates$
- reference = new.reference,
- prefix = new.prefix,
- adminUserName = new.adminUserName
- $updates$);
---//
-
-
--- ============================================================================
---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();
---//
-
diff --git a/src/main/resources/db/changelog/123-test-package-rbac.sql b/src/main/resources/db/changelog/123-test-package-rbac.sql
deleted file mode 100644
index 9e68468c..00000000
--- a/src/main/resources/db/changelog/123-test-package-rbac.sql
+++ /dev/null
@@ -1,109 +0,0 @@
---liquibase formatted sql
-
--- ============================================================================
---changeset test-package-rbac-OBJECT:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRelatedRbacObject('test_package');
---//
-
-
--- ============================================================================
---changeset test-package-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacRoleDescriptors('testPackage', 'test_package');
---//
-
-
--- ============================================================================
---changeset test-package-rbac-ROLES-CREATION:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-/*
- Creates the roles and their assignments for a new package for the AFTER INSERT TRIGGER.
- */
-create or replace function createRbacRolesForTestPackage()
- returns trigger
- language plpgsql
- strict as $$
-declare
- parentCustomer 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 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)]
- );
-
- -- an owner role is created and assigned to the package owner role
- perform createRoleWithGrants(
- testPackageAdmin(NEW),
- permissions => array ['add-domain'],
- 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)]
- );
-
- call leaveTriggerForObjectUuid(NEW.uuid);
- return NEW;
-end; $$;
-
-/*
- An AFTER INSERT TRIGGER which creates the role structure for a new package.
- */
-
-create trigger createRbacRolesForTestPackage_Trigger
- after insert
- on test_package
- for each row
-execute procedure createRbacRolesForTestPackage();
---//
-
-
--- ============================================================================
---changeset test-package-rbac-IDENTITY-VIEW:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacIdentityView('test_package', 'target.name');
---//
-
-
--- ============================================================================
---changeset test-package-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_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$);
-
---//
-
-
diff --git a/src/main/resources/db/changelog/133-test-domain-rbac.sql b/src/main/resources/db/changelog/133-test-domain-rbac.sql
deleted file mode 100644
index a78bfb5f..00000000
--- a/src/main/resources/db/changelog/133-test-domain-rbac.sql
+++ /dev/null
@@ -1,117 +0,0 @@
---liquibase formatted sql
-
--- ============================================================================
---changeset test-domain-rbac-OBJECT:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-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:--//
--- ----------------------------------------------------------------------------
-/*
- Creates the roles and their assignments for a new domain for the AFTER INSERT TRIGGER.
- */
-
-create or replace function createRbacRulesForTestDomain()
- 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);
- 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
- for each row
-execute procedure createRbacRulesForTestDomain();
---//
-
-
--- ============================================================================
---changeset test-domain-rbac-IDENTITY-VIEW:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacIdentityView('test_domain', $idName$
- target.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};
---//
diff --git a/src/main/resources/db/changelog/110-test-customer.sql b/src/main/resources/db/changelog/2-test/201-test-customer/2010-test-customer.sql
similarity index 100%
rename from src/main/resources/db/changelog/110-test-customer.sql
rename to src/main/resources/db/changelog/2-test/201-test-customer/2010-test-customer.sql
diff --git a/src/main/resources/db/changelog/2-test/201-test-customer/2013-test-customer-rbac.md b/src/main/resources/db/changelog/2-test/201-test-customer/2013-test-customer-rbac.md
new file mode 100644
index 00000000..19e67a38
--- /dev/null
+++ b/src/main/resources/db/changelog/2-test/201-test-customer/2013-test-customer-rbac.md
@@ -0,0 +1,45 @@
+### rbac customer
+
+This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually.
+
+```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:INSERT{{customer:INSERT}}
+ 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:global:ADMIN ==> perm:customer:INSERT
+role:customer:OWNER ==> perm:customer:DELETE
+role:customer:ADMIN ==> perm:customer:UPDATE
+role:customer:TENANT ==> perm:customer:SELECT
+
+```
diff --git a/src/main/resources/db/changelog/2-test/201-test-customer/2013-test-customer-rbac.sql b/src/main/resources/db/changelog/2-test/201-test-customer/2013-test-customer-rbac.sql
new file mode 100644
index 00000000..2f9ea4de
--- /dev/null
+++ b/src/main/resources/db/changelog/2-test/201-test-customer/2013-test-customer-rbac.sql
@@ -0,0 +1,163 @@
+--liquibase formatted sql
+-- This code generated was by RbacViewPostgresGenerator, do not amend manually.
+
+
+-- ============================================================================
+--changeset test-customer-rbac-OBJECT:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRelatedRbacObject('test_customer');
+--//
+
+
+-- ============================================================================
+--changeset test-customer-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRbacRoleDescriptors('testCustomer', 'test_customer');
+--//
+
+
+-- ============================================================================
+--changeset test-customer-rbac-insert-trigger:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates the roles, grants and permission for the AFTER INSERT TRIGGER.
+ */
+
+create or replace procedure buildRbacSystemForTestCustomer(
+ NEW test_customer
+)
+ language plpgsql as $$
+
+declare
+
+begin
+ call enterTriggerForObjectUuid(NEW.uuid);
+
+ perform createRoleWithGrants(
+ testCustomerOWNER(NEW),
+ permissions => array['DELETE'],
+ incomingSuperRoles => array[globalADMIN(unassumed())],
+ userUuids => array[currentUserUuid()]
+ );
+
+ perform createRoleWithGrants(
+ testCustomerADMIN(NEW),
+ permissions => array['UPDATE'],
+ incomingSuperRoles => array[testCustomerOWNER(NEW)]
+ );
+
+ perform createRoleWithGrants(
+ testCustomerTENANT(NEW),
+ permissions => array['SELECT'],
+ incomingSuperRoles => array[testCustomerADMIN(NEW)]
+ );
+
+ call leaveTriggerForObjectUuid(NEW.uuid);
+end; $$;
+
+/*
+ AFTER INSERT TRIGGER to create the role+grant structure for a new test_customer row.
+ */
+
+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 insertTriggerForTestCustomer_tf();
+--//
+
+
+-- ============================================================================
+--changeset test-customer-rbac-INSERT:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates INSERT INTO test_customer permissions for the related global rows.
+ */
+do language plpgsql $$
+ declare
+ row global;
+ begin
+ call defineContext('create INSERT INTO test_customer permissions for the related global rows');
+
+ FOR row IN SELECT * FROM global
+ LOOP
+ call grantPermissionToRole(
+ createPermission(row.uuid, 'INSERT', 'test_customer'),
+ globalADMIN());
+ END LOOP;
+ END;
+$$;
+
+/**
+ Adds test_customer INSERT permission to specified role of new global rows.
+*/
+create or replace function test_customer_global_insert_tf()
+ returns trigger
+ language plpgsql
+ strict as $$
+begin
+ call grantPermissionToRole(
+ createPermission(NEW.uuid, 'INSERT', 'test_customer'),
+ globalADMIN());
+ return NEW;
+end; $$;
+
+-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
+create trigger z_test_customer_global_insert_tg
+ after insert on global
+ for each row
+execute procedure test_customer_global_insert_tf();
+
+/**
+ Checks if the user or assumed roles are allowed to insert a row to test_customer,
+ where only global-admin has that permission.
+*/
+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
+ when ( not isGlobalAdmin() )
+ execute procedure test_customer_insert_permission_missing_tf();
+--//
+
+-- ============================================================================
+--changeset test-customer-rbac-IDENTITY-VIEW:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+call generateRbacIdentityViewFromProjection('test_customer',
+ $idName$
+ prefix
+ $idName$);
+--//
+
+-- ============================================================================
+--changeset test-customer-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRbacRestrictedView('test_customer',
+ $orderBy$
+ reference
+ $orderBy$,
+ $updates$
+ reference = new.reference,
+ prefix = new.prefix,
+ adminUserName = new.adminUserName
+ $updates$);
+--//
+
diff --git a/src/main/resources/db/changelog/118-test-customer-test-data.sql b/src/main/resources/db/changelog/2-test/201-test-customer/2018-test-customer-test-data.sql
similarity index 84%
rename from src/main/resources/db/changelog/118-test-customer-test-data.sql
rename to src/main/resources/db/changelog/2-test/201-test-customer/2018-test-customer-test-data.sql
index 353b8f59..73c8e535 100644
--- a/src/main/resources/db/changelog/118-test-customer-test-data.sql
+++ b/src/main/resources/db/changelog/2-test/201-test-customer/2018-test-customer-test-data.sql
@@ -28,17 +28,28 @@ 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');
+ call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global:ADMIN');
execute format('set local hsadminng.currentTask to %L', currentTask);
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; $$;
--//
diff --git a/src/main/resources/db/changelog/120-test-package.sql b/src/main/resources/db/changelog/2-test/202-test-package/2020-test-package.sql
similarity index 100%
rename from src/main/resources/db/changelog/120-test-package.sql
rename to src/main/resources/db/changelog/2-test/202-test-package/2020-test-package.sql
diff --git a/src/main/resources/db/changelog/2-test/202-test-package/2023-test-package-rbac.md b/src/main/resources/db/changelog/2-test/202-test-package/2023-test-package-rbac.md
new file mode 100644
index 00000000..368cfe2f
--- /dev/null
+++ b/src/main/resources/db/changelog/2-test/202-test-package/2023-test-package-rbac.md
@@ -0,0 +1,59 @@
+### rbac package
+
+This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually.
+
+```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
+
+```
diff --git a/src/main/resources/db/changelog/2-test/202-test-package/2023-test-package-rbac.sql b/src/main/resources/db/changelog/2-test/202-test-package/2023-test-package-rbac.sql
new file mode 100644
index 00000000..3a4d5d8b
--- /dev/null
+++ b/src/main/resources/db/changelog/2-test/202-test-package/2023-test-package-rbac.sql
@@ -0,0 +1,230 @@
+--liquibase formatted sql
+-- This code generated was by RbacViewPostgresGenerator, do not amend manually.
+
+
+-- ============================================================================
+--changeset test-package-rbac-OBJECT:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRelatedRbacObject('test_package');
+--//
+
+
+-- ============================================================================
+--changeset test-package-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRbacRoleDescriptors('testPackage', 'test_package');
+--//
+
+
+-- ============================================================================
+--changeset test-package-rbac-insert-trigger:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates the roles, grants and permission for the AFTER INSERT TRIGGER.
+ */
+
+create or replace procedure buildRbacSystemForTestPackage(
+ NEW test_package
+)
+ language plpgsql as $$
+
+declare
+ newCustomer test_customer;
+
+begin
+ call enterTriggerForObjectUuid(NEW.uuid);
+
+ SELECT * FROM test_customer WHERE uuid = NEW.customerUuid INTO newCustomer;
+ assert newCustomer.uuid is not null, format('newCustomer must not be null for NEW.customerUuid = %s', NEW.customerUuid);
+
+
+ perform createRoleWithGrants(
+ testPackageOWNER(NEW),
+ permissions => array['DELETE', 'UPDATE'],
+ incomingSuperRoles => array[testCustomerADMIN(newCustomer)]
+ );
+
+ perform createRoleWithGrants(
+ testPackageADMIN(NEW),
+ incomingSuperRoles => array[testPackageOWNER(NEW)]
+ );
+
+ perform createRoleWithGrants(
+ testPackageTENANT(NEW),
+ permissions => array['SELECT'],
+ incomingSuperRoles => array[testPackageADMIN(NEW)],
+ outgoingSubRoles => array[testCustomerTENANT(newCustomer)]
+ );
+
+ call leaveTriggerForObjectUuid(NEW.uuid);
+end; $$;
+
+/*
+ AFTER INSERT TRIGGER to create the role+grant structure for a new test_package row.
+ */
+
+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 insertTriggerForTestPackage_tf();
+--//
+
+
+-- ============================================================================
+--changeset test-package-rbac-update-trigger:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Called from the AFTER UPDATE TRIGGER to re-wire the grants.
+ */
+
+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 WHERE uuid = OLD.customerUuid INTO oldCustomer;
+ assert oldCustomer.uuid is not null, format('oldCustomer must not be null for OLD.customerUuid = %s', OLD.customerUuid);
+
+ SELECT * FROM test_customer WHERE uuid = NEW.customerUuid INTO newCustomer;
+ assert newCustomer.uuid is not null, format('newCustomer must not be null for NEW.customerUuid = %s', NEW.customerUuid);
+
+
+ if NEW.customerUuid <> OLD.customerUuid then
+
+ 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;
+ begin
+ call defineContext('create INSERT INTO test_package permissions for the related test_customer rows');
+
+ FOR row IN SELECT * FROM test_customer
+ LOOP
+ call grantPermissionToRole(
+ createPermission(row.uuid, 'INSERT', 'test_package'),
+ testCustomerADMIN(row));
+ 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(
+ createPermission(NEW.uuid, 'INSERT', 'test_package'),
+ testCustomerADMIN(NEW));
+ return NEW;
+end; $$;
+
+-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
+create trigger z_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,
+ where the check is performed by a direct role.
+
+ A direct role is a role depending on a foreign key directly available in the NEW row.
+*/
+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',
+ $orderBy$
+ name
+ $orderBy$,
+ $updates$
+ version = new.version,
+ customerUuid = new.customerUuid,
+ description = new.description
+ $updates$);
+--//
+
diff --git a/src/main/resources/db/changelog/128-test-package-test-data.sql b/src/main/resources/db/changelog/2-test/202-test-package/2028-test-package-test-data.sql
similarity index 93%
rename from src/main/resources/db/changelog/128-test-package-test-data.sql
rename to src/main/resources/db/changelog/2-test/202-test-package/2028-test-package-test-data.sql
index 4667b742..f50ad480 100644
--- a/src/main/resources/db/changelog/128-test-package-test-data.sql
+++ b/src/main/resources/db/changelog/2-test/202-test-package/2028-test-package-test-data.sql
@@ -25,8 +25,8 @@ begin
cust.uuid;
custAdminUser = 'customer-admin@' || cust.prefix || '.example.com';
- custAdminRole = 'test_customer#' || cust.prefix || '.admin';
- call defineContext(currentTask, null, custAdminUser, custAdminRole);
+ custAdminRole = 'test_customer#' || cust.prefix || ':ADMIN';
+ 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);
diff --git a/src/main/resources/db/changelog/130-test-domain.sql b/src/main/resources/db/changelog/2-test/203-test-domain/2030-test-domain.sql
similarity index 100%
rename from src/main/resources/db/changelog/130-test-domain.sql
rename to src/main/resources/db/changelog/2-test/203-test-domain/2030-test-domain.sql
diff --git a/src/main/resources/db/changelog/2-test/203-test-domain/2033-test-domain-rbac.md b/src/main/resources/db/changelog/2-test/203-test-domain/2033-test-domain-rbac.md
new file mode 100644
index 00000000..d9b3748c
--- /dev/null
+++ b/src/main/resources/db/changelog/2-test/203-test-domain/2033-test-domain-rbac.md
@@ -0,0 +1,75 @@
+### rbac domain
+
+This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually.
+
+```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: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
+
+```
diff --git a/src/main/resources/db/changelog/2-test/203-test-domain/2033-test-domain-rbac.sql b/src/main/resources/db/changelog/2-test/203-test-domain/2033-test-domain-rbac.sql
new file mode 100644
index 00000000..de5faa78
--- /dev/null
+++ b/src/main/resources/db/changelog/2-test/203-test-domain/2033-test-domain-rbac.sql
@@ -0,0 +1,229 @@
+--liquibase formatted sql
+-- This code generated was by RbacViewPostgresGenerator, do not amend manually.
+
+
+-- ============================================================================
+--changeset test-domain-rbac-OBJECT:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRelatedRbacObject('test_domain');
+--//
+
+
+-- ============================================================================
+--changeset test-domain-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRbacRoleDescriptors('testDomain', 'test_domain');
+--//
+
+
+-- ============================================================================
+--changeset test-domain-rbac-insert-trigger:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates the roles, grants and permission for the AFTER INSERT TRIGGER.
+ */
+
+create or replace procedure buildRbacSystemForTestDomain(
+ NEW test_domain
+)
+ language plpgsql as $$
+
+declare
+ newPackage test_package;
+
+begin
+ call enterTriggerForObjectUuid(NEW.uuid);
+
+ SELECT * FROM test_package WHERE uuid = NEW.packageUuid INTO newPackage;
+ assert newPackage.uuid is not null, format('newPackage must not be null for NEW.packageUuid = %s', NEW.packageUuid);
+
+
+ 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 $$
+begin
+ call buildRbacSystemForTestDomain(NEW);
+ return NEW;
+end; $$;
+
+create trigger insertTriggerForTestDomain_tg
+ after insert on test_domain
+ for each row
+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 WHERE uuid = OLD.packageUuid INTO oldPackage;
+ assert oldPackage.uuid is not null, format('oldPackage must not be null for OLD.packageUuid = %s', OLD.packageUuid);
+
+ SELECT * FROM test_package WHERE uuid = NEW.packageUuid INTO newPackage;
+ assert newPackage.uuid is not null, format('newPackage must not be null for NEW.packageUuid = %s', NEW.packageUuid);
+
+
+ if NEW.packageUuid <> OLD.packageUuid then
+
+ 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;
+ begin
+ call defineContext('create INSERT INTO test_domain permissions for the related test_package rows');
+
+ FOR row IN SELECT * FROM test_package
+ LOOP
+ call grantPermissionToRole(
+ createPermission(row.uuid, 'INSERT', 'test_domain'),
+ testPackageADMIN(row));
+ 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(
+ createPermission(NEW.uuid, 'INSERT', 'test_domain'),
+ testPackageADMIN(NEW));
+ return NEW;
+end; $$;
+
+-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
+create trigger z_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,
+ where the check is performed by a direct role.
+
+ A direct role is a role depending on a foreign key directly available in the NEW row.
+*/
+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 generateRbacIdentityViewFromProjection('test_domain',
+ $idName$
+ name
+ $idName$);
+--//
+
+-- ============================================================================
+--changeset test-domain-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRbacRestrictedView('test_domain',
+ $orderBy$
+ name
+ $orderBy$,
+ $updates$
+ version = new.version,
+ packageUuid = new.packageUuid,
+ description = new.description
+ $updates$);
+--//
+
diff --git a/src/main/resources/db/changelog/138-test-domain-test-data.sql b/src/main/resources/db/changelog/2-test/203-test-domain/2038-test-domain-test-data.sql
similarity index 100%
rename from src/main/resources/db/changelog/138-test-domain-test-data.sql
rename to src/main/resources/db/changelog/2-test/203-test-domain/2038-test-domain-test-data.sql
diff --git a/src/main/resources/db/changelog/203-hs-office-contact-rbac.sql b/src/main/resources/db/changelog/203-hs-office-contact-rbac.sql
deleted file mode 100644
index 7ba7891b..00000000
--- a/src/main/resources/db/changelog/203-hs-office-contact-rbac.sql
+++ /dev/null
@@ -1,141 +0,0 @@
---liquibase formatted sql
-
--- ============================================================================
---changeset hs-office-contact-rbac-OBJECT:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRelatedRbacObject('hs_office_contact');
---//
-
-
--- ============================================================================
---changeset hs-office-contact-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacRoleDescriptors('hsOfficeContact', 'hs_office_contact');
---//
-
-
--- ============================================================================
---changeset hs-office-contact-rbac-ROLES-CREATION:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-
-/*
- Creates the roles and their assignments for a new contact for the AFTER INSERT TRIGGER.
- */
-
-create or replace function createRbacRolesForHsOfficeContact()
- returns trigger
- language plpgsql
- strict as $$
-begin
- if TG_OP <> 'INSERT' then
- raise exception 'invalid usage of TRIGGER AFTER INSERT';
- end if;
-
- perform createRoleWithGrants(
- hsOfficeContactOwner(NEW),
- permissions => array['*'],
- incomingSuperRoles => array[globalAdmin()],
- userUuids => array[currentUserUuid()],
- grantedByRole => globalAdmin()
- );
-
- perform createRoleWithGrants(
- hsOfficeContactAdmin(NEW),
- permissions => array['edit'],
- incomingSuperRoles => array[hsOfficeContactOwner(NEW)]
- );
-
- perform createRoleWithGrants(
- hsOfficeContactTenant(NEW),
- incomingSuperRoles => array[hsOfficeContactAdmin(NEW)]
- );
-
- perform createRoleWithGrants(
- hsOfficeContactGuest(NEW),
- permissions => array['view'],
- incomingSuperRoles => array[hsOfficeContactTenant(NEW)]
- );
-
- return NEW;
-end; $$;
-
-/*
- An AFTER INSERT TRIGGER which creates the role structure for a new customer.
- */
-
-create trigger createRbacRolesForHsOfficeContact_Trigger
- after insert
- on hs_office_contact
- for each row
-execute procedure createRbacRolesForHsOfficeContact();
---//
-
-
--- ============================================================================
---changeset hs-office-contact-rbac-IDENTITY-VIEW:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-
-call generateRbacIdentityView('hs_office_contact', $idName$
- target.label
- $idName$);
---//
-
-
--- ============================================================================
---changeset hs-office-contact-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacRestrictedView('hs_office_contact', 'target.label',
- $updates$
- label = new.label,
- postalAddress = new.postalAddress,
- emailAddresses = new.emailAddresses,
- phoneNumbers = new.phoneNumbers
- $updates$);
---/
-
-
--- ============================================================================
---changeset hs-office-contact-rbac-NEW-CONTACT:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-/*
- Creates a global permission for new-contact and assigns it to the hostsharing admins role.
- */
-do language plpgsql $$
- declare
- addCustomerPermissions uuid[];
- globalObjectUuid uuid;
- globalAdminRoleUuid uuid;
- begin
- call defineContext('granting global new-contact permission to global admin role', null, null, null);
-
- globalAdminRoleUuid := findRoleId(globalAdmin());
- globalObjectUuid := (select uuid from global);
- addCustomerPermissions := createPermissions(globalObjectUuid, array ['new-contact']);
- call grantPermissionsToRole(globalAdminRoleUuid, addCustomerPermissions);
- end;
-$$;
-
-/**
- Used by the trigger to prevent the add-customer to current user respectively assumed roles.
- */
-create or replace function addHsOfficeContactNotAllowedForCurrentSubjects()
- returns trigger
- language PLPGSQL
-as $$
-begin
- raise exception '[403] new-contact 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_contact_insert_trigger
- before insert
- on hs_office_contact
- for each row
- -- TODO.spec: who is allowed to create new contacts
- when ( not hasAssumedRole() )
-execute procedure addHsOfficeContactNotAllowedForCurrentSubjects();
---//
-
diff --git a/src/main/resources/db/changelog/213-hs-office-person-rbac.sql b/src/main/resources/db/changelog/213-hs-office-person-rbac.sql
deleted file mode 100644
index 42eacf2f..00000000
--- a/src/main/resources/db/changelog/213-hs-office-person-rbac.sql
+++ /dev/null
@@ -1,139 +0,0 @@
---liquibase formatted sql
-
--- ============================================================================
---changeset hs-office-person-rbac-OBJECT:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRelatedRbacObject('hs_office_person');
---//
-
-
--- ============================================================================
---changeset hs-office-person-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacRoleDescriptors('hsOfficePerson', 'hs_office_person');
---//
-
-
--- ============================================================================
---changeset hs-office-person-rbac-ROLES-CREATION:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-/*
- Creates the roles and their assignments for a new person for the AFTER INSERT TRIGGER.
- */
-create or replace function createRbacRolesForHsOfficePerson()
- returns trigger
- language plpgsql
- strict as $$
-begin
- if TG_OP <> 'INSERT' then
- raise exception 'invalid usage of TRIGGER AFTER INSERT';
- end if;
-
- perform createRoleWithGrants(
- hsOfficePersonOwner(NEW),
- permissions => array['*'],
- 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?
- perform createRoleWithGrants(
- hsOfficePersonAdmin(NEW),
- permissions => array['edit'],
- incomingSuperRoles => array[hsOfficePersonOwner(NEW)]
- );
-
- perform createRoleWithGrants(
- hsOfficePersonTenant(NEW),
- incomingSuperRoles => array[hsOfficePersonAdmin(NEW)]
- );
-
- perform createRoleWithGrants(
- hsOfficePersonGuest(NEW),
- permissions => array['view'],
- incomingSuperRoles => array[hsOfficePersonTenant(NEW)]
- );
-
- return NEW;
-end; $$;
-
-/*
- An AFTER INSERT TRIGGER which creates the role structure for a new customer.
- */
-
-create trigger createRbacRolesForHsOfficePerson_Trigger
- after insert
- on hs_office_person
- for each row
-execute procedure createRbacRolesForHsOfficePerson();
---//
-
-
--- ============================================================================
---changeset hs-office-person-rbac-IDENTITY-VIEW:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacIdentityView('hs_office_person', $idName$
- concat(target.tradeName, target.familyName, target.givenName)
- $idName$);
---//
-
-
--- ============================================================================
---changeset hs-office-person-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacRestrictedView('hs_office_person', 'concat(target.tradeName, target.familyName, target.givenName)',
- $updates$
- personType = new.personType,
- tradeName = new.tradeName,
- givenName = new.givenName,
- familyName = new.familyName
- $updates$);
---//
-
-
--- ============================================================================
---changeset hs-office-person-rbac-NEW-PERSON:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-/*
- Creates a global permission for new-person and assigns it to the hostsharing admins role.
- */
-do language plpgsql $$
- declare
- addCustomerPermissions uuid[];
- globalObjectUuid uuid;
- globalAdminRoleUuid uuid ;
- begin
- call defineContext('granting global new-person permission to global admin role', null, null, null);
-
- globalAdminRoleUuid := findRoleId(globalAdmin());
- globalObjectUuid := (select uuid from global);
- addCustomerPermissions := createPermissions(globalObjectUuid, array ['new-person']);
- call grantPermissionsToRole(globalAdminRoleUuid, addCustomerPermissions);
- end;
-$$;
-
-/**
- Used by the trigger to prevent the add-customer to current user respectively assumed roles.
- */
-create or replace function addHsOfficePersonNotAllowedForCurrentSubjects()
- returns trigger
- language PLPGSQL
-as $$
-begin
- raise exception '[403] new-person 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_person_insert_trigger
- before insert
- on hs_office_person
- for each row
- -- TODO.spec: who is allowed to create new persons
- when ( not hasAssumedRole() )
-execute procedure addHsOfficePersonNotAllowedForCurrentSubjects();
---//
-
diff --git a/src/main/resources/db/changelog/220-hs-office-relationship.sql b/src/main/resources/db/changelog/220-hs-office-relationship.sql
deleted file mode 100644
index 44f9e500..00000000
--- a/src/main/resources/db/changelog/220-hs-office-relationship.sql
+++ /dev/null
@@ -1,36 +0,0 @@
---liquibase formatted sql
-
--- ============================================================================
---changeset hs-office-relationship-MAIN-TABLE:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-
-CREATE TYPE HsOfficeRelationshipType AS ENUM (
- 'UNKNOWN',
- 'PARTNER',
- 'EX_PARTNER',
- 'REPRESENTATIVE',
- 'VIP_CONTACT',
- 'ACCOUNTING',
- 'OPERATIONS',
- 'SUBSCRIBER');
-
-CREATE CAST (character varying as HsOfficeRelationshipType) WITH INOUT AS IMPLICIT;
-
-create table if not exists hs_office_relationship
-(
- uuid uuid unique references RbacObject (uuid) initially deferred, -- on delete cascade
- relAnchorUuid uuid not null references hs_office_person(uuid),
- relHolderUuid uuid not null references hs_office_person(uuid),
- contactUuid uuid references hs_office_contact(uuid),
- relType HsOfficeRelationshipType not null,
- relMark varchar(24)
-);
---//
-
-
--- ============================================================================
---changeset hs-office-relationship-MAIN-TABLE-JOURNAL:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-
-call create_journal('hs_office_relationship');
---//
diff --git a/src/main/resources/db/changelog/223-hs-office-relationship-rbac.md b/src/main/resources/db/changelog/223-hs-office-relationship-rbac.md
deleted file mode 100644
index c41de32c..00000000
--- a/src/main/resources/db/changelog/223-hs-office-relationship-rbac.md
+++ /dev/null
@@ -1,192 +0,0 @@
-### hs_office_relationship RBAC
-
-```mermaid
-
-flowchart TB
-
-subgraph global
- style global fill:#eee
-
- role:global.admin[global.admin]
-end
-
-subgraph hsOfficeContact
- direction TB
- style hsOfficeContact fill:#eee
-
- role:hsOfficeContact.admin[contact.admin]
- --> role:hsOfficeContact.tenant[contact.tenant]
- --> role:hsOfficeContact.guest[contact.guest]
-end
-
-subgraph hsOfficePerson
- direction TB
- style hsOfficePerson fill:#eee
-
- role:hsOfficePerson.admin[person.admin]
- --> role:hsOfficePerson.tenant[person.tenant]
- --> role:hsOfficePerson.guest[person.guest]
-end
-
-subgraph hsOfficeRelationship
-
- role:hsOfficePerson#relAnchor.admin[person#anchor.admin]
- --- role:hsOfficePerson.admin
-
- role:hsOfficeRelationship.owner[relationship.owner]
- %% permissions
- role:hsOfficeRelationship.owner --> perm:hsOfficeRelationship.*{{relationship.*}}
- %% incoming
- role:global.admin ---> role:hsOfficeRelationship.owner
- role:hsOfficePersonAdmin#relAnchor.admin
-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();
---//
-
diff --git a/src/main/resources/db/changelog/223-hs-office-relationship-rbac.sql b/src/main/resources/db/changelog/223-hs-office-relationship-rbac.sql
deleted file mode 100644
index 928af48c..00000000
--- a/src/main/resources/db/changelog/223-hs-office-relationship-rbac.sql
+++ /dev/null
@@ -1,192 +0,0 @@
---liquibase formatted sql
-
--- ============================================================================
---changeset hs-office-relationship-rbac-OBJECT:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRelatedRbacObject('hs_office_relationship');
---//
-
-
--- ============================================================================
---changeset hs-office-relationship-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacRoleDescriptors('hsOfficeRelationship', 'hs_office_relationship');
---//
-
-
--- ============================================================================
---changeset hs-office-relationship-rbac-ROLES-CREATION:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-
-/*
- Creates and updates the roles and their assignments for relationship entities.
- */
-
-create or replace function hsOfficeRelationshipRbacRolesTrigger()
- returns trigger
- language plpgsql
- strict as $$
-declare
- hsOfficeRelationshipTenant RbacRoleDescriptor;
- newRelAnchor hs_office_person;
- newRelHolder hs_office_person;
- oldContact hs_office_contact;
- newContact hs_office_contact;
-begin
- call enterTriggerForObjectUuid(NEW.uuid);
-
- hsOfficeRelationshipTenant := hsOfficeRelationshipTenant(NEW);
-
- select * from hs_office_person as p where p.uuid = NEW.relAnchorUuid into newRelAnchor;
- select * from hs_office_person as p where p.uuid = NEW.relHolderUuid into newRelHolder;
- select * from hs_office_contact as c where c.uuid = NEW.contactUuid into newContact;
-
- if TG_OP = 'INSERT' then
-
- perform createRoleWithGrants(
- hsOfficeRelationshipOwner(NEW),
- permissions => array['*'],
- incomingSuperRoles => array[
- globalAdmin(),
- hsOfficePersonAdmin(newRelAnchor)]
- );
-
- perform createRoleWithGrants(
- hsOfficeRelationshipAdmin(NEW),
- permissions => array['edit'],
- incomingSuperRoles => array[hsOfficeRelationshipOwner(NEW)]
- );
-
- -- the tenant role for those related users who can view the data
- perform createRoleWithGrants(
- hsOfficeRelationshipTenant,
- permissions => array['view'],
- incomingSuperRoles => array[
- hsOfficeRelationshipAdmin(NEW),
- hsOfficePersonAdmin(newRelAnchor),
- hsOfficePersonAdmin(newRelHolder),
- hsOfficeContactAdmin(newContact)],
- outgoingSubRoles => 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
- -- TODO: this can probably be avoided through agent+guest roles
- 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;
-
- call leaveTriggerForObjectUuid(NEW.uuid);
- 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();
---//
-
diff --git a/src/main/resources/db/changelog/228-hs-office-relationship-test-data.sql b/src/main/resources/db/changelog/228-hs-office-relationship-test-data.sql
deleted file mode 100644
index 39c15ac2..00000000
--- a/src/main/resources/db/changelog/228-hs-office-relationship-test-data.sql
+++ /dev/null
@@ -1,104 +0,0 @@
---liquibase formatted sql
-
-
--- ============================================================================
---changeset hs-office-relationship-TEST-DATA-GENERATOR:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-
-/*
- Creates a single relationship test record.
- */
-create or replace procedure createHsOfficeRelationshipTestData(
- holderPersonName varchar,
- relationshipType HsOfficeRelationshipType,
- anchorPersonTradeName varchar,
- contactLabel varchar,
- mark varchar default null)
- language plpgsql as $$
-declare
- currentTask varchar;
- idName varchar;
- anchorPerson hs_office_person;
- holderPerson hs_office_person;
- contact hs_office_contact;
-
-begin
- idName := cleanIdentifier( anchorPersonTradeName || '-' || holderPersonName);
- currentTask := 'creating relationship test-data ' || idName;
- call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global.admin');
- execute format('set local hsadminng.currentTask to %L', currentTask);
-
- select p.* from hs_office_person p where p.tradeName = anchorPersonTradeName into anchorPerson;
- if anchorPerson is null then
- raise exception 'anchorPerson "%" not found', anchorPersonTradeName;
- end if;
-
- select p.* from hs_office_person p
- where p.tradeName = holderPersonName or p.familyName = holderPersonName
- into holderPerson;
- if holderPerson is null then
- raise exception 'holderPerson "%" not found', holderPersonName;
- end if;
-
- select c.* from hs_office_contact c where c.label = contactLabel into contact;
- if contact is null then
- raise exception 'contact "%" not found', contactLabel;
- end if;
-
- raise notice 'creating test relationship: %', idName;
- raise notice '- using anchor person (%): %', anchorPerson.uuid, anchorPerson;
- raise notice '- using holder person (%): %', holderPerson.uuid, holderPerson;
- raise notice '- using contact (%): %', contact.uuid, contact;
- insert
- into hs_office_relationship (uuid, relanchoruuid, relholderuuid, reltype, relmark, contactUuid)
- values (uuid_generate_v4(), anchorPerson.uuid, holderPerson.uuid, relationshipType, mark, contact.uuid);
-end; $$;
---//
-
-/*
- Creates a range of test relationship for mass data generation.
- */
-create or replace procedure createHsOfficeRelationshipTestData(
- startCount integer, -- count of auto generated rows before the run
- endCount integer -- count of auto generated rows after the run
-)
- language plpgsql as $$
-declare
- person hs_office_person;
- contact hs_office_contact;
-begin
- for t in startCount..endCount
- loop
- select p.* from hs_office_person p where tradeName = intToVarChar(t, 4) into person;
- select c.* from hs_office_contact c where c.label = intToVarChar(t, 4) || '#' || t into contact;
-
- call createHsOfficeRelationshipTestData(person.uuid, contact.uuid, 'REPRESENTATIVE');
- commit;
- end loop;
-end; $$;
---//
-
-
--- ============================================================================
---changeset hs-office-relationship-TEST-DATA-GENERATION:1 –context=dev,tc endDelimiter:--//
--- ----------------------------------------------------------------------------
-
-do language plpgsql $$
- begin
- call createHsOfficeRelationshipTestData('First GmbH', 'PARTNER', 'Hostsharing eG', 'first contact');
- call createHsOfficeRelationshipTestData('Firby', 'REPRESENTATIVE', 'First GmbH', 'first contact');
-
- call createHsOfficeRelationshipTestData('Second e.K.', 'PARTNER', 'Hostsharing eG', 'second contact');
- call createHsOfficeRelationshipTestData('Smith', 'REPRESENTATIVE', 'Second e.K.', 'second contact');
-
- call createHsOfficeRelationshipTestData('Third OHG', 'PARTNER', 'Hostsharing eG', 'third contact');
- call createHsOfficeRelationshipTestData('Tucker', 'REPRESENTATIVE', 'Third OHG', 'third contact');
-
- call createHsOfficeRelationshipTestData('Fourth eG', 'PARTNER', 'Hostsharing eG', 'fourth contact');
- call createHsOfficeRelationshipTestData('Fouler', 'REPRESENTATIVE', 'Third OHG', 'third contact');
-
- call createHsOfficeRelationshipTestData('Smith', 'PARTNER', 'Hostsharing eG', 'sixth contact');
- call createHsOfficeRelationshipTestData('Smith', 'SUBSCRIBER', 'Third OHG', 'third contact', 'members-announce');
- end;
-$$;
---//
diff --git a/src/main/resources/db/changelog/233-hs-office-partner-rbac.md b/src/main/resources/db/changelog/233-hs-office-partner-rbac.md
deleted file mode 100644
index 148343c3..00000000
--- a/src/main/resources/db/changelog/233-hs-office-partner-rbac.md
+++ /dev/null
@@ -1,78 +0,0 @@
-### hs_office_partner RBAC
-
-```mermaid
-flowchart TB
-
-subgraph global
- style global fill:#eee
-
- role:global.admin[global.admin]
-end
-
-subgraph hsOfficeContact
- direction TB
- style hsOfficeContact fill:#eee
-
- role:hsOfficeContact.admin[contact.admin]
- --> role:hsOfficeContact.tenant[contact.tenant]
- --> role:hsOfficeContact.guest[contact.guest]
-end
-
-subgraph hsOfficePerson
- direction TB
- style hsOfficePerson fill:#eee
-
- role:hsOfficePerson.admin[person.admin]
- --> role:hsOfficePerson.tenant[person.tenant]
- --> role:hsOfficePerson.guest[person.guest]
-end
-
-subgraph hsOfficePartnerDetails
- direction TB
-
- perm:hsOfficePartnerDetails.*{{partner.*}}
- perm:hsOfficePartnerDetails.edit{{partner.edit}}
- perm:hsOfficePartnerDetails.view{{partner.view}}
-end
-
-subgraph hsOfficePartner
-
- role:hsOfficePartner.owner[partner.owner]
- %% permissions
- role:hsOfficePartner.owner --> perm:hsOfficePartner.*{{partner.*}}
- role:hsOfficePartner.owner --> perm:hsOfficePartnerDetails.*{{partner.*}}
- %% incoming
- role:global.admin ---> role:hsOfficePartner.owner
-
- role:hsOfficePartner.admin[partner.admin]
- %% permissions
- role:hsOfficePartner.admin --> perm:hsOfficePartner.edit{{partner.edit}}
- role:hsOfficePartner.admin --> perm:hsOfficePartnerDetails.edit{{partner.edit}}
- %% incoming
- role:hsOfficePartner.owner ---> role:hsOfficePartner.admin
- %% outgoing
- role:hsOfficePartner.admin --> role:hsOfficePerson.tenant
- role:hsOfficePartner.admin --> role:hsOfficeContact.tenant
-
- role:hsOfficePartner.agent[partner.agent]
- %% permissions
- role:hsOfficePartner.agent --> perm:hsOfficePartnerDetails.view{{partner.view}}
- %% incoming
- role:hsOfficePartner.admin ---> role:hsOfficePartner.agent
- role:hsOfficePerson.admin --> role:hsOfficePartner.agent
- role:hsOfficeContact.admin --> role:hsOfficePartner.agent
-
- role:hsOfficePartner.tenant[partner.tenant]
- %% incoming
- role:hsOfficePartner.agent --> role:hsOfficePartner.tenant
- %% outgoing
- role:hsOfficePartner.tenant --> role:hsOfficePerson.guest
- role:hsOfficePartner.tenant --> role:hsOfficeContact.guest
-
- role:hsOfficePartner.guest[partner.guest]
- %% permissions
- role:hsOfficePartner.guest --> perm:hsOfficePartner.view{{partner.view}}
- %% incoming
- role:hsOfficePartner.tenant --> role:hsOfficePartner.guest
-end
-```
diff --git a/src/main/resources/db/changelog/233-hs-office-partner-rbac.sql b/src/main/resources/db/changelog/233-hs-office-partner-rbac.sql
deleted file mode 100644
index 4b4da009..00000000
--- a/src/main/resources/db/changelog/233-hs-office-partner-rbac.sql
+++ /dev/null
@@ -1,256 +0,0 @@
---liquibase formatted sql
-
--- ============================================================================
---changeset hs-office-partner-rbac-OBJECT:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRelatedRbacObject('hs_office_partner');
---//
-
-
--- ============================================================================
---changeset hs-office-partner-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacRoleDescriptors('hsOfficePartner', 'hs_office_partner');
---//
-
-
--- ============================================================================
---changeset hs-office-partner-rbac-ROLES-CREATION:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-
-/*
- Creates and updates the roles and their assignments for partner entities.
- */
-
-create or replace function hsOfficePartnerRbacRolesTrigger()
- returns trigger
- language plpgsql
- strict as $$
-declare
- oldPartnerRole hs_office_relationship;
- newPartnerRole hs_office_relationship;
-
- oldPerson hs_office_person;
- newPerson hs_office_person;
-
- oldContact hs_office_contact;
- newContact hs_office_contact;
-begin
- call enterTriggerForObjectUuid(NEW.uuid);
-
- select * from hs_office_relationship as r where r.uuid = NEW.partnerroleuuid into newPartnerRole;
- select * from hs_office_person as p where p.uuid = NEW.personUuid into newPerson;
- select * from hs_office_contact as c where c.uuid = NEW.contactUuid into newContact;
-
- if TG_OP = 'INSERT' then
-
- -- === ATTENTION: code generated from related Mermaid flowchart: ===
-
- perform createRoleWithGrants(
- hsOfficePartnerOwner(NEW),
- permissions => array['*'],
- incomingSuperRoles => array[globalAdmin()]
- );
-
- perform createRoleWithGrants(
- hsOfficePartnerAdmin(NEW),
- permissions => array['edit'],
- incomingSuperRoles => array[
- hsOfficePartnerOwner(NEW)],
- outgoingSubRoles => array[
- hsOfficeRelationshipTenant(newPartnerRole),
- hsOfficePersonTenant(newPerson),
- hsOfficeContactTenant(newContact)]
- );
-
- perform createRoleWithGrants(
- hsOfficePartnerAgent(NEW),
- incomingSuperRoles => array[
- hsOfficePartnerAdmin(NEW),
- hsOfficeRelationshipAdmin(newPartnerRole),
- hsOfficePersonAdmin(newPerson),
- hsOfficeContactAdmin(newContact)]
- );
-
- perform createRoleWithGrants(
- hsOfficePartnerTenant(NEW),
- incomingSuperRoles => array[
- hsOfficePartnerAgent(NEW)],
- outgoingSubRoles => array[
- hsOfficeRelationshipTenant(newPartnerRole),
- hsOfficePersonGuest(newPerson),
- hsOfficeContactGuest(newContact)]
- );
-
- perform createRoleWithGrants(
- hsOfficePartnerGuest(NEW),
- permissions => array['view'],
- incomingSuperRoles => array[hsOfficePartnerTenant(NEW)]
- );
-
- -- === END of code generated from Mermaid flowchart. ===
-
- -- Each partner-details entity belong exactly to one partner entity
- -- and it makes little sense just to delegate partner-details roles.
- -- Therefore, we did not model partner-details roles,
- -- but instead just assign extra permissions to existing partner-roles.
-
- --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 ['*'])
- );
-
- call grantPermissionsToRole(
- getRoleId(hsOfficePartnerAdmin(NEW), 'fail'),
- createPermissions(NEW.detailsUuid, array ['edit'])
- );
-
- 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'])
- );
-
-
- elsif TG_OP = 'UPDATE' then
-
- if OLD.partnerRoleUuid <> NEW.partnerRoleUuid then
- select * from hs_office_relationship as r where r.uuid = OLD.partnerRoleUuid into oldPartnerRole;
-
- call revokeRoleFromRole(hsOfficeRelationshipTenant(oldPartnerRole), hsOfficePartnerAdmin(OLD));
- call grantRoleToRole(hsOfficeRelationshipTenant(newPartnerRole), hsOfficePartnerAdmin(NEW));
-
- call revokeRoleFromRole(hsOfficePartnerAgent(OLD), hsOfficeRelationshipAdmin(oldPartnerRole));
- call grantRoleToRole(hsOfficePartnerAgent(NEW), hsOfficeRelationshipAdmin(newPartnerRole));
-
- call revokeRoleFromRole(hsOfficeRelationshipGuest(oldPartnerRole), hsOfficePartnerTenant(OLD));
- call grantRoleToRole(hsOfficeRelationshipGuest(newPartnerRole), hsOfficePartnerTenant(NEW));
- end if;
-
- if OLD.personUuid <> NEW.personUuid then
- select * from hs_office_person as p where p.uuid = OLD.personUuid into oldPerson;
-
- call revokeRoleFromRole(hsOfficePersonTenant(oldPerson), hsOfficePartnerAdmin(OLD));
- call grantRoleToRole(hsOfficePersonTenant(newPerson), hsOfficePartnerAdmin(NEW));
-
- call revokeRoleFromRole(hsOfficePartnerAgent(OLD), hsOfficePersonAdmin(oldPerson));
- call grantRoleToRole(hsOfficePartnerAgent(NEW), hsOfficePersonAdmin(newPerson));
-
- call revokeRoleFromRole(hsOfficePersonGuest(oldPerson), hsOfficePartnerTenant(OLD));
- call grantRoleToRole(hsOfficePersonGuest(newPerson), hsOfficePartnerTenant(NEW));
- end if;
-
- if OLD.contactUuid <> NEW.contactUuid then
- select * from hs_office_contact as c where c.uuid = OLD.contactUuid into oldContact;
-
- call revokeRoleFromRole(hsOfficeContactTenant(oldContact), hsOfficePartnerAdmin(OLD));
- call grantRoleToRole(hsOfficeContactTenant(newContact), hsOfficePartnerAdmin(NEW));
-
- call revokeRoleFromRole(hsOfficePartnerAgent(OLD), hsOfficeContactAdmin(oldContact));
- call grantRoleToRole(hsOfficePartnerAgent(NEW), hsOfficeContactAdmin(newContact));
-
- call revokeRoleFromRole(hsOfficeContactGuest(oldContact), hsOfficePartnerTenant(OLD));
- call grantRoleToRole(hsOfficeContactGuest(newContact), hsOfficePartnerTenant(NEW));
- end if;
- else
- raise exception 'invalid usage of TRIGGER';
- end if;
-
- call leaveTriggerForObjectUuid(NEW.uuid);
- return NEW;
-end; $$;
-
-/*
- An AFTER INSERT TRIGGER which creates the role structure for a new customer.
- */
-create trigger createRbacRolesForHsOfficePartner_Trigger
- after insert
- on hs_office_partner
- for each row
-execute procedure hsOfficePartnerRbacRolesTrigger();
-
-/*
- An AFTER UPDATE TRIGGER which updates the role structure of a customer.
- */
-create trigger updateRbacRolesForHsOfficePartner_Trigger
- after update
- on hs_office_partner
- for each row
-execute procedure hsOfficePartnerRbacRolesTrigger();
---//
-
-
--- ============================================================================
---changeset hs-office-partner-rbac-IDENTITY-VIEW:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacIdentityView('hs_office_partner', $idName$
- partnerNumber || ':' ||
- (select idName from hs_office_person_iv p where p.uuid = target.personuuid)
- || '-' ||
- (select idName from hs_office_contact_iv c where c.uuid = target.contactuuid)
- $idName$);
---//
-
-
--- ============================================================================
---changeset hs-office-partner-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacRestrictedView('hs_office_partner',
- '(select idName from hs_office_person_iv p where p.uuid = target.personUuid)',
- $updates$
- partnerRoleUuid = new.partnerRoleUuid,
- personUuid = new.personUuid,
- contactUuid = new.contactUuid
- $updates$);
---//
-
-
--- ============================================================================
---changeset hs-office-partner-rbac-NEW-PARTNER:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-/*
- Creates a global permission for new-partner and assigns it to the Hostsharing admins role.
- */
-do language plpgsql $$
- declare
- addCustomerPermissions uuid[];
- globalObjectUuid uuid;
- globalAdminRoleUuid uuid ;
- begin
- call defineContext('granting global new-partner permission to global admin role', null, null, null);
-
- globalAdminRoleUuid := findRoleId(globalAdmin());
- globalObjectUuid := (select uuid from global);
- addCustomerPermissions := createPermissions(globalObjectUuid, array ['new-partner']);
- call grantPermissionsToRole(globalAdminRoleUuid, addCustomerPermissions);
- end;
-$$;
-
-/**
- Used by the trigger to prevent the add-customer to current user respectively assumed roles.
- */
-create or replace function addHsOfficePartnerNotAllowedForCurrentSubjects()
- returns trigger
- language PLPGSQL
-as $$
-begin
- raise exception '[403] new-partner 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_partner_insert_trigger
- before insert
- on hs_office_partner
- for each row
- -- TODO.spec: who is allowed to create new partners
- when ( not hasAssumedRole() )
-execute procedure addHsOfficePartnerNotAllowedForCurrentSubjects();
---//
-
diff --git a/src/main/resources/db/changelog/234-hs-office-partner-details-rbac.sql b/src/main/resources/db/changelog/234-hs-office-partner-details-rbac.sql
deleted file mode 100644
index ab94481e..00000000
--- a/src/main/resources/db/changelog/234-hs-office-partner-details-rbac.sql
+++ /dev/null
@@ -1,86 +0,0 @@
---liquibase formatted sql
-
--- ============================================================================
---changeset hs-office-partner-details-rbac-OBJECT:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRelatedRbacObject('hs_office_partner_details');
---//
-
-
-
-
-
--- ============================================================================
---changeset hs-office-partner-details-rbac-IDENTITY-VIEW:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacIdentityView('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)
- $idName$);
---//
-
-
--- ============================================================================
---changeset hs-office-partner-details-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacRestrictedView('hs_office_partner_details',
- 'target.uuid', -- no specific order required
- $updates$
- registrationOffice = new.registrationOffice,
- registrationNumber = new.registrationNumber,
- birthPlace = new.birthPlace,
- birthName = new.birthName,
- birthday = new.birthday,
- dateOfDeath = new.dateOfDeath
- $updates$);
---//
-
-
--- ============================================================================
---changeset hs-office-partner-details-rbac-NEW-CONTACT:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-/*
- Creates a global permission for new-partner-details and assigns it to the hostsharing admins role.
- */
-do language plpgsql $$
- declare
- addCustomerPermissions uuid[];
- globalObjectUuid uuid;
- globalAdminRoleUuid uuid ;
- begin
- call defineContext('granting global new-partner-details permission to global admin role', null, null, null);
-
- globalAdminRoleUuid := findRoleId(globalAdmin());
- globalObjectUuid := (select uuid from global);
- addCustomerPermissions := createPermissions(globalObjectUuid, array ['new-partner-details']);
- call grantPermissionsToRole(globalAdminRoleUuid, addCustomerPermissions);
- end;
-$$;
-
--- TODO.refa: the code below could be moved to a generator, maybe even the code above.
--- Additionally, the code below is not neccesary for all entities, specifiy when it is!
-
-/**
- Used by the trigger to prevent the add-partner-details to current user respectively assumed roles.
- */
-create or replace function addHsOfficePartnerDetailsNotAllowedForCurrentSubjects()
- returns trigger
- language PLPGSQL
-as $$
-begin
- raise exception '[403] new-partner-details not permitted for %',
- array_to_string(currentSubjects(), ';', 'null');
-end; $$;
-
-/**
- Checks if the user or assumed roles are allowed to create new partner-details.
- */
-create trigger hs_office_partner_details_insert_trigger
- before insert
- on hs_office_partner_details
- for each row
- when ( not hasAssumedRole() )
-execute procedure addHsOfficePartnerDetailsNotAllowedForCurrentSubjects();
---//
-
diff --git a/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.md b/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.md
deleted file mode 100644
index fc34f147..00000000
--- a/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.md
+++ /dev/null
@@ -1,40 +0,0 @@
-### hs_office_bankaccount RBAC Roles
-
-```mermaid
-flowchart TB
-
-subgraph global
- style hsOfficeBankAccount fill: #e9f7ef
-
- role:global.admin[global.admin]
-end
-
-subgraph hsOfficeBankAccount
- direction TB
- style hsOfficeBankAccount fill: #e9f7ef
-
- user:hsOfficeBankAccount.creator([bankAccount.creator])
-
- role:hsOfficeBankAccount.owner[[bankAccount.owner]]
- %% permissions
- role:hsOfficeBankAccount.owner --> perm:hsOfficeBankAccount.*{{hsOfficeBankAccount.delete}}
- %% incoming
- role:global.admin --> role:hsOfficeBankAccount.owner
- user:hsOfficeBankAccount.creator ---> role:hsOfficeBankAccount.owner
-
- role:hsOfficeBankAccount.admin[[bankAccount.admin]]
- %% incoming
- role:hsOfficeBankAccount.owner ---> role:hsOfficeBankAccount.admin
-
- role:hsOfficeBankAccount.tenant[[bankAccount.tenant]]
- %% incoming
- role:hsOfficeBankAccount.admin ---> role:hsOfficeBankAccount.tenant
-
- role:hsOfficeBankAccount.guest[[bankAccount.guest]]
- %% permissions
- role:hsOfficeBankAccount.guest --> perm:hsOfficeBankAccount.view{{hsOfficeBankAccount.view}}
- %% incoming
- role:hsOfficeBankAccount.tenant ---> role:hsOfficeBankAccount.guest
-end
-```
-
diff --git a/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.sql b/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.sql
deleted file mode 100644
index 148e0ee2..00000000
--- a/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.sql
+++ /dev/null
@@ -1,139 +0,0 @@
---liquibase formatted sql
-
--- ============================================================================
---changeset hs-office-bankaccount-rbac-OBJECT:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRelatedRbacObject('hs_office_bankaccount');
---//
-
-
--- ============================================================================
---changeset hs-office-bankaccount-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacRoleDescriptors('hsOfficeBankAccount', 'hs_office_bankaccount');
---//
-
-
--- ============================================================================
---changeset hs-office-bankaccount-rbac-ROLES-CREATION:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-
-/*
- Creates the roles and their assignments for a new bankaccount for the AFTER INSERT TRIGGER.
- */
-
-create or replace function createRbacRolesForHsOfficeBankAccount()
- returns trigger
- language plpgsql
- strict as $$
-begin
- if TG_OP <> 'INSERT' then
- raise exception 'invalid usage of TRIGGER AFTER INSERT';
- end if;
-
- perform createRoleWithGrants(
- hsOfficeBankAccountOwner(NEW),
- permissions => array['delete'],
- incomingSuperRoles => array[globalAdmin()],
- userUuids => array[currentUserUuid()],
- grantedByRole => globalAdmin()
- );
-
- perform createRoleWithGrants(
- hsOfficeBankAccountAdmin(NEW),
- incomingSuperRoles => array[hsOfficeBankAccountOwner(NEW)]
- );
-
- perform createRoleWithGrants(
- hsOfficeBankAccountTenant(NEW),
- incomingSuperRoles => array[hsOfficeBankAccountAdmin(NEW)]
- );
-
- perform createRoleWithGrants(
- hsOfficeBankAccountGuest(NEW),
- permissions => array['view'],
- incomingSuperRoles => array[hsOfficeBankAccountTenant(NEW)]
- );
-
- return NEW;
-end; $$;
-
-/*
- An AFTER INSERT TRIGGER which creates the role structure for a new customer.
- */
-
-create trigger createRbacRolesForHsOfficeBankAccount_Trigger
- after insert
- on hs_office_bankaccount
- for each row
-execute procedure createRbacRolesForHsOfficeBankAccount();
---//
-
-
--- ============================================================================
---changeset hs-office-bankaccount-rbac-IDENTITY-VIEW:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-
-call generateRbacIdentityView('hs_office_bankaccount', $idName$
- target.holder
- $idName$);
---//
-
-
--- ============================================================================
---changeset hs-office-bankaccount-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacRestrictedView('hs_office_bankaccount', 'target.holder',
- $updates$
- holder = new.holder,
- iban = new.iban,
- bic = new.bic
- $updates$);
---/
-
-
--- ============================================================================
---changeset hs-office-bankaccount-rbac-NEW-BANKACCOUNT:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-/*
- Creates a global permission for new-bankaccount and assigns it to the hostsharing admins role.
- */
-do language plpgsql $$
- declare
- addCustomerPermissions uuid[];
- globalObjectUuid uuid;
- globalAdminRoleUuid uuid ;
- begin
- call defineContext('granting global new-bankaccount permission to global admin role', null, null, null);
-
- globalAdminRoleUuid := findRoleId(globalAdmin());
- globalObjectUuid := (select uuid from global);
- addCustomerPermissions := createPermissions(globalObjectUuid, array ['new-bankaccount']);
- call grantPermissionsToRole(globalAdminRoleUuid, addCustomerPermissions);
- end;
-$$;
-
-/**
- Used by the trigger to prevent the add-customer to current user respectively assumed roles.
- */
-create or replace function addHsOfficeBankAccountNotAllowedForCurrentSubjects()
- returns trigger
- language PLPGSQL
-as $$
-begin
- raise exception '[403] new-bankaccount 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_bankaccount_insert_trigger
- before insert
- on hs_office_bankaccount
- for each row
- -- TODO.spec: who is allowed to create new bankaccounts
- when ( not hasAssumedRole() )
-execute procedure addHsOfficeBankAccountNotAllowedForCurrentSubjects();
---//
-
diff --git a/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.md b/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.md
deleted file mode 100644
index 78bb7751..00000000
--- a/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.md
+++ /dev/null
@@ -1,71 +0,0 @@
-### hs_office_sepaMandate RBAC
-
-```mermaid
-flowchart TB
-
-subgraph global
- style global fill:#eee
-
- role:global.admin[global.admin]
-end
-
-subgraph hsOfficeBankAccount
- direction TB
- style hsOfficeBankAccount fill:#eee
-
- role:hsOfficeBankAccount.owner[bankAccount.owner]
- --> role:hsOfficeBankAccount.admin[bankAccount.admin]
- --> role:hsOfficeBankAccount.tenant[bankAccount.tenant]
- --> role:hsOfficeBankAccount.guest[bankAccount.guest]
-end
-
-subgraph hsOfficeDebitor
- direction TB
- style hsOfficeDebitor fill:#eee
-
- role:hsOfficeDebitor.owner[debitor.admin]
- --> role:hsOfficeDebitor.admin[debitor.admin]
- --> role:hsOfficeDebitor.agent[debitor.agent]
- --> role:hsOfficeDebitor.tenant[debitor.tenant]
- --> role:hsOfficeDebitor.guest[debitor.guest]
-end
-
-subgraph hsOfficeSepaMandate
-
- role:hsOfficeSepaMandate.owner[sepaMandate.owner]
- %% permissions
- role:hsOfficeSepaMandate.owner --> perm:hsOfficeSepaMandate.*{{sepaMandate.*}}
- %% incoming
- role:global.admin ---> role:hsOfficeSepaMandate.owner
-
- role:hsOfficeSepaMandate.admin[sepaMandate.admin]
- %% permissions
- role:hsOfficeSepaMandate.admin --> perm:hsOfficeSepaMandate.edit{{sepaMandate.edit}}
- %% incoming
- role:hsOfficeSepaMandate.owner ---> role:hsOfficeSepaMandate.admin
-
- role:hsOfficeSepaMandate.agent[sepaMandate.agent]
- %% incoming
- role:hsOfficeSepaMandate.admin ---> role:hsOfficeSepaMandate.agent
- role:hsOfficeDebitor.admin --> role:hsOfficeSepaMandate.agent
- role:hsOfficeBankAccount.admin --> role:hsOfficeSepaMandate.agent
- %% outgoing
- role:hsOfficeSepaMandate.agent --> role:hsOfficeDebitor.tenant
- role:hsOfficeSepaMandate.admin --> role:hsOfficeBankAccount.tenant
-
- role:hsOfficeSepaMandate.tenant[sepaMandate.tenant]
- %% incoming
- role:hsOfficeSepaMandate.agent --> role:hsOfficeSepaMandate.tenant
- %% outgoing
- role:hsOfficeSepaMandate.tenant --> role:hsOfficeDebitor.guest
- role:hsOfficeSepaMandate.tenant --> role:hsOfficeBankAccount.guest
-
- role:hsOfficeSepaMandate.guest[sepaMandate.guest]
- %% permissions
- role:hsOfficeSepaMandate.guest --> perm:hsOfficeSepaMandate.view{{sepaMandate.view}}
- %% incoming
- role:hsOfficeSepaMandate.tenant --> role:hsOfficeSepaMandate.guest
-end
-
-
-```
diff --git a/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.sql b/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.sql
deleted file mode 100644
index 02895c48..00000000
--- a/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.sql
+++ /dev/null
@@ -1,158 +0,0 @@
---liquibase formatted sql
-
--- ============================================================================
---changeset hs-office-sepamandate-rbac-OBJECT:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRelatedRbacObject('hs_office_sepamandate');
---//
-
-
--- ============================================================================
---changeset hs-office-sepamandate-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacRoleDescriptors('hsOfficeSepaMandate', 'hs_office_sepamandate');
---//
-
-
--- ============================================================================
---changeset hs-office-sepamandate-rbac-ROLES-CREATION:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-
-/*
- Creates and updates the roles and their assignments for sepaMandate entities.
- */
-
-create or replace function hsOfficeSepaMandateRbacRolesTrigger()
- returns trigger
- language plpgsql
- strict as $$
-declare
- newHsOfficeDebitor hs_office_debitor;
- newHsOfficeBankAccount hs_office_bankAccount;
-begin
- call enterTriggerForObjectUuid(NEW.uuid);
-
- select * from hs_office_debitor as p where p.uuid = NEW.debitorUuid into newHsOfficeDebitor;
- select * from hs_office_bankAccount as c where c.uuid = NEW.bankAccountUuid into newHsOfficeBankAccount;
-
- if TG_OP = 'INSERT' then
-
- -- === ATTENTION: code generated from related Mermaid flowchart: ===
-
- perform createRoleWithGrants(
- hsOfficeSepaMandateOwner(NEW),
- permissions => array['*'],
- incomingSuperRoles => array[globalAdmin()]
- );
-
- perform createRoleWithGrants(
- hsOfficeSepaMandateAdmin(NEW),
- permissions => array['edit'],
- incomingSuperRoles => array[hsOfficeSepaMandateOwner(NEW)],
- outgoingSubRoles => array[hsOfficeBankAccountTenant(newHsOfficeBankAccount)]
- );
-
- perform createRoleWithGrants(
- hsOfficeSepaMandateAgent(NEW),
- incomingSuperRoles => array[hsOfficeSepaMandateAdmin(NEW), hsOfficeDebitorAdmin(newHsOfficeDebitor), hsOfficeBankAccountAdmin(newHsOfficeBankAccount)],
- outgoingSubRoles => array[hsOfficeDebitorTenant(newHsOfficeDebitor)]
- );
-
- perform createRoleWithGrants(
- hsOfficeSepaMandateTenant(NEW),
- incomingSuperRoles => array[hsOfficeSepaMandateAgent(NEW)],
- outgoingSubRoles => array[hsOfficeDebitorGuest(newHsOfficeDebitor), hsOfficeBankAccountGuest(newHsOfficeBankAccount)]
- );
-
- perform createRoleWithGrants(
- hsOfficeSepaMandateGuest(NEW),
- permissions => array['view'],
- incomingSuperRoles => array[hsOfficeSepaMandateTenant(NEW)]
- );
-
- -- === END of code generated from Mermaid flowchart. ===
-
- else
- raise exception 'invalid usage of TRIGGER';
- end if;
-
- call leaveTriggerForObjectUuid(NEW.uuid);
- return NEW;
-end; $$;
-
-/*
- An AFTER INSERT TRIGGER which creates the role structure for a new customer.
- */
-create trigger createRbacRolesForHsOfficeSepaMandate_Trigger
- after insert
- on hs_office_sepamandate
- for each row
-execute procedure hsOfficeSepaMandateRbacRolesTrigger();
---//
-
-
--- ============================================================================
---changeset hs-office-sepamandate-rbac-IDENTITY-VIEW:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacIdentityView('hs_office_sepamandate', idNameExpression => 'target.reference');
---//
-
-
--- ============================================================================
---changeset hs-office-sepamandate-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacRestrictedView('hs_office_sepamandate',
- orderby => 'target.reference',
- columnUpdates => $updates$
- reference = new.reference,
- agreement = new.agreement,
- validity = new.validity
- $updates$);
---//
-
-
--- ============================================================================
---changeset hs-office-sepamandate-rbac-NEW-SepaMandate:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-/*
- Creates a global permission for new-sepaMandate and assigns it to the hostsharing admins role.
- */
-do language plpgsql $$
- declare
- addCustomerPermissions uuid[];
- globalObjectUuid uuid;
- globalAdminRoleUuid uuid ;
- begin
- call defineContext('granting global new-sepaMandate permission to global admin role', null, null, null);
-
- globalAdminRoleUuid := findRoleId(globalAdmin());
- globalObjectUuid := (select uuid from global);
- addCustomerPermissions := createPermissions(globalObjectUuid, array ['new-sepamandate']);
- call grantPermissionsToRole(globalAdminRoleUuid, addCustomerPermissions);
- end;
-$$;
-
-/**
- Used by the trigger to prevent the add-customer to current user respectively assumed roles.
- */
-create or replace function addHsOfficeSepaMandateNotAllowedForCurrentSubjects()
- returns trigger
- language PLPGSQL
-as $$
-begin
- raise exception '[403] new-sepaMandate 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_sepamandate_insert_trigger
- before insert
- on hs_office_sepamandate
- for each row
- -- TODO.spec: who is allowed to create new sepaMandates
- when ( not hasAssumedRole() )
-execute procedure addHsOfficeSepaMandateNotAllowedForCurrentSubjects();
---//
-
diff --git a/src/main/resources/db/changelog/258-hs-office-sepamandate-test-data.sql b/src/main/resources/db/changelog/258-hs-office-sepamandate-test-data.sql
deleted file mode 100644
index eb96d1a0..00000000
--- a/src/main/resources/db/changelog/258-hs-office-sepamandate-test-data.sql
+++ /dev/null
@@ -1,51 +0,0 @@
---liquibase formatted sql
-
-
--- ============================================================================
---changeset hs-office-sepaMandate-TEST-DATA-GENERATOR:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-
-/*
- Creates a single sepaMandate test record.
- */
-create or replace procedure createHsOfficeSepaMandateTestData( tradeNameAndHolderName varchar )
- language plpgsql as $$
-declare
- currentTask varchar;
- idName varchar;
- relatedDebitor hs_office_debitor;
- relatedBankAccount hs_office_bankAccount;
-begin
- idName := cleanIdentifier( tradeNameAndHolderName);
- currentTask := 'creating SEPA-mandate test-data ' || idName;
- call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global.admin');
- execute format('set local hsadminng.currentTask to %L', currentTask);
-
- select debitor.* from hs_office_debitor debitor
- join hs_office_partner parter on parter.uuid = debitor.partnerUuid
- join hs_office_person person on person.uuid = parter.personUuid
- where person.tradeName = tradeNameAndHolderName into relatedDebitor;
- select c.* from hs_office_bankAccount c where c.holder = tradeNameAndHolderName into relatedBankAccount;
-
- raise notice 'creating test SEPA-mandate: %', idName;
- raise notice '- using debitor (%): %', relatedDebitor.uuid, relatedDebitor;
- raise notice '- using bankAccount (%): %', relatedBankAccount.uuid, relatedBankAccount;
- insert
- into hs_office_sepamandate (uuid, debitoruuid, bankAccountuuid, reference, agreement, validity)
- values (uuid_generate_v4(), relatedDebitor.uuid, relatedBankAccount.uuid, 'ref'||idName, '20220930', daterange('20221001' , '20261231', '[]'));
-end; $$;
---//
-
-
--- ============================================================================
---changeset hs-office-sepaMandate-TEST-DATA-GENERATION:1 –context=dev,tc endDelimiter:--//
--- ----------------------------------------------------------------------------
-
-do language plpgsql $$
- begin
- call createHsOfficeSepaMandateTestData('First GmbH');
- call createHsOfficeSepaMandateTestData('Second e.K.');
- call createHsOfficeSepaMandateTestData('Third OHG');
- end;
-$$;
---//
diff --git a/src/main/resources/db/changelog/273-hs-office-debitor-rbac.md b/src/main/resources/db/changelog/273-hs-office-debitor-rbac.md
deleted file mode 100644
index 6830a7b1..00000000
--- a/src/main/resources/db/changelog/273-hs-office-debitor-rbac.md
+++ /dev/null
@@ -1,250 +0,0 @@
-### hs_office_debitor RBAC Roles
-
-```mermaid
-flowchart TB
-
-subgraph global
- style global fill:#eee
-
- role:global.admin[global.admin]
-end
-
-subgraph office
- style office fill:#eee
-
- subgraph sepa
-
- subgraph bankaccount
- style bankaccount fill: #e9f7ef
-
- user:hsOfficeBankAccount.creator([bankaccount.creator])
-
- role:hsOfficeBankAccount.owner[bankaccount.owner]
- %% permissions
- role:hsOfficeBankAccount.owner --> perm:hsOfficeBankAccount.*{{bankaccount.*}}
- %% incoming
- role:global.admin --> role:hsOfficeBankAccount.owner
- user:hsOfficeBankAccount.creator ---> role:hsOfficeBankAccount.owner
-
- role:hsOfficeBankAccount.admin[bankaccount.admin]
- %% permissions
- role:hsOfficeBankAccount.admin --> perm:hsOfficeBankAccount.edit{{bankaccount.edit}}
- %% incoming
- role:hsOfficeBankAccount.owner ---> role:hsOfficeBankAccount.admin
-
- role:hsOfficeBankAccount.tenant[bankaccount.tenant]
- %% incoming
- role:hsOfficeBankAccount.admin ---> role:hsOfficeBankAccount.tenant
-
- role:hsOfficeBankAccount.guest[bankaccount.guest]
- %% permissions
- role:hsOfficeBankAccount.guest --> perm:hsOfficeBankAccount.view{{bankaccount.view}}
- %% incoming
- role:hsOfficeBankAccount.tenant ---> role:hsOfficeBankAccount.guest
- end
-
- subgraph hsOfficeSepaMandate
- end
-
- end
-
- subgraph contact
- style contact fill: #e9f7ef
-
- user:hsOfficeContact.creator([contact.creator])
-
- role:hsOfficeContact.owner[contact.owner]
- %% permissions
- role:hsOfficeContact.owner --> perm:hsOfficeContact.*{{contact.*}}
- %% incoming
- role:global.admin --> role:hsOfficeContact.owner
- user:hsOfficeContact.creator ---> role:hsOfficeContact.owner
-
- role:hsOfficeContact.admin[contact.admin]
- %% permissions
- role:hsOfficeContact.admin ---> perm:hsOfficeContact.edit{{contact.edit}}
- %% incoming
- role:hsOfficeContact.owner ---> role:hsOfficeContact.admin
-
- role:hsOfficeContact.tenant[contact.tenant]
- %% incoming
- role:hsOfficeContact.admin ----> role:hsOfficeContact.tenant
-
- role:hsOfficeContact.guest[contact.guest]
- %% permissions
- role:hsOfficeContact.guest --> perm:hsOfficeContact.view{{contact.view}}
- %% incoming
- role:hsOfficeContact.tenant ---> role:hsOfficeContact.guest
- end
-
- subgraph partner-person
-
- subgraph person
- style person fill: #e9f7ef
-
- user:hsOfficePerson.creator([personcreator])
-
- role:hsOfficePerson.owner[person.owner]
- %% permissions
- role:hsOfficePerson.owner --> perm:hsOfficePerson.*{{person.*}}
- %% incoming
- user:hsOfficePerson.creator ---> role:hsOfficePerson.owner
- role:global.admin --> role:hsOfficePerson.owner
-
- role:hsOfficePerson.admin[person.admin]
- %% permissions
- role:hsOfficePerson.admin --> perm:hsOfficePerson.edit{{person.edit}}
- %% incoming
- role:hsOfficePerson.owner ---> role:hsOfficePerson.admin
-
- role:hsOfficePerson.tenant[person.tenant]
- %% incoming
- role:hsOfficePerson.admin -----> role:hsOfficePerson.tenant
-
- role:hsOfficePerson.guest[person.guest]
- %% permissions
- role:hsOfficePerson.guest --> perm:hsOfficePerson.edit{{person.view}}
- %% incoming
- role:hsOfficePerson.tenant ---> role:hsOfficePerson.guest
- end
-
- subgraph partner
-
- role:hsOfficePartner.owner[partner.owner]
- %% permissions
- role:hsOfficePartner.owner --> perm:hsOfficePartner.*{{partner.*}}
- %% incoming
- role:global.admin ---> role:hsOfficePartner.owner
-
- role:hsOfficePartner.admin[partner.admin]
- %% permissions
- role:hsOfficePartner.admin --> perm:hsOfficePartner.edit{{partner.edit}}
- %% incoming
- role:hsOfficePartner.owner ---> role:hsOfficePartner.admin
- %% outgoing
- role:hsOfficePartner.admin --> role:hsOfficePerson.tenant
- role:hsOfficePartner.admin --> role:hsOfficeContact.tenant
-
- role:hsOfficePartner.agent[partner.agent]
- %% incoming
- role:hsOfficePartner.admin --> role:hsOfficePartner.agent
- role:hsOfficePerson.admin --> role:hsOfficePartner.agent
- role:hsOfficeContact.admin --> role:hsOfficePartner.agent
-
- role:hsOfficePartner.tenant[partner.tenant]
- %% incoming
- role:hsOfficePartner.agent ---> role:hsOfficePartner.tenant
- %% outgoing
- role:hsOfficePartner.tenant --> role:hsOfficePerson.guest
- role:hsOfficePartner.tenant --> role:hsOfficeContact.guest
-
- role:hsOfficePartner.guest[partner.guest]
- %% permissions
- role:hsOfficePartner.guest --> perm:hsOfficePartner.view{{partner.view}}
- %% incoming
- role:hsOfficePartner.tenant ---> role:hsOfficePartner.guest
- end
-
- end
-
- subgraph debitor
- style debitor stroke-width:6px
-
- user:hsOfficeDebitor.creator([debitor.creator])
- %% created by role
- user:hsOfficeDebitor.creator --> role:hsOfficePartner.agent
-
- role:hsOfficeDebitor.owner[debitor.owner]
- %% permissions
- role:hsOfficeDebitor.owner --> perm:hsOfficeDebitor.*{{debitor.*}}
- %% incoming
- user:hsOfficeDebitor.creator --> role:hsOfficeDebitor.owner
- role:global.admin --> role:hsOfficeDebitor.owner
-
- role:hsOfficeDebitor.admin[debitor.admin]
- %% permissions
- role:hsOfficeDebitor.admin --> perm:hsOfficeDebitor.edit{{debitor.edit}}
- %% incoming
- role:hsOfficeDebitor.owner ---> role:hsOfficeDebitor.admin
-
- role:hsOfficeDebitor.agent[debitor.agent]
- %% incoming
- role:hsOfficeDebitor.admin ---> role:hsOfficeDebitor.agent
- role:hsOfficePartner.admin --> role:hsOfficeDebitor.agent
- %% outgoing
- role:hsOfficeDebitor.agent --> role:hsOfficeBankAccount.tenant
-
- role:hsOfficeDebitor.tenant[debitor.tenant]
- %% incoming
- role:hsOfficeDebitor.agent ---> role:hsOfficeDebitor.tenant
- role:hsOfficePartner.agent --> role:hsOfficeDebitor.tenant
- role:hsOfficeBankAccount.admin --> role:hsOfficeDebitor.tenant
- %% outgoing
- role:hsOfficeDebitor.tenant --> role:hsOfficePartner.tenant
- role:hsOfficeDebitor.tenant --> role:hsOfficeContact.guest
-
- role:hsOfficeDebitor.guest[debitor.guest]
- %% permissions
- role:hsOfficeDebitor.guest --> perm:hsOfficeDebitor.view{{debitor.view}}
- %% incoming
- role:hsOfficeDebitor.tenant --> role:hsOfficeDebitor.guest
- end
-
-end
-
-subgraph hsOfficeSepaMandate
-
- role:hsOfficeSepaMandate.owner[sepaMandate.owner]
- %% permissions
- role:hsOfficeSepaMandate.owner --> perm:hsOfficeSepaMandate.*{{sepaMandate.*}}
- %% incoming
- role:global.admin ---> role:hsOfficeSepaMandate.owner
-
- role:hsOfficeSepaMandate.admin[sepaMandate.admin]
- %% permissions
- role:hsOfficeSepaMandate.admin --> perm:hsOfficeSepaMandate.edit{{sepaMandate.edit}}
- %% incoming
- role:hsOfficeSepaMandate.owner ---> role:hsOfficeSepaMandate.admin
-
- role:hsOfficeSepaMandate.agent[sepaMandate.agent]
- %% incoming
- role:hsOfficeSepaMandate.admin ---> role:hsOfficeSepaMandate.agent
- role:hsOfficeDebitor.admin --> role:hsOfficeSepaMandate.agent
- role:hsOfficeBankAccount.admin --> role:hsOfficeSepaMandate.agent
- %% outgoing
- role:hsOfficeSepaMandate.agent --> role:hsOfficeDebitor.tenant
- role:hsOfficeSepaMandate.admin --> role:hsOfficeBankAccount.tenant
-
- role:hsOfficeSepaMandate.tenant[sepaMandate.tenant]
- %% incoming
- role:hsOfficeSepaMandate.agent --> role:hsOfficeSepaMandate.tenant
- %% outgoing
- role:hsOfficeSepaMandate.tenant --> role:hsOfficeDebitor.guest
- role:hsOfficeSepaMandate.tenant --> role:hsOfficeBankAccount.guest
-
- role:hsOfficeSepaMandate.guest[sepaMandate.guest]
- %% permissions
- role:hsOfficeSepaMandate.guest --> perm:hsOfficeSepaMandate.view{{sepaMandate.view}}
- %% incoming
- role:hsOfficeSepaMandate.tenant --> role:hsOfficeSepaMandate.guest
-end
-
-subgraph hosting
- style hosting fill:#eee
-
- subgraph package
- style package fill: #e9f7ef
-
- role:package.owner[package.owner]
- --> role:package.admin[package.admin]
- --> role:package.tenant[package.tenant]
-
- role:hsOfficeDebitor.agent --> role:package.owner
- role:package.admin --> role:hsOfficeDebitor.tenant
- role:hsOfficePartner.tenant --> role:hsOfficeDebitor.guest
- end
-end
-
-
-```
-
diff --git a/src/main/resources/db/changelog/273-hs-office-debitor-rbac.sql b/src/main/resources/db/changelog/273-hs-office-debitor-rbac.sql
deleted file mode 100644
index 30573125..00000000
--- a/src/main/resources/db/changelog/273-hs-office-debitor-rbac.sql
+++ /dev/null
@@ -1,247 +0,0 @@
---liquibase formatted sql
-
--- ============================================================================
---changeset hs-office-debitor-rbac-OBJECT:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRelatedRbacObject('hs_office_debitor');
---//
-
-
--- ============================================================================
---changeset hs-office-debitor-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacRoleDescriptors('hsOfficeDebitor', 'hs_office_debitor');
---//
-
-
--- ============================================================================
---changeset hs-office-debitor-rbac-ROLES-CREATION:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-
-/*
- Creates and updates the roles and their assignments for debitor entities.
- */
-
-create or replace function hsOfficeDebitorRbacRolesTrigger()
- returns trigger
- language plpgsql
- strict as $$
-declare
- hsOfficeDebitorTenant RbacRoleDescriptor;
- oldPartner hs_office_partner;
- newPartner hs_office_partner;
- newPerson hs_office_person;
- oldContact hs_office_contact;
- newContact hs_office_contact;
- newBankAccount hs_office_bankaccount;
- oldBankAccount hs_office_bankaccount;
-begin
- call enterTriggerForObjectUuid(NEW.uuid);
-
- hsOfficeDebitorTenant := hsOfficeDebitorTenant(NEW);
-
- select * from hs_office_partner as p where p.uuid = NEW.partnerUuid into newPartner;
- select * from hs_office_person as p where p.uuid = newPartner.personUuid into newPerson;
- select * from hs_office_contact as c where c.uuid = NEW.billingContactUuid into newContact;
- select * from hs_office_bankaccount as b where b.uuid = NEW.refundBankAccountUuid into newBankAccount;
- if TG_OP = 'INSERT' then
-
-
- perform createRoleWithGrants(
- hsOfficeDebitorOwner(NEW),
- permissions => array['*'],
- incomingSuperRoles => array[globalAdmin()],
- userUuids => array[currentUserUuid()],
- grantedByRole => globalAdmin()
- );
-
- perform createRoleWithGrants(
- hsOfficeDebitorAdmin(NEW),
- permissions => array['edit'],
- incomingSuperRoles => array[hsOfficeDebitorOwner(NEW)]
- );
-
- perform createRoleWithGrants(
- hsOfficeDebitorAgent(NEW),
- incomingSuperRoles => array[
- hsOfficeDebitorAdmin(NEW),
- hsOfficePartnerAdmin(newPartner),
- hsOfficeContactAdmin(newContact)],
- outgoingSubRoles => array[
- hsOfficeBankAccountTenant(newBankaccount)]
- );
-
- perform createRoleWithGrants(
- hsOfficeDebitorTenant(NEW),
- incomingSuperRoles => array[
- hsOfficeDebitorAgent(NEW),
- hsOfficePartnerAgent(newPartner),
- hsOfficeBankAccountAdmin(newBankaccount)],
- outgoingSubRoles => array[
- hsOfficePartnerTenant(newPartner),
- hsOfficeContactGuest(newContact),
- hsOfficeBankAccountGuest(newBankaccount)]
- );
-
- perform createRoleWithGrants(
- hsOfficeDebitorGuest(NEW),
- permissions => array['view'],
- incomingSuperRoles => array[
- hsOfficeDebitorTenant(NEW)]
- );
-
- elsif TG_OP = 'UPDATE' then
-
- if OLD.partnerUuid <> NEW.partnerUuid then
- select * from hs_office_partner as p where p.uuid = OLD.partnerUuid into oldPartner;
-
- call revokeRoleFromRole(hsOfficeDebitorAgent(OLD), hsOfficePartnerAdmin(oldPartner));
- call grantRoleToRole(hsOfficeDebitorAgent(NEW), hsOfficePartnerAdmin(newPartner));
-
- call revokeRoleFromRole(hsOfficeDebitorTenant(OLD), hsOfficePartnerAgent(oldPartner));
- call grantRoleToRole(hsOfficeDebitorTenant(NEW), hsOfficePartnerAgent(newPartner));
-
- call revokeRoleFromRole(hsOfficePartnerTenant(oldPartner), hsOfficeDebitorTenant(OLD));
- call grantRoleToRole(hsOfficePartnerTenant(newPartner), hsOfficeDebitorTenant(NEW));
- end if;
-
- if OLD.billingContactUuid <> NEW.billingContactUuid then
- select * from hs_office_contact as c where c.uuid = OLD.billingContactUuid into oldContact;
-
- call revokeRoleFromRole(hsOfficeDebitorAgent(OLD), hsOfficeContactAdmin(oldContact));
- call grantRoleToRole(hsOfficeDebitorAgent(NEW), hsOfficeContactAdmin(newContact));
-
- call revokeRoleFromRole(hsOfficeContactGuest(oldContact), hsOfficeDebitorTenant(OLD));
- call grantRoleToRole(hsOfficeContactGuest(newContact), hsOfficeDebitorTenant(NEW));
- end if;
-
- if (OLD.refundBankAccountUuid is not null or NEW.refundBankAccountUuid is not null) and
- ( OLD.refundBankAccountUuid is null or NEW.refundBankAccountUuid is null or
- OLD.refundBankAccountUuid <> NEW.refundBankAccountUuid ) then
-
- select * from hs_office_bankaccount as b where b.uuid = OLD.refundBankAccountUuid into oldBankAccount;
-
- if oldBankAccount is not null then
- call revokeRoleFromRole(hsOfficeBankAccountTenant(oldBankaccount), hsOfficeDebitorAgent(OLD));
- end if;
- if newBankAccount is not null then
- call grantRoleToRole(hsOfficeBankAccountTenant(newBankaccount), hsOfficeDebitorAgent(NEW));
- end if;
-
- if oldBankAccount is not null then
- call revokeRoleFromRole(hsOfficeDebitorTenant(OLD), hsOfficeBankAccountAdmin(oldBankaccount));
- end if;
- if newBankAccount is not null then
- call grantRoleToRole(hsOfficeDebitorTenant(NEW), hsOfficeBankAccountAdmin(newBankaccount));
- end if;
-
- if oldBankAccount is not null then
- call revokeRoleFromRole(hsOfficeBankAccountGuest(oldBankaccount), hsOfficeDebitorTenant(OLD));
- end if;
- if newBankAccount is not null then
- call grantRoleToRole(hsOfficeBankAccountGuest(newBankaccount), hsOfficeDebitorTenant(NEW));
- end if;
- end if;
- else
- raise exception 'invalid usage of TRIGGER';
- end if;
-
- call leaveTriggerForObjectUuid(NEW.uuid);
- return NEW;
-end; $$;
-
-/*
- An AFTER INSERT TRIGGER which creates the role structure for a new debitor.
- */
-create trigger createRbacRolesForHsOfficeDebitor_Trigger
- after insert
- on hs_office_debitor
- for each row
-execute procedure hsOfficeDebitorRbacRolesTrigger();
-
-/*
- An AFTER UPDATE TRIGGER which updates the role structure of a debitor.
- */
-create trigger updateRbacRolesForHsOfficeDebitor_Trigger
- after update
- on hs_office_debitor
- for each row
-execute procedure hsOfficeDebitorRbacRolesTrigger();
---//
-
-
--- ============================================================================
---changeset hs-office-debitor-rbac-IDENTITY-VIEW:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacIdentityView('hs_office_debitor', $idName$
- '#' ||
- (select partnerNumber from hs_office_partner p where p.uuid = target.partnerUuid) ||
- to_char(debitorNumberSuffix, 'fm00') ||
- ':' || (select split_part(idName, ':', 2) from hs_office_partner_iv pi where pi.uuid = target.partnerUuid)
- $idName$);
---//
-
-
--- ============================================================================
---changeset hs-office-debitor-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacRestrictedView('hs_office_debitor', 'target.debitorNumberSuffix',
- $updates$
- partnerUuid = new.partnerUuid, -- TODO: remove? should never do anything
- billable = new.billable,
- billingContactUuid = new.billingContactUuid,
- debitorNumberSuffix = new.debitorNumberSuffix, -- TODO: Should it be allowed to updated this value?
- refundBankAccountUuid = new.refundBankAccountUuid,
- vatId = new.vatId,
- vatCountryCode = new.vatCountryCode,
- vatBusiness = new.vatBusiness,
- vatreversecharge = new.vatreversecharge,
- defaultPrefix = new.defaultPrefix -- TODO: Should it be allowed to updated this value?
- $updates$);
---//
-
--- ============================================================================
---changeset hs-office-debitor-rbac-NEW-DEBITOR:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-/*
- Creates a global permission for new-debitor and assigns it to the hostsharing admins role.
- */
-do language plpgsql $$
- declare
- addDebitorPermissions uuid[];
- globalObjectUuid uuid;
- globalAdminRoleUuid uuid ;
- begin
- call defineContext('granting global new-debitor permission to global admin role', null, null, null);
-
- globalAdminRoleUuid := findRoleId(globalAdmin());
- globalObjectUuid := (select uuid from global);
- addDebitorPermissions := createPermissions(globalObjectUuid, array ['new-debitor']);
- call grantPermissionsToRole(globalAdminRoleUuid, addDebitorPermissions);
- end;
-$$;
-
-/**
- Used by the trigger to prevent the add-debitor to current user respectively assumed roles.
- */
-create or replace function addHsOfficeDebitorNotAllowedForCurrentSubjects()
- returns trigger
- language PLPGSQL
-as $$
-begin
- raise exception '[403] new-debitor not permitted for %',
- array_to_string(currentSubjects(), ';', 'null');
-end; $$;
-
-/**
- Checks if the user or assumed roles are allowed to create a new debitor.
- */
-create trigger hs_office_debitor_insert_trigger
- before insert
- on hs_office_debitor
- for each row
- -- TODO.spec: who is allowed to create new debitors
- when ( not hasAssumedRole() )
-execute procedure addHsOfficeDebitorNotAllowedForCurrentSubjects();
---//
-
diff --git a/src/main/resources/db/changelog/278-hs-office-debitor-test-data.sql b/src/main/resources/db/changelog/278-hs-office-debitor-test-data.sql
deleted file mode 100644
index af75d074..00000000
--- a/src/main/resources/db/changelog/278-hs-office-debitor-test-data.sql
+++ /dev/null
@@ -1,57 +0,0 @@
---liquibase formatted sql
-
-
--- ============================================================================
---changeset hs-office-debitor-TEST-DATA-GENERATOR:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-
-/*
- Creates a single debitor test record.
- */
-create or replace procedure createHsOfficeDebitorTestData(
- debitorNumberSuffix numeric(5),
- partnerTradeName varchar,
- billingContactLabel varchar,
- defaultPrefix varchar
- )
- language plpgsql as $$
-declare
- currentTask varchar;
- idName varchar;
- relatedPartner hs_office_partner;
- relatedContact hs_office_contact;
- relatedBankAccountUuid uuid;
-begin
- idName := cleanIdentifier( partnerTradeName|| '-' || billingContactLabel);
- currentTask := 'creating debitor test-data ' || idName;
- call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global.admin');
- execute format('set local hsadminng.currentTask to %L', currentTask);
-
- select partner.* from hs_office_partner partner
- join hs_office_person person on person.uuid = partner.personUuid
- where person.tradeName = partnerTradeName into relatedPartner;
- select c.* from hs_office_contact c where c.label = billingContactLabel into relatedContact;
- select b.uuid from hs_office_bankaccount b where b.holder = partnerTradeName into relatedBankAccountUuid;
-
- raise notice 'creating test debitor: % (#%)', idName, debitorNumberSuffix;
- raise notice '- using partner (%): %', relatedPartner.uuid, relatedPartner;
- raise notice '- using billingContact (%): %', relatedContact.uuid, relatedContact;
- insert
- into hs_office_debitor (uuid, partneruuid, debitornumbersuffix, billable, billingcontactuuid, vatbusiness, vatreversecharge, refundbankaccountuuid, defaultprefix)
- values (uuid_generate_v4(), relatedPartner.uuid, debitorNumberSuffix, true, relatedContact.uuid, true, false, relatedBankAccountUuid, defaultPrefix);
-end; $$;
---//
-
-
--- ============================================================================
---changeset hs-office-debitor-TEST-DATA-GENERATION:1 –context=dev,tc endDelimiter:--//
--- ----------------------------------------------------------------------------
-
-do language plpgsql $$
- begin
- call createHsOfficeDebitorTestData(11, 'First GmbH', 'first contact', 'fir');
- call createHsOfficeDebitorTestData(12, 'Second e.K.', 'second contact', 'sec');
- call createHsOfficeDebitorTestData(13, 'Third OHG', 'third contact', 'thi');
- end;
-$$;
---//
diff --git a/src/main/resources/db/changelog/303-hs-office-membership-rbac.md b/src/main/resources/db/changelog/303-hs-office-membership-rbac.md
deleted file mode 100644
index 8cf604ab..00000000
--- a/src/main/resources/db/changelog/303-hs-office-membership-rbac.md
+++ /dev/null
@@ -1,75 +0,0 @@
-### hs_office_membership RBAC
-
-```mermaid
-flowchart TB
-
-subgraph global
- style global fill:#eee
-
- role:global.admin[global.admin]
-end
-
-subgraph hsOfficeDebitor
- direction TB
- style hsOfficeDebitor fill:#eee
-
- role:hsOfficeDebitor.owner[debitor.owner]
- --> role:hsOfficeDebitor.admin[debitor.admin]
- --> role:hsOfficeDebitor.tenant[debitor.tenant]
- --> role:hsOfficeDebitor.guest[debitor.guest]
-end
-
-subgraph hsOfficePartner
- direction TB
- style hsOfficePartner fill:#eee
-
- role:hsOfficePartner.owner[partner.admin]
- --> role:hsOfficePartner.admin[partner.admin]
- --> role:hsOfficePartner.agent[partner.agent]
- --> role:hsOfficePartner.tenant[partner.tenant]
- --> role:hsOfficePartner.guest[partner.guest]
-end
-
-subgraph hsOfficeMembership
-
- role:hsOfficeMembership.owner[membership.owner]
- %% permissions
- role:hsOfficeMembership.owner --> perm:hsOfficeMembership.*{{membership.*}}
- %% incoming
- role:global.admin ---> role:hsOfficeMembership.owner
-
- role:hsOfficeMembership.admin[membership.admin]
- %% permissions
- role:hsOfficeMembership.admin --> perm:hsOfficeMembership.edit{{membership.edit}}
- %% incoming
- role:hsOfficeMembership.owner ---> role:hsOfficeMembership.admin
-
- role:hsOfficeMembership.agent[membership.agent]
- %% incoming
- role:hsOfficeMembership.admin ---> role:hsOfficeMembership.agent
- role:hsOfficePartner.admin --> role:hsOfficeMembership.agent
- role:hsOfficeDebitor.admin --> role:hsOfficeMembership.agent
- %% outgoing
- role:hsOfficeMembership.agent --> role:hsOfficePartner.tenant
- role:hsOfficeMembership.agent --> role:hsOfficeDebitor.tenant
-
- role:hsOfficeMembership.tenant[membership.tenant]
- %% incoming
- role:hsOfficeMembership.agent --> role:hsOfficeMembership.tenant
- role:hsOfficePartner.agent --> role:hsOfficeMembership.tenant
- role:hsOfficeDebitor.agent --> role:hsOfficeMembership.tenant
- %% outgoing
- role:hsOfficeMembership.tenant --> role:hsOfficePartner.guest
- role:hsOfficeMembership.tenant --> role:hsOfficeDebitor.guest
-
- role:hsOfficeMembership.guest[membership.guest]
- %% permissions
- role:hsOfficeMembership.guest --> perm:hsOfficeMembership.view{{membership.view}}
- %% incoming
- role:hsOfficeMembership.tenant --> role:hsOfficeMembership.guest
- role:hsOfficePartner.tenant --> role:hsOfficeMembership.guest
- role:hsOfficeDebitor.tenant --> role:hsOfficeMembership.guest
-end
-
-
-```
diff --git a/src/main/resources/db/changelog/303-hs-office-membership-rbac.sql b/src/main/resources/db/changelog/303-hs-office-membership-rbac.sql
deleted file mode 100644
index 949f939c..00000000
--- a/src/main/resources/db/changelog/303-hs-office-membership-rbac.sql
+++ /dev/null
@@ -1,162 +0,0 @@
---liquibase formatted sql
-
--- ============================================================================
---changeset hs-office-membership-rbac-OBJECT:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRelatedRbacObject('hs_office_membership');
---//
-
-
--- ============================================================================
---changeset hs-office-membership-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacRoleDescriptors('hsOfficeMembership', 'hs_office_membership');
---//
-
-
--- ============================================================================
---changeset hs-office-membership-rbac-ROLES-CREATION:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-
-/*
- Creates and updates the roles and their assignments for membership entities.
- */
-
-create or replace function hsOfficeMembershipRbacRolesTrigger()
- returns trigger
- language plpgsql
- strict as $$
-declare
- newHsOfficePartner hs_office_partner;
- newHsOfficeDebitor hs_office_debitor;
-begin
- call enterTriggerForObjectUuid(NEW.uuid);
-
- select * from hs_office_partner as p where p.uuid = NEW.partnerUuid into newHsOfficePartner;
- select * from hs_office_debitor as c where c.uuid = NEW.mainDebitorUuid into newHsOfficeDebitor;
-
- if TG_OP = 'INSERT' then
-
- -- === ATTENTION: code generated from related Mermaid flowchart: ===
-
- perform createRoleWithGrants(
- hsOfficeMembershipOwner(NEW),
- permissions => array['*'],
- incomingSuperRoles => array[globalAdmin()]
- );
-
- perform createRoleWithGrants(
- hsOfficeMembershipAdmin(NEW),
- permissions => array['edit'],
- incomingSuperRoles => array[hsOfficeMembershipOwner(NEW)]
- );
-
- perform createRoleWithGrants(
- hsOfficeMembershipAgent(NEW),
- incomingSuperRoles => array[hsOfficeMembershipAdmin(NEW), hsOfficePartnerAdmin(newHsOfficePartner), hsOfficeDebitorAdmin(newHsOfficeDebitor)],
- outgoingSubRoles => array[hsOfficePartnerTenant(newHsOfficePartner), hsOfficeDebitorTenant(newHsOfficeDebitor)]
- );
-
- perform createRoleWithGrants(
- hsOfficeMembershipTenant(NEW),
- incomingSuperRoles => array[hsOfficeMembershipAgent(NEW), hsOfficePartnerAgent(newHsOfficePartner), hsOfficeDebitorAgent(newHsOfficeDebitor)],
- outgoingSubRoles => array[hsOfficePartnerGuest(newHsOfficePartner), hsOfficeDebitorGuest(newHsOfficeDebitor)]
- );
-
- perform createRoleWithGrants(
- hsOfficeMembershipGuest(NEW),
- permissions => array['view'],
- incomingSuperRoles => array[hsOfficeMembershipTenant(NEW), hsOfficePartnerTenant(newHsOfficePartner), hsOfficeDebitorTenant(newHsOfficeDebitor)]
- );
-
- -- === END of code generated from Mermaid flowchart. ===
-
- else
- raise exception 'invalid usage of TRIGGER';
- end if;
-
- call leaveTriggerForObjectUuid(NEW.uuid);
- return NEW;
-end; $$;
-
-/*
- An AFTER INSERT TRIGGER which creates the role structure for a new customer.
- */
-create trigger createRbacRolesForHsOfficeMembership_Trigger
- after insert
- on hs_office_membership
- for each row
-execute procedure hsOfficeMembershipRbacRolesTrigger();
---//
-
-
--- ============================================================================
---changeset hs-office-membership-rbac-IDENTITY-VIEW:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacIdentityView('hs_office_membership', idNameExpression => $idName$
- '#' ||
- (select partnerNumber from hs_office_partner p where p.uuid = target.partnerUuid) ||
- memberNumberSuffix ||
- ':' || (select split_part(idName, ':', 2) from hs_office_partner_iv p where p.uuid = target.partnerUuid)
- $idName$);
---//
-
-
--- ============================================================================
---changeset hs-office-membership-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacRestrictedView('hs_office_membership',
- orderby => 'target.memberNumberSuffix',
- columnUpdates => $updates$
- validity = new.validity,
- reasonForTermination = new.reasonForTermination,
- membershipFeeBillable = new.membershipFeeBillable
- $updates$);
---//
-
-
--- ============================================================================
---changeset hs-office-membership-rbac-NEW-Membership:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-/*
- Creates a global permission for new-membership and assigns it to the hostsharing admins role.
- */
-do language plpgsql $$
- declare
- addCustomerPermissions uuid[];
- globalObjectUuid uuid;
- globalAdminRoleUuid uuid ;
- begin
- call defineContext('granting global new-membership permission to global admin role', null, null, null);
-
- globalAdminRoleUuid := findRoleId(globalAdmin());
- globalObjectUuid := (select uuid from global);
- addCustomerPermissions := createPermissions(globalObjectUuid, array ['new-membership']);
- call grantPermissionsToRole(globalAdminRoleUuid, addCustomerPermissions);
- end;
-$$;
-
-/**
- Used by the trigger to prevent the add-customer to current user respectively assumed roles.
- */
-create or replace function addHsOfficeMembershipNotAllowedForCurrentSubjects()
- returns trigger
- language PLPGSQL
-as $$
-begin
- raise exception '[403] new-membership 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_membership_insert_trigger
- before insert
- on hs_office_membership
- for each row
- -- TODO.spec: who is allowed to create new memberships
- when ( not hasAssumedRole() )
-execute procedure addHsOfficeMembershipNotAllowedForCurrentSubjects();
---//
-
diff --git a/src/main/resources/db/changelog/308-hs-office-membership-test-data.sql b/src/main/resources/db/changelog/308-hs-office-membership-test-data.sql
deleted file mode 100644
index 637c87ca..00000000
--- a/src/main/resources/db/changelog/308-hs-office-membership-test-data.sql
+++ /dev/null
@@ -1,56 +0,0 @@
---liquibase formatted sql
-
-
--- ============================================================================
---changeset hs-office-membership-TEST-DATA-GENERATOR:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-
-/*
- Creates a single membership test record.
- */
-create or replace procedure createHsOfficeMembershipTestData(
- forPartnerTradeName varchar,
- forMainDebitorNumberSuffix numeric,
- newMemberNumberSuffix char(2) )
- language plpgsql as $$
-declare
- currentTask varchar;
- idName varchar;
- relatedPartner hs_office_partner;
- relatedDebitor hs_office_debitor;
-begin
- idName := cleanIdentifier( forPartnerTradeName || '#' || forMainDebitorNumberSuffix);
- currentTask := 'creating Membership test-data ' || idName;
- call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global.admin');
- execute format('set local hsadminng.currentTask to %L', currentTask);
-
- select partner.* from hs_office_partner partner
- join hs_office_person person on person.uuid = partner.personUuid
- where person.tradeName = forPartnerTradeName into relatedPartner;
- select d.* from hs_office_debitor d
- where d.partneruuid = relatedPartner.uuid
- and d.debitorNumberSuffix = forMainDebitorNumberSuffix
- into relatedDebitor;
-
- raise notice 'creating test Membership: %', idName;
- raise notice '- using partner (%): %', relatedPartner.uuid, relatedPartner;
- raise notice '- using debitor (%): %', relatedDebitor.uuid, relatedDebitor;
- insert
- into hs_office_membership (uuid, partneruuid, maindebitoruuid, memberNumberSuffix, validity, reasonfortermination)
- values (uuid_generate_v4(), relatedPartner.uuid, relatedDebitor.uuid, newMemberNumberSuffix, daterange('20221001' , null, '[]'), 'NONE');
-end; $$;
---//
-
-
--- ============================================================================
---changeset hs-office-membership-TEST-DATA-GENERATION:1 –context=dev,tc endDelimiter:--//
--- ----------------------------------------------------------------------------
-
-do language plpgsql $$
- begin
- call createHsOfficeMembershipTestData('First GmbH', 11, '01');
- call createHsOfficeMembershipTestData('Second e.K.', 12, '02');
- call createHsOfficeMembershipTestData('Third OHG', 13, '03');
- end;
-$$;
---//
diff --git a/src/main/resources/db/changelog/313-hs-office-coopshares-rbac.md b/src/main/resources/db/changelog/313-hs-office-coopshares-rbac.md
deleted file mode 100644
index 4093eb2d..00000000
--- a/src/main/resources/db/changelog/313-hs-office-coopshares-rbac.md
+++ /dev/null
@@ -1,29 +0,0 @@
-### hs_office_coopSharesTransaction RBAC
-
-```mermaid
-flowchart TB
-
-subgraph hsOfficeMembership
- direction TB
- style hsOfficeMembership fill:#eee
-
- role:hsOfficeMembership.owner[membership.admin]
- --> role:hsOfficeMembership.admin[membership.admin]
- --> role:hsOfficeMembership.agent[membership.agent]
- --> role:hsOfficeMembership.tenant[membership.tenant]
- --> role:hsOfficeMembership.guest[membership.guest]
-
- role:hsOfficePartner.agent --> role:hsOfficeMembership.agent
-end
-
-subgraph hsOfficeCoopSharesTransaction
-
- role:hsOfficeMembership.admin
- --> perm:hsOfficeCoopSharesTransaction.create{{coopSharesTx.create}}
-
- role:hsOfficeMembership.agent
- --> perm:hsOfficeCoopSharesTransaction.view{{coopSharesTx.view}}
-end
-
-
-```
diff --git a/src/main/resources/db/changelog/313-hs-office-coopshares-rbac.sql b/src/main/resources/db/changelog/313-hs-office-coopshares-rbac.sql
deleted file mode 100644
index dd465d9f..00000000
--- a/src/main/resources/db/changelog/313-hs-office-coopshares-rbac.sql
+++ /dev/null
@@ -1,126 +0,0 @@
---liquibase formatted sql
-
--- ============================================================================
---changeset hs-office-coopSharesTransaction-rbac-OBJECT:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRelatedRbacObject('hs_office_coopSharesTransaction');
---//
-
-
--- ============================================================================
---changeset hs-office-coopSharesTransaction-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacRoleDescriptors('hsOfficeCoopSharesTransaction', 'hs_office_coopSharesTransaction');
---//
-
-
--- ============================================================================
---changeset hs-office-coopSharesTransaction-rbac-ROLES-CREATION:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-
-/*
- Creates and updates the permissions for coopSharesTransaction entities.
- */
-
-create or replace function hsOfficeCoopSharesTransactionRbacRolesTrigger()
- returns trigger
- language plpgsql
- strict as $$
-declare
- newHsOfficeMembership hs_office_membership;
-begin
- call enterTriggerForObjectUuid(NEW.uuid);
-
- select * from hs_office_membership as p where p.uuid = NEW.membershipUuid into newHsOfficeMembership;
-
- if TG_OP = 'INSERT' then
-
- -- Each coopSharesTransaction entity belong exactly to one membership entity
- -- and it makes little sense just to delegate coopSharesTransaction roles.
- -- Therefore, we do not create coopSharesTransaction roles at all,
- -- but instead just assign extra permissions to existing membership-roles.
-
- -- coopsharestransactions cannot be edited nor deleted, just created+viewed
- call grantPermissionsToRole(
- getRoleId(hsOfficeMembershipTenant(newHsOfficeMembership), 'fail'),
- createPermissions(NEW.uuid, array ['view'])
- );
-
- else
- raise exception 'invalid usage of TRIGGER';
- end if;
-
- call leaveTriggerForObjectUuid(NEW.uuid);
- return NEW;
-end; $$;
-
-/*
- An AFTER INSERT TRIGGER which creates the role structure for a new customer.
- */
-create trigger createRbacRolesForHsOfficeCoopSharesTransaction_Trigger
- after insert
- on hs_office_coopSharesTransaction
- for each row
-execute procedure hsOfficeCoopSharesTransactionRbacRolesTrigger();
---//
-
-
--- ============================================================================
---changeset hs-office-coopSharesTransaction-rbac-IDENTITY-VIEW:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacIdentityView('hs_office_coopSharesTransaction',
- idNameExpression => 'target.reference');
---//
-
-
--- ============================================================================
---changeset hs-office-coopSharesTransaction-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacRestrictedView('hs_office_coopSharesTransaction', orderby => 'target.reference');
---//
-
-
--- ============================================================================
---changeset hs-office-coopSharesTransaction-rbac-NEW-CoopSharesTransaction:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-/*
- Creates a global permission for new-coopSharesTransaction and assigns it to the hostsharing admins role.
- */
-do language plpgsql $$
- declare
- addCustomerPermissions uuid[];
- globalObjectUuid uuid;
- globalAdminRoleUuid uuid ;
- begin
- call defineContext('granting global new-coopSharesTransaction permission to global admin role', null, null, null);
-
- globalAdminRoleUuid := findRoleId(globalAdmin());
- globalObjectUuid := (select uuid from global);
- addCustomerPermissions := createPermissions(globalObjectUuid, array ['new-coopsharestransaction']);
- call grantPermissionsToRole(globalAdminRoleUuid, addCustomerPermissions);
- end;
-$$;
-
-/**
- Used by the trigger to prevent the add-customer to current user respectively assumed roles.
- */
-create or replace function addHsOfficeCoopSharesTransactionNotAllowedForCurrentSubjects()
- returns trigger
- language PLPGSQL
-as $$
-begin
- raise exception '[403] new-coopsharestransaction 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_coopSharesTransaction_insert_trigger
- before insert
- on hs_office_coopSharesTransaction
- for each row
- when ( not hasAssumedRole() )
-execute procedure addHsOfficeCoopSharesTransactionNotAllowedForCurrentSubjects();
---//
-
diff --git a/src/main/resources/db/changelog/323-hs-office-coopassets-rbac.md b/src/main/resources/db/changelog/323-hs-office-coopassets-rbac.md
deleted file mode 100644
index 94ce746a..00000000
--- a/src/main/resources/db/changelog/323-hs-office-coopassets-rbac.md
+++ /dev/null
@@ -1,29 +0,0 @@
-### hs_office_coopAssetsTransaction RBAC
-
-```mermaid
-flowchart TB
-
-subgraph hsOfficeMembership
- direction TB
- style hsOfficeMembership fill:#eee
-
- role:hsOfficeMembership.owner[membership.admin]
- --> role:hsOfficeMembership.admin[membership.admin]
- --> role:hsOfficeMembership.agent[membership.agent]
- --> role:hsOfficeMembership.tenant[membership.tenant]
- --> role:hsOfficeMembership.guest[membership.guest]
-
- role:hsOfficePartner.agent --> role:hsOfficeMembership.agent
-end
-
-subgraph hsOfficeCoopAssetsTransaction
-
- role:hsOfficeMembership.admin
- --> perm:hsOfficeCoopAssetsTransaction.create{{coopAssetsTx.create}}
-
- role:hsOfficeMembership.agent
- --> perm:hsOfficeCoopAssetsTransaction.view{{coopAssetsTx.view}}
-end
-
-
-```
diff --git a/src/main/resources/db/changelog/323-hs-office-coopassets-rbac.sql b/src/main/resources/db/changelog/323-hs-office-coopassets-rbac.sql
deleted file mode 100644
index ac65c141..00000000
--- a/src/main/resources/db/changelog/323-hs-office-coopassets-rbac.sql
+++ /dev/null
@@ -1,126 +0,0 @@
---liquibase formatted sql
-
--- ============================================================================
---changeset hs-office-coopAssetsTransaction-rbac-OBJECT:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRelatedRbacObject('hs_office_coopAssetsTransaction');
---//
-
-
--- ============================================================================
---changeset hs-office-coopAssetsTransaction-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacRoleDescriptors('hsOfficeCoopAssetsTransaction', 'hs_office_coopAssetsTransaction');
---//
-
-
--- ============================================================================
---changeset hs-office-coopAssetsTransaction-rbac-ROLES-CREATION:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-
-/*
- Creates and updates the permissions for coopAssetsTransaction entities.
- */
-
-create or replace function hsOfficeCoopAssetsTransactionRbacRolesTrigger()
- returns trigger
- language plpgsql
- strict as $$
-declare
- newHsOfficeMembership hs_office_membership;
-begin
- call enterTriggerForObjectUuid(NEW.uuid);
-
- select * from hs_office_membership as p where p.uuid = NEW.membershipUuid into newHsOfficeMembership;
-
- if TG_OP = 'INSERT' then
-
- -- Each coopAssetsTransaction entity belong exactly to one membership entity
- -- and it makes little sense just to delegate coopAssetsTransaction roles.
- -- Therefore, we do not create coopAssetsTransaction roles at all,
- -- but instead just assign extra permissions to existing membership-roles.
-
- -- coopassetstransactions cannot be edited nor deleted, just created+viewed
- call grantPermissionsToRole(
- getRoleId(hsOfficeMembershipTenant(newHsOfficeMembership), 'fail'),
- createPermissions(NEW.uuid, array ['view'])
- );
-
- else
- raise exception 'invalid usage of TRIGGER';
- end if;
-
- call leaveTriggerForObjectUuid(NEW.uuid);
- return NEW;
-end; $$;
-
-/*
- An AFTER INSERT TRIGGER which creates the role structure for a new customer.
- */
-create trigger createRbacRolesForHsOfficeCoopAssetsTransaction_Trigger
- after insert
- on hs_office_coopAssetsTransaction
- for each row
-execute procedure hsOfficeCoopAssetsTransactionRbacRolesTrigger();
---//
-
-
--- ============================================================================
---changeset hs-office-coopAssetsTransaction-rbac-IDENTITY-VIEW:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacIdentityView('hs_office_coopAssetsTransaction',
- idNameExpression => 'target.reference');
---//
-
-
--- ============================================================================
---changeset hs-office-coopAssetsTransaction-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-call generateRbacRestrictedView('hs_office_coopAssetsTransaction', orderby => 'target.reference');
---//
-
-
--- ============================================================================
---changeset hs-office-coopAssetsTransaction-rbac-NEW-CoopAssetsTransaction:1 endDelimiter:--//
--- ----------------------------------------------------------------------------
-/*
- Creates a global permission for new-coopAssetsTransaction and assigns it to the hostsharing admins role.
- */
-do language plpgsql $$
- declare
- addCustomerPermissions uuid[];
- globalObjectUuid uuid;
- globalAdminRoleUuid uuid ;
- begin
- call defineContext('granting global new-coopAssetsTransaction permission to global admin role', null, null, null);
-
- globalAdminRoleUuid := findRoleId(globalAdmin());
- globalObjectUuid := (select uuid from global);
- addCustomerPermissions := createPermissions(globalObjectUuid, array ['new-coopassetstransaction']);
- call grantPermissionsToRole(globalAdminRoleUuid, addCustomerPermissions);
- end;
-$$;
-
-/**
- Used by the trigger to prevent the add-customer to current user respectively assumed roles.
- */
-create or replace function addHsOfficeCoopAssetsTransactionNotAllowedForCurrentSubjects()
- returns trigger
- language PLPGSQL
-as $$
-begin
- raise exception '[403] new-coopassetstransaction 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_coopAssetsTransaction_insert_trigger
- before insert
- on hs_office_coopAssetsTransaction
- for each row
- when ( not hasAssumedRole() )
-execute procedure addHsOfficeCoopAssetsTransactionNotAllowedForCurrentSubjects();
---//
-
diff --git a/src/main/resources/db/changelog/200-hs-office-contact.sql b/src/main/resources/db/changelog/5-hs-office/501-contact/5010-hs-office-contact.sql
similarity index 100%
rename from src/main/resources/db/changelog/200-hs-office-contact.sql
rename to src/main/resources/db/changelog/5-hs-office/501-contact/5010-hs-office-contact.sql
diff --git a/src/main/resources/db/changelog/5-hs-office/501-contact/5013-hs-office-contact-rbac.md b/src/main/resources/db/changelog/5-hs-office/501-contact/5013-hs-office-contact-rbac.md
new file mode 100644
index 00000000..fe736072
--- /dev/null
+++ b/src/main/resources/db/changelog/5-hs-office/501-contact/5013-hs-office-contact-rbac.md
@@ -0,0 +1,45 @@
+### rbac contact
+
+This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually.
+
+```mermaid
+%%{init:{'flowchart':{'htmlLabels':false}}}%%
+flowchart TB
+
+subgraph contact["`**contact**`"]
+ direction TB
+ style contact fill:#dd4901,stroke:#274d6e,stroke-width:8px
+
+ subgraph contact:roles[ ]
+ style contact:roles fill:#dd4901,stroke:white
+
+ role:contact:OWNER[[contact:OWNER]]
+ role:contact:ADMIN[[contact:ADMIN]]
+ role:contact:REFERRER[[contact:REFERRER]]
+ end
+
+ subgraph contact:permissions[ ]
+ style contact:permissions fill:#dd4901,stroke:white
+
+ perm:contact:DELETE{{contact:DELETE}}
+ perm:contact:UPDATE{{contact:UPDATE}}
+ perm:contact:SELECT{{contact:SELECT}}
+ perm:contact:INSERT{{contact:INSERT}}
+ end
+end
+
+%% granting roles to users
+user:creator ==> role:contact:OWNER
+
+%% granting roles to roles
+role:global:ADMIN ==> role:contact:OWNER
+role:contact:OWNER ==> role:contact:ADMIN
+role:contact:ADMIN ==> role:contact:REFERRER
+
+%% granting permissions to roles
+role:contact:OWNER ==> perm:contact:DELETE
+role:contact:ADMIN ==> perm:contact:UPDATE
+role:contact:REFERRER ==> perm:contact:SELECT
+role:global:GUEST ==> perm:contact:INSERT
+
+```
diff --git a/src/main/resources/db/changelog/5-hs-office/501-contact/5013-hs-office-contact-rbac.sql b/src/main/resources/db/changelog/5-hs-office/501-contact/5013-hs-office-contact-rbac.sql
new file mode 100644
index 00000000..0f53b167
--- /dev/null
+++ b/src/main/resources/db/changelog/5-hs-office/501-contact/5013-hs-office-contact-rbac.sql
@@ -0,0 +1,146 @@
+--liquibase formatted sql
+-- This code generated was by RbacViewPostgresGenerator, do not amend manually.
+
+
+-- ============================================================================
+--changeset hs-office-contact-rbac-OBJECT:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRelatedRbacObject('hs_office_contact');
+--//
+
+
+-- ============================================================================
+--changeset hs-office-contact-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRbacRoleDescriptors('hsOfficeContact', 'hs_office_contact');
+--//
+
+
+-- ============================================================================
+--changeset hs-office-contact-rbac-insert-trigger:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates the roles, grants and permission for the AFTER INSERT TRIGGER.
+ */
+
+create or replace procedure buildRbacSystemForHsOfficeContact(
+ NEW hs_office_contact
+)
+ language plpgsql as $$
+
+declare
+
+begin
+ call enterTriggerForObjectUuid(NEW.uuid);
+
+ perform createRoleWithGrants(
+ hsOfficeContactOWNER(NEW),
+ permissions => array['DELETE'],
+ incomingSuperRoles => array[globalADMIN()],
+ userUuids => array[currentUserUuid()]
+ );
+
+ perform createRoleWithGrants(
+ hsOfficeContactADMIN(NEW),
+ permissions => array['UPDATE'],
+ incomingSuperRoles => array[hsOfficeContactOWNER(NEW)]
+ );
+
+ perform createRoleWithGrants(
+ hsOfficeContactREFERRER(NEW),
+ permissions => array['SELECT'],
+ incomingSuperRoles => array[hsOfficeContactADMIN(NEW)]
+ );
+
+ call leaveTriggerForObjectUuid(NEW.uuid);
+end; $$;
+
+/*
+ AFTER INSERT TRIGGER to create the role+grant structure for a new hs_office_contact row.
+ */
+
+create or replace function insertTriggerForHsOfficeContact_tf()
+ returns trigger
+ language plpgsql
+ strict as $$
+begin
+ call buildRbacSystemForHsOfficeContact(NEW);
+ return NEW;
+end; $$;
+
+create trigger insertTriggerForHsOfficeContact_tg
+ after insert on hs_office_contact
+ for each row
+execute procedure insertTriggerForHsOfficeContact_tf();
+--//
+
+
+-- ============================================================================
+--changeset hs-office-contact-rbac-INSERT:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates INSERT INTO hs_office_contact permissions for the related global rows.
+ */
+do language plpgsql $$
+ declare
+ row global;
+ begin
+ call defineContext('create INSERT INTO hs_office_contact permissions for the related global rows');
+
+ FOR row IN SELECT * FROM global
+ LOOP
+ call grantPermissionToRole(
+ createPermission(row.uuid, 'INSERT', 'hs_office_contact'),
+ globalGUEST());
+ END LOOP;
+ END;
+$$;
+
+/**
+ Adds hs_office_contact INSERT permission to specified role of new global rows.
+*/
+create or replace function hs_office_contact_global_insert_tf()
+ returns trigger
+ language plpgsql
+ strict as $$
+begin
+ call grantPermissionToRole(
+ createPermission(NEW.uuid, 'INSERT', 'hs_office_contact'),
+ globalGUEST());
+ return NEW;
+end; $$;
+
+-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
+create trigger z_hs_office_contact_global_insert_tg
+ after insert on global
+ for each row
+execute procedure hs_office_contact_global_insert_tf();
+--//
+
+-- ============================================================================
+--changeset hs-office-contact-rbac-IDENTITY-VIEW:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+call generateRbacIdentityViewFromProjection('hs_office_contact',
+ $idName$
+ label
+ $idName$);
+--//
+
+-- ============================================================================
+--changeset hs-office-contact-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRbacRestrictedView('hs_office_contact',
+ $orderBy$
+ label
+ $orderBy$,
+ $updates$
+ label = new.label,
+ postalAddress = new.postalAddress,
+ emailAddresses = new.emailAddresses,
+ phoneNumbers = new.phoneNumbers
+ $updates$);
+--//
+
diff --git a/src/main/resources/db/changelog/206-hs-office-contact-migration.sql b/src/main/resources/db/changelog/5-hs-office/501-contact/5016-hs-office-contact-migration.sql
similarity index 100%
rename from src/main/resources/db/changelog/206-hs-office-contact-migration.sql
rename to src/main/resources/db/changelog/5-hs-office/501-contact/5016-hs-office-contact-migration.sql
diff --git a/src/main/resources/db/changelog/208-hs-office-contact-test-data.sql b/src/main/resources/db/changelog/5-hs-office/501-contact/5018-hs-office-contact-test-data.sql
similarity index 100%
rename from src/main/resources/db/changelog/208-hs-office-contact-test-data.sql
rename to src/main/resources/db/changelog/5-hs-office/501-contact/5018-hs-office-contact-test-data.sql
diff --git a/src/main/resources/db/changelog/210-hs-office-person.sql b/src/main/resources/db/changelog/5-hs-office/502-person/5020-hs-office-person.sql
similarity index 99%
rename from src/main/resources/db/changelog/210-hs-office-person.sql
rename to src/main/resources/db/changelog/5-hs-office/502-person/5020-hs-office-person.sql
index 899192fd..1b51278b 100644
--- a/src/main/resources/db/changelog/210-hs-office-person.sql
+++ b/src/main/resources/db/changelog/5-hs-office/502-person/5020-hs-office-person.sql
@@ -24,7 +24,6 @@ create table if not exists hs_office_person
givenName varchar(48),
familyName varchar(48)
);
---//
-- ============================================================================
diff --git a/src/main/resources/db/changelog/5-hs-office/502-person/5023-hs-office-person-rbac.md b/src/main/resources/db/changelog/5-hs-office/502-person/5023-hs-office-person-rbac.md
new file mode 100644
index 00000000..d0eebfdd
--- /dev/null
+++ b/src/main/resources/db/changelog/5-hs-office/502-person/5023-hs-office-person-rbac.md
@@ -0,0 +1,45 @@
+### rbac person
+
+This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually.
+
+```mermaid
+%%{init:{'flowchart':{'htmlLabels':false}}}%%
+flowchart TB
+
+subgraph person["`**person**`"]
+ direction TB
+ style person fill:#dd4901,stroke:#274d6e,stroke-width:8px
+
+ subgraph person:roles[ ]
+ style person:roles fill:#dd4901,stroke:white
+
+ role:person:OWNER[[person:OWNER]]
+ role:person:ADMIN[[person:ADMIN]]
+ role:person:REFERRER[[person:REFERRER]]
+ end
+
+ subgraph person:permissions[ ]
+ style person:permissions fill:#dd4901,stroke:white
+
+ perm:person:INSERT{{person:INSERT}}
+ perm:person:DELETE{{person:DELETE}}
+ perm:person:UPDATE{{person:UPDATE}}
+ perm:person:SELECT{{person:SELECT}}
+ end
+end
+
+%% granting roles to users
+user:creator ==> role:person:OWNER
+
+%% granting roles to roles
+role:global:ADMIN ==> role:person:OWNER
+role:person:OWNER ==> role:person:ADMIN
+role:person:ADMIN ==> role:person:REFERRER
+
+%% granting permissions to roles
+role:global:GUEST ==> perm:person:INSERT
+role:person:OWNER ==> perm:person:DELETE
+role:person:ADMIN ==> perm:person:UPDATE
+role:person:REFERRER ==> perm:person:SELECT
+
+```
diff --git a/src/main/resources/db/changelog/5-hs-office/502-person/5023-hs-office-person-rbac.sql b/src/main/resources/db/changelog/5-hs-office/502-person/5023-hs-office-person-rbac.sql
new file mode 100644
index 00000000..6dbbf21b
--- /dev/null
+++ b/src/main/resources/db/changelog/5-hs-office/502-person/5023-hs-office-person-rbac.sql
@@ -0,0 +1,146 @@
+--liquibase formatted sql
+-- This code generated was by RbacViewPostgresGenerator, do not amend manually.
+
+
+-- ============================================================================
+--changeset hs-office-person-rbac-OBJECT:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRelatedRbacObject('hs_office_person');
+--//
+
+
+-- ============================================================================
+--changeset hs-office-person-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRbacRoleDescriptors('hsOfficePerson', 'hs_office_person');
+--//
+
+
+-- ============================================================================
+--changeset hs-office-person-rbac-insert-trigger:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates the roles, grants and permission for the AFTER INSERT TRIGGER.
+ */
+
+create or replace procedure buildRbacSystemForHsOfficePerson(
+ NEW hs_office_person
+)
+ language plpgsql as $$
+
+declare
+
+begin
+ call enterTriggerForObjectUuid(NEW.uuid);
+
+ perform createRoleWithGrants(
+ hsOfficePersonOWNER(NEW),
+ permissions => array['DELETE'],
+ incomingSuperRoles => array[globalADMIN()],
+ userUuids => array[currentUserUuid()]
+ );
+
+ perform createRoleWithGrants(
+ hsOfficePersonADMIN(NEW),
+ permissions => array['UPDATE'],
+ incomingSuperRoles => array[hsOfficePersonOWNER(NEW)]
+ );
+
+ perform createRoleWithGrants(
+ hsOfficePersonREFERRER(NEW),
+ permissions => array['SELECT'],
+ incomingSuperRoles => array[hsOfficePersonADMIN(NEW)]
+ );
+
+ call leaveTriggerForObjectUuid(NEW.uuid);
+end; $$;
+
+/*
+ AFTER INSERT TRIGGER to create the role+grant structure for a new hs_office_person row.
+ */
+
+create or replace function insertTriggerForHsOfficePerson_tf()
+ returns trigger
+ language plpgsql
+ strict as $$
+begin
+ call buildRbacSystemForHsOfficePerson(NEW);
+ return NEW;
+end; $$;
+
+create trigger insertTriggerForHsOfficePerson_tg
+ after insert on hs_office_person
+ for each row
+execute procedure insertTriggerForHsOfficePerson_tf();
+--//
+
+
+-- ============================================================================
+--changeset hs-office-person-rbac-INSERT:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates INSERT INTO hs_office_person permissions for the related global rows.
+ */
+do language plpgsql $$
+ declare
+ row global;
+ begin
+ call defineContext('create INSERT INTO hs_office_person permissions for the related global rows');
+
+ FOR row IN SELECT * FROM global
+ LOOP
+ call grantPermissionToRole(
+ createPermission(row.uuid, 'INSERT', 'hs_office_person'),
+ globalGUEST());
+ END LOOP;
+ END;
+$$;
+
+/**
+ Adds hs_office_person INSERT permission to specified role of new global rows.
+*/
+create or replace function hs_office_person_global_insert_tf()
+ returns trigger
+ language plpgsql
+ strict as $$
+begin
+ call grantPermissionToRole(
+ createPermission(NEW.uuid, 'INSERT', 'hs_office_person'),
+ globalGUEST());
+ return NEW;
+end; $$;
+
+-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
+create trigger z_hs_office_person_global_insert_tg
+ after insert on global
+ for each row
+execute procedure hs_office_person_global_insert_tf();
+--//
+
+-- ============================================================================
+--changeset hs-office-person-rbac-IDENTITY-VIEW:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+call generateRbacIdentityViewFromProjection('hs_office_person',
+ $idName$
+ concat(tradeName, familyName, givenName)
+ $idName$);
+--//
+
+-- ============================================================================
+--changeset hs-office-person-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRbacRestrictedView('hs_office_person',
+ $orderBy$
+ concat(tradeName, familyName, givenName)
+ $orderBy$,
+ $updates$
+ personType = new.personType,
+ tradeName = new.tradeName,
+ givenName = new.givenName,
+ familyName = new.familyName
+ $updates$);
+--//
+
diff --git a/src/main/resources/db/changelog/218-hs-office-person-test-data.sql b/src/main/resources/db/changelog/5-hs-office/502-person/5028-hs-office-person-test-data.sql
similarity index 93%
rename from src/main/resources/db/changelog/218-hs-office-person-test-data.sql
rename to src/main/resources/db/changelog/5-hs-office/502-person/5028-hs-office-person-test-data.sql
index 6d087754..775ecaa6 100644
--- a/src/main/resources/db/changelog/218-hs-office-person-test-data.sql
+++ b/src/main/resources/db/changelog/5-hs-office/502-person/5028-hs-office-person-test-data.sql
@@ -28,7 +28,7 @@ begin
call defineContext(currentTask, null, emailAddr);
execute format('set local hsadminng.currentTask to %L', currentTask);
- raise notice 'creating test person: %', fullName;
+ raise notice 'creating test person: % by %', fullName, emailAddr;
insert
into hs_office_person (persontype, tradename, givenname, familyname)
values (newPersonType, newTradeName, newGivenName, newFamilyName);
@@ -67,9 +67,10 @@ do language plpgsql $$
call createHsOfficePersonTestData('NP', null, 'Fouler', 'Ellie');
call createHsOfficePersonTestData('LP', 'Second e.K.', 'Smith', 'Peter');
call createHsOfficePersonTestData('IF', 'Third OHG');
- call createHsOfficePersonTestData('IF', 'Fourth eG');
+ call createHsOfficePersonTestData('LP', 'Fourth eG');
call createHsOfficePersonTestData('UF', 'Erben Bessler', 'Mel', 'Bessler');
call createHsOfficePersonTestData('NP', null, 'Bessler', 'Anita');
+ call createHsOfficePersonTestData('NP', null, 'Bessler', 'Bert');
call createHsOfficePersonTestData('NP', null, 'Winkler', 'Paul');
end;
$$;
diff --git a/src/main/resources/db/changelog/5-hs-office/503-relation/5030-hs-office-relation.sql b/src/main/resources/db/changelog/5-hs-office/503-relation/5030-hs-office-relation.sql
new file mode 100644
index 00000000..8e6e56a1
--- /dev/null
+++ b/src/main/resources/db/changelog/5-hs-office/503-relation/5030-hs-office-relation.sql
@@ -0,0 +1,36 @@
+--liquibase formatted sql
+
+-- ============================================================================
+--changeset hs-office-relation-MAIN-TABLE:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+CREATE TYPE HsOfficeRelationType AS ENUM (
+ 'UNKNOWN',
+ 'PARTNER',
+ 'EX_PARTNER',
+ 'REPRESENTATIVE',
+ 'DEBITOR',
+ 'VIP_CONTACT',
+ 'OPERATIONS',
+ 'SUBSCRIBER');
+
+CREATE CAST (character varying as HsOfficeRelationType) WITH INOUT AS IMPLICIT;
+
+create table if not exists hs_office_relation
+(
+ uuid uuid unique references RbacObject (uuid) initially deferred, -- on delete cascade
+ anchorUuid uuid not null references hs_office_person(uuid),
+ holderUuid uuid not null references hs_office_person(uuid),
+ contactUuid uuid references hs_office_contact(uuid),
+ type HsOfficeRelationType not null,
+ mark varchar(24)
+);
+--//
+
+
+-- ============================================================================
+--changeset hs-office-relation-MAIN-TABLE-JOURNAL:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+call create_journal('hs_office_relation');
+--//
diff --git a/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.md b/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.md
new file mode 100644
index 00000000..8014cdaf
--- /dev/null
+++ b/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.md
@@ -0,0 +1,102 @@
+### rbac relation
+
+This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually.
+
+```mermaid
+%%{init:{'flowchart':{'htmlLabels':false}}}%%
+flowchart TB
+
+subgraph holderPerson["`**holderPerson**`"]
+ direction TB
+ style holderPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph holderPerson:roles[ ]
+ style holderPerson:roles fill:#99bcdb,stroke:white
+
+ role:holderPerson:OWNER[[holderPerson:OWNER]]
+ role:holderPerson:ADMIN[[holderPerson:ADMIN]]
+ role:holderPerson:REFERRER[[holderPerson:REFERRER]]
+ end
+end
+
+subgraph anchorPerson["`**anchorPerson**`"]
+ direction TB
+ style anchorPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph anchorPerson:roles[ ]
+ style anchorPerson:roles fill:#99bcdb,stroke:white
+
+ role:anchorPerson:OWNER[[anchorPerson:OWNER]]
+ role:anchorPerson:ADMIN[[anchorPerson:ADMIN]]
+ role:anchorPerson:REFERRER[[anchorPerson:REFERRER]]
+ end
+end
+
+subgraph contact["`**contact**`"]
+ direction TB
+ style contact fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph contact:roles[ ]
+ style contact:roles fill:#99bcdb,stroke:white
+
+ role:contact:OWNER[[contact:OWNER]]
+ role:contact:ADMIN[[contact:ADMIN]]
+ role:contact:REFERRER[[contact:REFERRER]]
+ end
+end
+
+subgraph relation["`**relation**`"]
+ direction TB
+ style relation fill:#dd4901,stroke:#274d6e,stroke-width:8px
+
+ subgraph relation:roles[ ]
+ style relation:roles fill:#dd4901,stroke:white
+
+ role:relation:OWNER[[relation:OWNER]]
+ role:relation:ADMIN[[relation:ADMIN]]
+ role:relation:AGENT[[relation:AGENT]]
+ role:relation:TENANT[[relation:TENANT]]
+ end
+
+ subgraph relation:permissions[ ]
+ style relation:permissions fill:#dd4901,stroke:white
+
+ perm:relation:DELETE{{relation:DELETE}}
+ perm:relation:UPDATE{{relation:UPDATE}}
+ perm:relation:SELECT{{relation:SELECT}}
+ perm:relation:INSERT{{relation:INSERT}}
+ end
+end
+
+%% granting roles to users
+user:creator ==> role:relation:OWNER
+
+%% granting roles to roles
+role:global:ADMIN -.-> role:anchorPerson:OWNER
+role:anchorPerson:OWNER -.-> role:anchorPerson:ADMIN
+role:anchorPerson:ADMIN -.-> role:anchorPerson:REFERRER
+role:global:ADMIN -.-> role:holderPerson:OWNER
+role:holderPerson:OWNER -.-> role:holderPerson:ADMIN
+role:holderPerson:ADMIN -.-> role:holderPerson:REFERRER
+role:global:ADMIN -.-> role:contact:OWNER
+role:contact:OWNER -.-> role:contact:ADMIN
+role:contact:ADMIN -.-> role:contact:REFERRER
+role:global:ADMIN ==> role:relation:OWNER
+role:relation:OWNER ==> role:relation:ADMIN
+role:anchorPerson:ADMIN ==> role:relation:ADMIN
+role:relation:ADMIN ==> role:relation:AGENT
+role:holderPerson:ADMIN ==> role:relation:AGENT
+role:relation:AGENT ==> role:relation:TENANT
+role:holderPerson:ADMIN ==> role:relation:TENANT
+role:contact:ADMIN ==> role:relation:TENANT
+role:relation:TENANT ==> role:anchorPerson:REFERRER
+role:relation:TENANT ==> role:holderPerson:REFERRER
+role:relation:TENANT ==> role:contact:REFERRER
+
+%% granting permissions to roles
+role:relation:OWNER ==> perm:relation:DELETE
+role:relation:ADMIN ==> perm:relation:UPDATE
+role:relation:TENANT ==> perm:relation:SELECT
+role:anchorPerson:ADMIN ==> perm:relation:INSERT
+
+```
diff --git a/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.sql b/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.sql
new file mode 100644
index 00000000..ff890a59
--- /dev/null
+++ b/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.sql
@@ -0,0 +1,271 @@
+--liquibase formatted sql
+-- This code generated was by RbacViewPostgresGenerator, do not amend manually.
+
+
+-- ============================================================================
+--changeset hs-office-relation-rbac-OBJECT:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRelatedRbacObject('hs_office_relation');
+--//
+
+
+-- ============================================================================
+--changeset hs-office-relation-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRbacRoleDescriptors('hsOfficeRelation', 'hs_office_relation');
+--//
+
+
+-- ============================================================================
+--changeset hs-office-relation-rbac-insert-trigger:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates the roles, grants and permission for the AFTER INSERT TRIGGER.
+ */
+
+create or replace procedure buildRbacSystemForHsOfficeRelation(
+ NEW hs_office_relation
+)
+ language plpgsql as $$
+
+declare
+ newHolderPerson hs_office_person;
+ newAnchorPerson hs_office_person;
+ newContact hs_office_contact;
+
+begin
+ call enterTriggerForObjectUuid(NEW.uuid);
+
+ SELECT * FROM hs_office_person WHERE uuid = NEW.holderUuid INTO newHolderPerson;
+ assert newHolderPerson.uuid is not null, format('newHolderPerson must not be null for NEW.holderUuid = %s', NEW.holderUuid);
+
+ SELECT * FROM hs_office_person WHERE uuid = NEW.anchorUuid INTO newAnchorPerson;
+ assert newAnchorPerson.uuid is not null, format('newAnchorPerson must not be null for NEW.anchorUuid = %s', NEW.anchorUuid);
+
+ SELECT * FROM hs_office_contact WHERE uuid = NEW.contactUuid INTO newContact;
+ assert newContact.uuid is not null, format('newContact must not be null for NEW.contactUuid = %s', NEW.contactUuid);
+
+
+ perform createRoleWithGrants(
+ hsOfficeRelationOWNER(NEW),
+ permissions => array['DELETE'],
+ incomingSuperRoles => array[globalADMIN()],
+ userUuids => array[currentUserUuid()]
+ );
+
+ perform createRoleWithGrants(
+ hsOfficeRelationADMIN(NEW),
+ permissions => array['UPDATE'],
+ incomingSuperRoles => array[
+ hsOfficePersonADMIN(newAnchorPerson),
+ hsOfficeRelationOWNER(NEW)]
+ );
+
+ perform createRoleWithGrants(
+ hsOfficeRelationAGENT(NEW),
+ incomingSuperRoles => array[
+ hsOfficePersonADMIN(newHolderPerson),
+ hsOfficeRelationADMIN(NEW)]
+ );
+
+ perform createRoleWithGrants(
+ hsOfficeRelationTENANT(NEW),
+ permissions => array['SELECT'],
+ incomingSuperRoles => array[
+ hsOfficeContactADMIN(newContact),
+ hsOfficePersonADMIN(newHolderPerson),
+ hsOfficeRelationAGENT(NEW)],
+ outgoingSubRoles => array[
+ hsOfficeContactREFERRER(newContact),
+ hsOfficePersonREFERRER(newAnchorPerson),
+ hsOfficePersonREFERRER(newHolderPerson)]
+ );
+
+ call leaveTriggerForObjectUuid(NEW.uuid);
+end; $$;
+
+/*
+ AFTER INSERT TRIGGER to create the role+grant structure for a new hs_office_relation row.
+ */
+
+create or replace function insertTriggerForHsOfficeRelation_tf()
+ returns trigger
+ language plpgsql
+ strict as $$
+begin
+ call buildRbacSystemForHsOfficeRelation(NEW);
+ return NEW;
+end; $$;
+
+create trigger insertTriggerForHsOfficeRelation_tg
+ after insert on hs_office_relation
+ for each row
+execute procedure insertTriggerForHsOfficeRelation_tf();
+--//
+
+
+-- ============================================================================
+--changeset hs-office-relation-rbac-update-trigger:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Called from the AFTER UPDATE TRIGGER to re-wire the grants.
+ */
+
+create or replace procedure updateRbacRulesForHsOfficeRelation(
+ OLD hs_office_relation,
+ NEW hs_office_relation
+)
+ language plpgsql as $$
+
+declare
+ oldHolderPerson hs_office_person;
+ newHolderPerson hs_office_person;
+ oldAnchorPerson hs_office_person;
+ newAnchorPerson hs_office_person;
+ oldContact hs_office_contact;
+ newContact hs_office_contact;
+
+begin
+ call enterTriggerForObjectUuid(NEW.uuid);
+
+ SELECT * FROM hs_office_person WHERE uuid = OLD.holderUuid INTO oldHolderPerson;
+ assert oldHolderPerson.uuid is not null, format('oldHolderPerson must not be null for OLD.holderUuid = %s', OLD.holderUuid);
+
+ SELECT * FROM hs_office_person WHERE uuid = NEW.holderUuid INTO newHolderPerson;
+ assert newHolderPerson.uuid is not null, format('newHolderPerson must not be null for NEW.holderUuid = %s', NEW.holderUuid);
+
+ SELECT * FROM hs_office_person WHERE uuid = OLD.anchorUuid INTO oldAnchorPerson;
+ assert oldAnchorPerson.uuid is not null, format('oldAnchorPerson must not be null for OLD.anchorUuid = %s', OLD.anchorUuid);
+
+ SELECT * FROM hs_office_person WHERE uuid = NEW.anchorUuid INTO newAnchorPerson;
+ assert newAnchorPerson.uuid is not null, format('newAnchorPerson must not be null for NEW.anchorUuid = %s', NEW.anchorUuid);
+
+ SELECT * FROM hs_office_contact WHERE uuid = OLD.contactUuid INTO oldContact;
+ assert oldContact.uuid is not null, format('oldContact must not be null for OLD.contactUuid = %s', OLD.contactUuid);
+
+ SELECT * FROM hs_office_contact WHERE uuid = NEW.contactUuid INTO newContact;
+ assert newContact.uuid is not null, format('newContact must not be null for NEW.contactUuid = %s', NEW.contactUuid);
+
+
+ if NEW.contactUuid <> OLD.contactUuid then
+
+ call revokeRoleFromRole(hsOfficeRelationTENANT(OLD), hsOfficeContactADMIN(oldContact));
+ call grantRoleToRole(hsOfficeRelationTENANT(NEW), hsOfficeContactADMIN(newContact));
+
+ call revokeRoleFromRole(hsOfficeContactREFERRER(oldContact), hsOfficeRelationTENANT(OLD));
+ call grantRoleToRole(hsOfficeContactREFERRER(newContact), hsOfficeRelationTENANT(NEW));
+
+ end if;
+
+ call leaveTriggerForObjectUuid(NEW.uuid);
+end; $$;
+
+/*
+ AFTER INSERT TRIGGER to re-wire the grant structure for a new hs_office_relation row.
+ */
+
+create or replace function updateTriggerForHsOfficeRelation_tf()
+ returns trigger
+ language plpgsql
+ strict as $$
+begin
+ call updateRbacRulesForHsOfficeRelation(OLD, NEW);
+ return NEW;
+end; $$;
+
+create trigger updateTriggerForHsOfficeRelation_tg
+ after update on hs_office_relation
+ for each row
+execute procedure updateTriggerForHsOfficeRelation_tf();
+--//
+
+
+-- ============================================================================
+--changeset hs-office-relation-rbac-INSERT:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates INSERT INTO hs_office_relation permissions for the related hs_office_person rows.
+ */
+do language plpgsql $$
+ declare
+ row hs_office_person;
+ begin
+ call defineContext('create INSERT INTO hs_office_relation permissions for the related hs_office_person rows');
+
+ FOR row IN SELECT * FROM hs_office_person
+ LOOP
+ call grantPermissionToRole(
+ createPermission(row.uuid, 'INSERT', 'hs_office_relation'),
+ hsOfficePersonADMIN(row));
+ END LOOP;
+ END;
+$$;
+
+/**
+ Adds hs_office_relation INSERT permission to specified role of new hs_office_person rows.
+*/
+create or replace function hs_office_relation_hs_office_person_insert_tf()
+ returns trigger
+ language plpgsql
+ strict as $$
+begin
+ call grantPermissionToRole(
+ createPermission(NEW.uuid, 'INSERT', 'hs_office_relation'),
+ hsOfficePersonADMIN(NEW));
+ return NEW;
+end; $$;
+
+-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
+create trigger z_hs_office_relation_hs_office_person_insert_tg
+ after insert on hs_office_person
+ for each row
+execute procedure hs_office_relation_hs_office_person_insert_tf();
+
+/**
+ Checks if the user or assumed roles are allowed to insert a row to hs_office_relation,
+ where the check is performed by a direct role.
+
+ A direct role is a role depending on a foreign key directly available in the NEW row.
+*/
+create or replace function hs_office_relation_insert_permission_missing_tf()
+ returns trigger
+ language plpgsql as $$
+begin
+ raise exception '[403] insert into hs_office_relation not allowed for current subjects % (%)',
+ currentSubjects(), currentSubjectsUuids();
+end; $$;
+
+create trigger hs_office_relation_insert_permission_check_tg
+ before insert on hs_office_relation
+ for each row
+ when ( not hasInsertPermission(NEW.anchorUuid, 'INSERT', 'hs_office_relation') )
+ execute procedure hs_office_relation_insert_permission_missing_tf();
+--//
+
+-- ============================================================================
+--changeset hs-office-relation-rbac-IDENTITY-VIEW:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+call generateRbacIdentityViewFromProjection('hs_office_relation',
+ $idName$
+ (select idName from hs_office_person_iv p where p.uuid = anchorUuid)
+ || '-with-' || target.type || '-'
+ || (select idName from hs_office_person_iv p where p.uuid = holderUuid)
+ $idName$);
+--//
+
+-- ============================================================================
+--changeset hs-office-relation-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRbacRestrictedView('hs_office_relation',
+ $orderBy$
+ (select idName from hs_office_person_iv p where p.uuid = target.holderUuid)
+ $orderBy$,
+ $updates$
+ contactUuid = new.contactUuid
+ $updates$);
+--//
+
diff --git a/src/main/resources/db/changelog/5-hs-office/503-relation/5038-hs-office-relation-test-data.sql b/src/main/resources/db/changelog/5-hs-office/503-relation/5038-hs-office-relation-test-data.sql
new file mode 100644
index 00000000..61691d6f
--- /dev/null
+++ b/src/main/resources/db/changelog/5-hs-office/503-relation/5038-hs-office-relation-test-data.sql
@@ -0,0 +1,113 @@
+--liquibase formatted sql
+
+
+-- ============================================================================
+--changeset hs-office-relation-TEST-DATA-GENERATOR:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates a single relation test record.
+ */
+create or replace procedure createHsOfficeRelationTestData(
+ holderPersonName varchar,
+ relationType HsOfficeRelationType,
+ anchorPersonName varchar,
+ contactLabel varchar,
+ mark varchar default null)
+ language plpgsql as $$
+declare
+ currentTask varchar;
+ idName varchar;
+ anchorPerson hs_office_person;
+ holderPerson hs_office_person;
+ contact hs_office_contact;
+
+begin
+ idName := cleanIdentifier( anchorPersonName || '-' || holderPersonName);
+ currentTask := 'creating relation test-data ' || idName;
+ call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global:ADMIN');
+ execute format('set local hsadminng.currentTask to %L', currentTask);
+
+ select p.*
+ into anchorPerson
+ from hs_office_person p
+ where p.tradeName = anchorPersonName or p.familyName = anchorPersonName;
+ if anchorPerson is null then
+ raise exception 'anchorPerson "%" not found', anchorPersonName;
+ end if;
+
+ select p.*
+ into holderPerson
+ from hs_office_person p
+ where p.tradeName = holderPersonName or p.familyName = holderPersonName;
+ if holderPerson is null then
+ raise exception 'holderPerson "%" not found', holderPersonName;
+ end if;
+
+ select c.* into contact from hs_office_contact c where c.label = contactLabel;
+ if contact is null then
+ raise exception 'contact "%" not found', contactLabel;
+ end if;
+
+ raise notice 'creating test relation: %', idName;
+ raise notice '- using anchor person (%): %', anchorPerson.uuid, anchorPerson;
+ raise notice '- using holder person (%): %', holderPerson.uuid, holderPerson;
+ raise notice '- using contact (%): %', contact.uuid, contact;
+ insert
+ into hs_office_relation (uuid, anchoruuid, holderuuid, type, mark, contactUuid)
+ values (uuid_generate_v4(), anchorPerson.uuid, holderPerson.uuid, relationType, mark, contact.uuid);
+end; $$;
+--//
+
+/*
+ Creates a range of test relation for mass data generation.
+ */
+create or replace procedure createHsOfficeRelationTestData(
+ startCount integer, -- count of auto generated rows before the run
+ endCount integer -- count of auto generated rows after the run
+)
+ language plpgsql as $$
+declare
+ person hs_office_person;
+ contact hs_office_contact;
+begin
+ for t in startCount..endCount
+ loop
+ select p.* from hs_office_person p where tradeName = intToVarChar(t, 4) into person;
+ select c.* from hs_office_contact c where c.label = intToVarChar(t, 4) || '#' || t into contact;
+
+ call createHsOfficeRelationTestData(person.uuid, contact.uuid, 'REPRESENTATIVE');
+ commit;
+ end loop;
+end; $$;
+--//
+
+
+-- ============================================================================
+--changeset hs-office-relation-TEST-DATA-GENERATION:1 –context=dev,tc endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+do language plpgsql $$
+ begin
+ call createHsOfficeRelationTestData('First GmbH', 'PARTNER', 'Hostsharing eG', 'first contact');
+ call createHsOfficeRelationTestData('Firby', 'REPRESENTATIVE', 'First GmbH', 'first contact');
+ call createHsOfficeRelationTestData('First GmbH', 'DEBITOR', 'First GmbH', 'first contact');
+
+ call createHsOfficeRelationTestData('Second e.K.', 'PARTNER', 'Hostsharing eG', 'second contact');
+ call createHsOfficeRelationTestData('Smith', 'REPRESENTATIVE', 'Second e.K.', 'second contact');
+ call createHsOfficeRelationTestData('Second e.K.', 'DEBITOR', 'Second e.K.', 'second contact');
+
+ call createHsOfficeRelationTestData('Third OHG', 'PARTNER', 'Hostsharing eG', 'third contact');
+ call createHsOfficeRelationTestData('Tucker', 'REPRESENTATIVE', 'Third OHG', 'third contact');
+ call createHsOfficeRelationTestData('Third OHG', 'DEBITOR', 'Third OHG', 'third contact');
+
+ call createHsOfficeRelationTestData('Fourth eG', 'PARTNER', 'Hostsharing eG', 'fourth contact');
+ call createHsOfficeRelationTestData('Fouler', 'REPRESENTATIVE', 'Third OHG', 'third contact');
+ call createHsOfficeRelationTestData('Third OHG', 'DEBITOR', 'Third OHG', 'third contact');
+
+ call createHsOfficeRelationTestData('Smith', 'PARTNER', 'Hostsharing eG', 'sixth contact');
+ call createHsOfficeRelationTestData('Smith', 'DEBITOR', 'Smith', 'third contact');
+ call createHsOfficeRelationTestData('Smith', 'SUBSCRIBER', 'Third OHG', 'third contact', 'members-announce');
+ end;
+$$;
+--//
diff --git a/src/main/resources/db/changelog/230-hs-office-partner.sql b/src/main/resources/db/changelog/5-hs-office/504-partner/5040-hs-office-partner.sql
similarity index 77%
rename from src/main/resources/db/changelog/230-hs-office-partner.sql
rename to src/main/resources/db/changelog/5-hs-office/504-partner/5040-hs-office-partner.sql
index d1db4400..d02ed017 100644
--- a/src/main/resources/db/changelog/230-hs-office-partner.sql
+++ b/src/main/resources/db/changelog/5-hs-office/504-partner/5040-hs-office-partner.sql
@@ -33,23 +33,20 @@ create table hs_office_partner
(
uuid uuid unique references RbacObject (uuid) initially deferred,
partnerNumber numeric(5) unique not null,
- partnerRoleUuid uuid not null references hs_office_relationship(uuid), -- TODO: delete in after delete trigger
- personUuid uuid not null references hs_office_person(uuid), -- TODO: remove, replaced by partnerRoleUuid
- contactUuid uuid not null references hs_office_contact(uuid), -- TODO: remove, replaced by partnerRoleUuid
+ partnerRelUuid uuid not null references hs_office_relation(uuid), -- deleted in after delete trigger
detailsUuid uuid not null references hs_office_partner_details(uuid) -- deleted in after delete trigger
);
--//
-- ============================================================================
---changeset hs-office-partner-DELETE-DETAILS-TRIGGER:1 endDelimiter:--//
+--changeset hs-office-partner-DELETE-DEPENDENTS-TRIGGER:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
-
/**
Trigger function to delete related details of a partner to delete.
*/
-create or replace function deleteHsOfficeDetailsOnPartnerDelete()
+create or replace function deleteHsOfficeDependentsOnPartnerDelete()
returns trigger
language PLPGSQL
as $$
@@ -61,17 +58,24 @@ begin
if counter = 0 then
raise exception 'partner details % could not be deleted', OLD.detailsUuid;
end if;
+
+ DELETE FROM hs_office_relation r WHERE r.uuid = OLD.partnerRelUuid;
+ GET DIAGNOSTICS counter = ROW_COUNT;
+ if counter = 0 then
+ raise exception 'partner relation % could not be deleted', OLD.partnerRelUuid;
+ end if;
+
RETURN OLD;
end; $$;
/**
- Triggers deletion of related details of a partner to delete.
+ Triggers deletion of related rows of a partner to delete.
*/
-create trigger hs_office_partner_delete_details_trigger
+create trigger hs_office_partner_delete_dependents_trigger
after delete
on hs_office_partner
for each row
- execute procedure deleteHsOfficeDetailsOnPartnerDelete();
+ execute procedure deleteHsOfficeDependentsOnPartnerDelete();
-- ============================================================================
--changeset hs-office-partner-MAIN-TABLE-JOURNAL:1 endDelimiter:--//
diff --git a/src/main/resources/db/changelog/5-hs-office/504-partner/5043-hs-office-partner-rbac.md b/src/main/resources/db/changelog/5-hs-office/504-partner/5043-hs-office-partner-rbac.md
new file mode 100644
index 00000000..a0caa074
--- /dev/null
+++ b/src/main/resources/db/changelog/5-hs-office/504-partner/5043-hs-office-partner-rbac.md
@@ -0,0 +1,120 @@
+### rbac partner
+
+This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually.
+
+```mermaid
+%%{init:{'flowchart':{'htmlLabels':false}}}%%
+flowchart TB
+
+subgraph partnerRel.contact["`**partnerRel.contact**`"]
+ direction TB
+ style partnerRel.contact fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph partnerRel.contact:roles[ ]
+ style partnerRel.contact:roles fill:#99bcdb,stroke:white
+
+ role:partnerRel.contact:OWNER[[partnerRel.contact:OWNER]]
+ role:partnerRel.contact:ADMIN[[partnerRel.contact:ADMIN]]
+ role:partnerRel.contact:REFERRER[[partnerRel.contact:REFERRER]]
+ end
+end
+
+subgraph partner["`**partner**`"]
+ direction TB
+ style partner fill:#dd4901,stroke:#274d6e,stroke-width:8px
+
+ subgraph partner:permissions[ ]
+ style partner:permissions fill:#dd4901,stroke:white
+
+ perm:partner:INSERT{{partner:INSERT}}
+ perm:partner:DELETE{{partner:DELETE}}
+ perm:partner:UPDATE{{partner:UPDATE}}
+ perm:partner:SELECT{{partner:SELECT}}
+ end
+
+ subgraph partnerRel["`**partnerRel**`"]
+ direction TB
+ style partnerRel fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph partnerRel:roles[ ]
+ style partnerRel:roles fill:#99bcdb,stroke:white
+
+ role:partnerRel:OWNER[[partnerRel:OWNER]]
+ role:partnerRel:ADMIN[[partnerRel:ADMIN]]
+ role:partnerRel:AGENT[[partnerRel:AGENT]]
+ role:partnerRel:TENANT[[partnerRel:TENANT]]
+ end
+ end
+end
+
+subgraph partnerDetails["`**partnerDetails**`"]
+ direction TB
+ style partnerDetails fill:#feb28c,stroke:#274d6e,stroke-width:8px
+
+ subgraph partnerDetails:permissions[ ]
+ style partnerDetails:permissions fill:#feb28c,stroke:white
+
+ perm:partnerDetails:DELETE{{partnerDetails:DELETE}}
+ perm:partnerDetails:UPDATE{{partnerDetails:UPDATE}}
+ perm:partnerDetails:SELECT{{partnerDetails:SELECT}}
+ end
+end
+
+subgraph partnerRel.anchorPerson["`**partnerRel.anchorPerson**`"]
+ direction TB
+ style partnerRel.anchorPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph partnerRel.anchorPerson:roles[ ]
+ style partnerRel.anchorPerson:roles fill:#99bcdb,stroke:white
+
+ role:partnerRel.anchorPerson:OWNER[[partnerRel.anchorPerson:OWNER]]
+ role:partnerRel.anchorPerson:ADMIN[[partnerRel.anchorPerson:ADMIN]]
+ role:partnerRel.anchorPerson:REFERRER[[partnerRel.anchorPerson:REFERRER]]
+ end
+end
+
+subgraph partnerRel.holderPerson["`**partnerRel.holderPerson**`"]
+ direction TB
+ style partnerRel.holderPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph partnerRel.holderPerson:roles[ ]
+ style partnerRel.holderPerson:roles fill:#99bcdb,stroke:white
+
+ role:partnerRel.holderPerson:OWNER[[partnerRel.holderPerson:OWNER]]
+ role:partnerRel.holderPerson:ADMIN[[partnerRel.holderPerson:ADMIN]]
+ role:partnerRel.holderPerson:REFERRER[[partnerRel.holderPerson:REFERRER]]
+ end
+end
+
+%% granting roles to roles
+role:global:ADMIN -.-> role:partnerRel.anchorPerson:OWNER
+role:partnerRel.anchorPerson:OWNER -.-> role:partnerRel.anchorPerson:ADMIN
+role:partnerRel.anchorPerson:ADMIN -.-> role:partnerRel.anchorPerson:REFERRER
+role:global:ADMIN -.-> role:partnerRel.holderPerson:OWNER
+role:partnerRel.holderPerson:OWNER -.-> role:partnerRel.holderPerson:ADMIN
+role:partnerRel.holderPerson:ADMIN -.-> role:partnerRel.holderPerson:REFERRER
+role:global:ADMIN -.-> role:partnerRel.contact:OWNER
+role:partnerRel.contact:OWNER -.-> role:partnerRel.contact:ADMIN
+role:partnerRel.contact:ADMIN -.-> role:partnerRel.contact:REFERRER
+role:global:ADMIN -.-> role:partnerRel:OWNER
+role:partnerRel:OWNER -.-> role:partnerRel:ADMIN
+role:partnerRel.anchorPerson:ADMIN -.-> role:partnerRel:ADMIN
+role:partnerRel:ADMIN -.-> role:partnerRel:AGENT
+role:partnerRel.holderPerson:ADMIN -.-> role:partnerRel:AGENT
+role:partnerRel:AGENT -.-> role:partnerRel:TENANT
+role:partnerRel.holderPerson:ADMIN -.-> role:partnerRel:TENANT
+role:partnerRel.contact:ADMIN -.-> role:partnerRel:TENANT
+role:partnerRel:TENANT -.-> role:partnerRel.anchorPerson:REFERRER
+role:partnerRel:TENANT -.-> role:partnerRel.holderPerson:REFERRER
+role:partnerRel:TENANT -.-> role:partnerRel.contact:REFERRER
+
+%% granting permissions to roles
+role:global:ADMIN ==> perm:partner:INSERT
+role:partnerRel:ADMIN ==> perm:partner:DELETE
+role:partnerRel:AGENT ==> perm:partner:UPDATE
+role:partnerRel:TENANT ==> perm:partner:SELECT
+role:partnerRel:ADMIN ==> perm:partnerDetails:DELETE
+role:partnerRel:AGENT ==> perm:partnerDetails:UPDATE
+role:partnerRel:AGENT ==> perm:partnerDetails:SELECT
+
+```
diff --git a/src/main/resources/db/changelog/5-hs-office/504-partner/5043-hs-office-partner-rbac.sql b/src/main/resources/db/changelog/5-hs-office/504-partner/5043-hs-office-partner-rbac.sql
new file mode 100644
index 00000000..b5510d8c
--- /dev/null
+++ b/src/main/resources/db/changelog/5-hs-office/504-partner/5043-hs-office-partner-rbac.sql
@@ -0,0 +1,238 @@
+--liquibase formatted sql
+-- This code generated was by RbacViewPostgresGenerator, do not amend manually.
+
+
+-- ============================================================================
+--changeset hs-office-partner-rbac-OBJECT:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRelatedRbacObject('hs_office_partner');
+--//
+
+
+-- ============================================================================
+--changeset hs-office-partner-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRbacRoleDescriptors('hsOfficePartner', 'hs_office_partner');
+--//
+
+
+-- ============================================================================
+--changeset hs-office-partner-rbac-insert-trigger:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates the roles, grants and permission for the AFTER INSERT TRIGGER.
+ */
+
+create or replace procedure buildRbacSystemForHsOfficePartner(
+ NEW hs_office_partner
+)
+ language plpgsql as $$
+
+declare
+ newPartnerRel hs_office_relation;
+ newPartnerDetails hs_office_partner_details;
+
+begin
+ call enterTriggerForObjectUuid(NEW.uuid);
+
+ SELECT * FROM hs_office_relation WHERE uuid = NEW.partnerRelUuid INTO newPartnerRel;
+ assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.partnerRelUuid = %s', NEW.partnerRelUuid);
+
+ SELECT * FROM hs_office_partner_details WHERE uuid = NEW.detailsUuid INTO newPartnerDetails;
+ assert newPartnerDetails.uuid is not null, format('newPartnerDetails must not be null for NEW.detailsUuid = %s', NEW.detailsUuid);
+
+ call grantPermissionToRole(createPermission(NEW.uuid, 'DELETE'), hsOfficeRelationADMIN(newPartnerRel));
+ call grantPermissionToRole(createPermission(NEW.uuid, 'SELECT'), hsOfficeRelationTENANT(newPartnerRel));
+ call grantPermissionToRole(createPermission(NEW.uuid, 'UPDATE'), hsOfficeRelationAGENT(newPartnerRel));
+ call grantPermissionToRole(createPermission(newPartnerDetails.uuid, 'DELETE'), hsOfficeRelationADMIN(newPartnerRel));
+ call grantPermissionToRole(createPermission(newPartnerDetails.uuid, 'SELECT'), hsOfficeRelationAGENT(newPartnerRel));
+ call grantPermissionToRole(createPermission(newPartnerDetails.uuid, 'UPDATE'), hsOfficeRelationAGENT(newPartnerRel));
+
+ call leaveTriggerForObjectUuid(NEW.uuid);
+end; $$;
+
+/*
+ AFTER INSERT TRIGGER to create the role+grant structure for a new hs_office_partner row.
+ */
+
+create or replace function insertTriggerForHsOfficePartner_tf()
+ returns trigger
+ language plpgsql
+ strict as $$
+begin
+ call buildRbacSystemForHsOfficePartner(NEW);
+ return NEW;
+end; $$;
+
+create trigger insertTriggerForHsOfficePartner_tg
+ after insert on hs_office_partner
+ for each row
+execute procedure insertTriggerForHsOfficePartner_tf();
+--//
+
+
+-- ============================================================================
+--changeset hs-office-partner-rbac-update-trigger:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Called from the AFTER UPDATE TRIGGER to re-wire the grants.
+ */
+
+create or replace procedure updateRbacRulesForHsOfficePartner(
+ OLD hs_office_partner,
+ NEW hs_office_partner
+)
+ language plpgsql as $$
+
+declare
+ oldPartnerRel hs_office_relation;
+ newPartnerRel hs_office_relation;
+ oldPartnerDetails hs_office_partner_details;
+ newPartnerDetails hs_office_partner_details;
+
+begin
+ call enterTriggerForObjectUuid(NEW.uuid);
+
+ SELECT * FROM hs_office_relation WHERE uuid = OLD.partnerRelUuid INTO oldPartnerRel;
+ assert oldPartnerRel.uuid is not null, format('oldPartnerRel must not be null for OLD.partnerRelUuid = %s', OLD.partnerRelUuid);
+
+ SELECT * FROM hs_office_relation WHERE uuid = NEW.partnerRelUuid INTO newPartnerRel;
+ assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.partnerRelUuid = %s', NEW.partnerRelUuid);
+
+ SELECT * FROM hs_office_partner_details WHERE uuid = OLD.detailsUuid INTO oldPartnerDetails;
+ assert oldPartnerDetails.uuid is not null, format('oldPartnerDetails must not be null for OLD.detailsUuid = %s', OLD.detailsUuid);
+
+ SELECT * FROM hs_office_partner_details WHERE uuid = NEW.detailsUuid INTO newPartnerDetails;
+ assert newPartnerDetails.uuid is not null, format('newPartnerDetails must not be null for NEW.detailsUuid = %s', NEW.detailsUuid);
+
+
+ if NEW.partnerRelUuid <> OLD.partnerRelUuid then
+
+ call revokePermissionFromRole(getPermissionId(OLD.uuid, 'DELETE'), hsOfficeRelationADMIN(oldPartnerRel));
+ call grantPermissionToRole(createPermission(NEW.uuid, 'DELETE'), hsOfficeRelationADMIN(newPartnerRel));
+
+ call revokePermissionFromRole(getPermissionId(OLD.uuid, 'UPDATE'), hsOfficeRelationAGENT(oldPartnerRel));
+ call grantPermissionToRole(createPermission(NEW.uuid, 'UPDATE'), hsOfficeRelationAGENT(newPartnerRel));
+
+ call revokePermissionFromRole(getPermissionId(OLD.uuid, 'SELECT'), hsOfficeRelationTENANT(oldPartnerRel));
+ call grantPermissionToRole(createPermission(NEW.uuid, 'SELECT'), hsOfficeRelationTENANT(newPartnerRel));
+
+ call revokePermissionFromRole(getPermissionId(oldPartnerDetails.uuid, 'DELETE'), hsOfficeRelationADMIN(oldPartnerRel));
+ call grantPermissionToRole(createPermission(newPartnerDetails.uuid, 'DELETE'), hsOfficeRelationADMIN(newPartnerRel));
+
+ call revokePermissionFromRole(getPermissionId(oldPartnerDetails.uuid, 'UPDATE'), hsOfficeRelationAGENT(oldPartnerRel));
+ call grantPermissionToRole(createPermission(newPartnerDetails.uuid, 'UPDATE'), hsOfficeRelationAGENT(newPartnerRel));
+
+ call revokePermissionFromRole(getPermissionId(oldPartnerDetails.uuid, 'SELECT'), hsOfficeRelationAGENT(oldPartnerRel));
+ call grantPermissionToRole(createPermission(newPartnerDetails.uuid, 'SELECT'), hsOfficeRelationAGENT(newPartnerRel));
+
+ end if;
+
+ call leaveTriggerForObjectUuid(NEW.uuid);
+end; $$;
+
+/*
+ AFTER INSERT TRIGGER to re-wire the grant structure for a new hs_office_partner row.
+ */
+
+create or replace function updateTriggerForHsOfficePartner_tf()
+ returns trigger
+ language plpgsql
+ strict as $$
+begin
+ call updateRbacRulesForHsOfficePartner(OLD, NEW);
+ return NEW;
+end; $$;
+
+create trigger updateTriggerForHsOfficePartner_tg
+ after update on hs_office_partner
+ for each row
+execute procedure updateTriggerForHsOfficePartner_tf();
+--//
+
+
+-- ============================================================================
+--changeset hs-office-partner-rbac-INSERT:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates INSERT INTO hs_office_partner permissions for the related global rows.
+ */
+do language plpgsql $$
+ declare
+ row global;
+ begin
+ call defineContext('create INSERT INTO hs_office_partner permissions for the related global rows');
+
+ FOR row IN SELECT * FROM global
+ LOOP
+ call grantPermissionToRole(
+ createPermission(row.uuid, 'INSERT', 'hs_office_partner'),
+ globalADMIN());
+ END LOOP;
+ END;
+$$;
+
+/**
+ Adds hs_office_partner INSERT permission to specified role of new global rows.
+*/
+create or replace function hs_office_partner_global_insert_tf()
+ returns trigger
+ language plpgsql
+ strict as $$
+begin
+ call grantPermissionToRole(
+ createPermission(NEW.uuid, 'INSERT', 'hs_office_partner'),
+ globalADMIN());
+ return NEW;
+end; $$;
+
+-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
+create trigger z_hs_office_partner_global_insert_tg
+ after insert on global
+ for each row
+execute procedure hs_office_partner_global_insert_tf();
+
+/**
+ Checks if the user or assumed roles are allowed to insert a row to hs_office_partner,
+ where only global-admin has that permission.
+*/
+create or replace function hs_office_partner_insert_permission_missing_tf()
+ returns trigger
+ language plpgsql as $$
+begin
+ raise exception '[403] insert into hs_office_partner not allowed for current subjects % (%)',
+ currentSubjects(), currentSubjectsUuids();
+end; $$;
+
+create trigger hs_office_partner_insert_permission_check_tg
+ before insert on hs_office_partner
+ for each row
+ when ( not isGlobalAdmin() )
+ execute procedure hs_office_partner_insert_permission_missing_tf();
+--//
+
+-- ============================================================================
+--changeset hs-office-partner-rbac-IDENTITY-VIEW:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+call generateRbacIdentityViewFromProjection('hs_office_partner',
+ $idName$
+ 'P-' || partnerNumber
+ $idName$);
+--//
+
+-- ============================================================================
+--changeset hs-office-partner-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRbacRestrictedView('hs_office_partner',
+ $orderBy$
+ 'P-' || partnerNumber
+ $orderBy$,
+ $updates$
+ partnerRelUuid = new.partnerRelUuid
+ $updates$);
+--//
+
diff --git a/src/main/resources/db/changelog/5-hs-office/504-partner/5044-hs-office-partner-details-rbac.md b/src/main/resources/db/changelog/5-hs-office/504-partner/5044-hs-office-partner-details-rbac.md
new file mode 100644
index 00000000..347896bb
--- /dev/null
+++ b/src/main/resources/db/changelog/5-hs-office/504-partner/5044-hs-office-partner-details-rbac.md
@@ -0,0 +1,23 @@
+### rbac partnerDetails
+
+This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually.
+
+```mermaid
+%%{init:{'flowchart':{'htmlLabels':false}}}%%
+flowchart TB
+
+subgraph partnerDetails["`**partnerDetails**`"]
+ direction TB
+ style partnerDetails fill:#dd4901,stroke:#274d6e,stroke-width:8px
+
+ subgraph partnerDetails:permissions[ ]
+ style partnerDetails:permissions fill:#dd4901,stroke:white
+
+ perm:partnerDetails:INSERT{{partnerDetails:INSERT}}
+ end
+end
+
+%% granting permissions to roles
+role:global:ADMIN ==> perm:partnerDetails:INSERT
+
+```
diff --git a/src/main/resources/db/changelog/5-hs-office/504-partner/5044-hs-office-partner-details-rbac.sql b/src/main/resources/db/changelog/5-hs-office/504-partner/5044-hs-office-partner-details-rbac.sql
new file mode 100644
index 00000000..c99639bb
--- /dev/null
+++ b/src/main/resources/db/changelog/5-hs-office/504-partner/5044-hs-office-partner-details-rbac.sql
@@ -0,0 +1,150 @@
+--liquibase formatted sql
+-- This code generated was by RbacViewPostgresGenerator, do not amend manually.
+
+
+-- ============================================================================
+--changeset hs-office-partner-details-rbac-OBJECT:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRelatedRbacObject('hs_office_partner_details');
+--//
+
+
+-- ============================================================================
+--changeset hs-office-partner-details-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRbacRoleDescriptors('hsOfficePartnerDetails', 'hs_office_partner_details');
+--//
+
+
+-- ============================================================================
+--changeset hs-office-partner-details-rbac-insert-trigger:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates the roles, grants and permission for the AFTER INSERT TRIGGER.
+ */
+
+create or replace procedure buildRbacSystemForHsOfficePartnerDetails(
+ NEW hs_office_partner_details
+)
+ language plpgsql as $$
+
+declare
+
+begin
+ call enterTriggerForObjectUuid(NEW.uuid);
+
+ call leaveTriggerForObjectUuid(NEW.uuid);
+end; $$;
+
+/*
+ AFTER INSERT TRIGGER to create the role+grant structure for a new hs_office_partner_details row.
+ */
+
+create or replace function insertTriggerForHsOfficePartnerDetails_tf()
+ returns trigger
+ language plpgsql
+ strict as $$
+begin
+ call buildRbacSystemForHsOfficePartnerDetails(NEW);
+ return NEW;
+end; $$;
+
+create trigger insertTriggerForHsOfficePartnerDetails_tg
+ after insert on hs_office_partner_details
+ for each row
+execute procedure insertTriggerForHsOfficePartnerDetails_tf();
+--//
+
+
+-- ============================================================================
+--changeset hs-office-partner-details-rbac-INSERT:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates INSERT INTO hs_office_partner_details permissions for the related global rows.
+ */
+do language plpgsql $$
+ declare
+ row global;
+ begin
+ call defineContext('create INSERT INTO hs_office_partner_details permissions for the related global rows');
+
+ FOR row IN SELECT * FROM global
+ LOOP
+ call grantPermissionToRole(
+ createPermission(row.uuid, 'INSERT', 'hs_office_partner_details'),
+ globalADMIN());
+ END LOOP;
+ END;
+$$;
+
+/**
+ Adds hs_office_partner_details INSERT permission to specified role of new global rows.
+*/
+create or replace function hs_office_partner_details_global_insert_tf()
+ returns trigger
+ language plpgsql
+ strict as $$
+begin
+ call grantPermissionToRole(
+ createPermission(NEW.uuid, 'INSERT', 'hs_office_partner_details'),
+ globalADMIN());
+ return NEW;
+end; $$;
+
+-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
+create trigger z_hs_office_partner_details_global_insert_tg
+ after insert on global
+ for each row
+execute procedure hs_office_partner_details_global_insert_tf();
+
+/**
+ Checks if the user or assumed roles are allowed to insert a row to hs_office_partner_details,
+ where only global-admin has that permission.
+*/
+create or replace function hs_office_partner_details_insert_permission_missing_tf()
+ returns trigger
+ language plpgsql as $$
+begin
+ raise exception '[403] insert into hs_office_partner_details not allowed for current subjects % (%)',
+ currentSubjects(), currentSubjectsUuids();
+end; $$;
+
+create trigger hs_office_partner_details_insert_permission_check_tg
+ before insert on hs_office_partner_details
+ for each row
+ when ( not isGlobalAdmin() )
+ execute procedure hs_office_partner_details_insert_permission_missing_tf();
+--//
+
+-- ============================================================================
+--changeset hs-office-partner-details-rbac-IDENTITY-VIEW:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+ call generateRbacIdentityViewFromQuery('hs_office_partner_details',
+ $idName$
+ SELECT partnerDetails.uuid as uuid, partner_iv.idName as idName
+ 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
+ $idName$);
+--//
+
+-- ============================================================================
+--changeset hs-office-partner-details-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRbacRestrictedView('hs_office_partner_details',
+ $orderBy$
+ uuid
+ $orderBy$,
+ $updates$
+ registrationOffice = new.registrationOffice,
+ registrationNumber = new.registrationNumber,
+ birthPlace = new.birthPlace,
+ birthName = new.birthName,
+ birthday = new.birthday,
+ dateOfDeath = new.dateOfDeath
+ $updates$);
+--//
+
diff --git a/src/main/resources/db/changelog/236-hs-office-partner-migration.sql b/src/main/resources/db/changelog/5-hs-office/504-partner/5046-hs-office-partner-migration.sql
similarity index 100%
rename from src/main/resources/db/changelog/236-hs-office-partner-migration.sql
rename to src/main/resources/db/changelog/5-hs-office/504-partner/5046-hs-office-partner-migration.sql
diff --git a/src/main/resources/db/changelog/238-hs-office-partner-test-data.sql b/src/main/resources/db/changelog/5-hs-office/504-partner/5048-hs-office-partner-test-data.sql
similarity index 71%
rename from src/main/resources/db/changelog/238-hs-office-partner-test-data.sql
rename to src/main/resources/db/changelog/5-hs-office/504-partner/5048-hs-office-partner-test-data.sql
index 146f2f1d..65017b18 100644
--- a/src/main/resources/db/changelog/238-hs-office-partner-test-data.sql
+++ b/src/main/resources/db/changelog/5-hs-office/504-partner/5048-hs-office-partner-test-data.sql
@@ -9,23 +9,22 @@
Creates a single partner test record.
*/
create or replace procedure createHsOfficePartnerTestData(
- mandantTradeName varchar,
- partnerNumber numeric(5),
+ mandantTradeName varchar,
+ newPartnerNumber numeric(5),
partnerPersonName varchar,
- contactLabel varchar )
+ contactLabel varchar )
language plpgsql as $$
declare
currentTask varchar;
idName varchar;
mandantPerson hs_office_person;
- partnerRole hs_office_relationship;
+ partnerRel hs_office_relation;
relatedPerson hs_office_person;
- relatedContact hs_office_contact;
relatedDetailsUuid uuid;
begin
idName := cleanIdentifier( partnerPersonName|| '-' || contactLabel);
currentTask := 'creating partner test-data ' || idName;
- call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global.admin');
+ call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global:ADMIN');
execute format('set local hsadminng.currentTask to %L', currentTask);
select p.* from hs_office_person p
@@ -38,22 +37,18 @@ begin
select p.* from hs_office_person p
where p.tradeName = partnerPersonName or p.familyName = partnerPersonName
into relatedPerson;
- select c.* from hs_office_contact c
- where c.label = contactLabel
- into relatedContact;
- select r.* from hs_office_relationship r
- where r.reltype = 'PARTNER'
- and r.relanchoruuid = mandantPerson.uuid and r.relholderuuid = relatedPerson.uuid
- into partnerRole;
- if partnerRole is null then
- raise exception 'partnerRole "%"-"%" not found', mandantPerson.tradename, partnerPersonName;
+ select r.* from hs_office_relation r
+ where r.type = 'PARTNER'
+ and r.anchoruuid = mandantPerson.uuid and r.holderuuid = relatedPerson.uuid
+ into partnerRel;
+ if partnerRel is null then
+ raise exception 'partnerRel "%"-"%" not found', mandantPerson.tradename, partnerPersonName;
end if;
raise notice 'creating test partner: %', idName;
- raise notice '- using partnerRole (%): %', partnerRole.uuid, partnerRole;
+ raise notice '- using partnerRel (%): %', partnerRel.uuid, partnerRel;
raise notice '- using person (%): %', relatedPerson.uuid, relatedPerson;
- raise notice '- using contact (%): %', relatedContact.uuid, relatedContact;
if relatedPerson.persontype = 'NP' then
insert
@@ -68,8 +63,8 @@ begin
end if;
insert
- into hs_office_partner (uuid, partnerNumber, partnerRoleUuid, personuuid, contactuuid, detailsUuid)
- values (uuid_generate_v4(), partnerNumber, partnerRole.uuid, relatedPerson.uuid, relatedContact.uuid, relatedDetailsUuid);
+ into hs_office_partner (uuid, partnerNumber, partnerRelUuid, detailsUuid)
+ values (uuid_generate_v4(), newPartnerNumber, partnerRel.uuid, relatedDetailsUuid);
end; $$;
--//
diff --git a/src/main/resources/db/changelog/240-hs-office-bankaccount.sql b/src/main/resources/db/changelog/5-hs-office/505-bankaccount/5050-hs-office-bankaccount.sql
similarity index 100%
rename from src/main/resources/db/changelog/240-hs-office-bankaccount.sql
rename to src/main/resources/db/changelog/5-hs-office/505-bankaccount/5050-hs-office-bankaccount.sql
diff --git a/src/main/resources/db/changelog/5-hs-office/505-bankaccount/5053-hs-office-bankaccount-rbac.md b/src/main/resources/db/changelog/5-hs-office/505-bankaccount/5053-hs-office-bankaccount-rbac.md
new file mode 100644
index 00000000..4558815c
--- /dev/null
+++ b/src/main/resources/db/changelog/5-hs-office/505-bankaccount/5053-hs-office-bankaccount-rbac.md
@@ -0,0 +1,45 @@
+### rbac bankAccount
+
+This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually.
+
+```mermaid
+%%{init:{'flowchart':{'htmlLabels':false}}}%%
+flowchart TB
+
+subgraph bankAccount["`**bankAccount**`"]
+ direction TB
+ style bankAccount fill:#dd4901,stroke:#274d6e,stroke-width:8px
+
+ subgraph bankAccount:roles[ ]
+ style bankAccount:roles fill:#dd4901,stroke:white
+
+ role:bankAccount:OWNER[[bankAccount:OWNER]]
+ role:bankAccount:ADMIN[[bankAccount:ADMIN]]
+ role:bankAccount:REFERRER[[bankAccount:REFERRER]]
+ end
+
+ subgraph bankAccount:permissions[ ]
+ style bankAccount:permissions fill:#dd4901,stroke:white
+
+ perm:bankAccount:INSERT{{bankAccount:INSERT}}
+ perm:bankAccount:DELETE{{bankAccount:DELETE}}
+ perm:bankAccount:UPDATE{{bankAccount:UPDATE}}
+ perm:bankAccount:SELECT{{bankAccount:SELECT}}
+ end
+end
+
+%% granting roles to users
+user:creator ==> role:bankAccount:OWNER
+
+%% granting roles to roles
+role:global:ADMIN ==> role:bankAccount:OWNER
+role:bankAccount:OWNER ==> role:bankAccount:ADMIN
+role:bankAccount:ADMIN ==> role:bankAccount:REFERRER
+
+%% granting permissions to roles
+role:global:GUEST ==> perm:bankAccount:INSERT
+role:bankAccount:OWNER ==> perm:bankAccount:DELETE
+role:bankAccount:ADMIN ==> perm:bankAccount:UPDATE
+role:bankAccount:REFERRER ==> perm:bankAccount:SELECT
+
+```
diff --git a/src/main/resources/db/changelog/5-hs-office/505-bankaccount/5053-hs-office-bankaccount-rbac.sql b/src/main/resources/db/changelog/5-hs-office/505-bankaccount/5053-hs-office-bankaccount-rbac.sql
new file mode 100644
index 00000000..c12c4c88
--- /dev/null
+++ b/src/main/resources/db/changelog/5-hs-office/505-bankaccount/5053-hs-office-bankaccount-rbac.sql
@@ -0,0 +1,145 @@
+--liquibase formatted sql
+-- This code generated was by RbacViewPostgresGenerator, do not amend manually.
+
+
+-- ============================================================================
+--changeset hs-office-bankaccount-rbac-OBJECT:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRelatedRbacObject('hs_office_bankaccount');
+--//
+
+
+-- ============================================================================
+--changeset hs-office-bankaccount-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRbacRoleDescriptors('hsOfficeBankAccount', 'hs_office_bankaccount');
+--//
+
+
+-- ============================================================================
+--changeset hs-office-bankaccount-rbac-insert-trigger:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates the roles, grants and permission for the AFTER INSERT TRIGGER.
+ */
+
+create or replace procedure buildRbacSystemForHsOfficeBankAccount(
+ NEW hs_office_bankaccount
+)
+ language plpgsql as $$
+
+declare
+
+begin
+ call enterTriggerForObjectUuid(NEW.uuid);
+
+ perform createRoleWithGrants(
+ hsOfficeBankAccountOWNER(NEW),
+ permissions => array['DELETE'],
+ incomingSuperRoles => array[globalADMIN()],
+ userUuids => array[currentUserUuid()]
+ );
+
+ perform createRoleWithGrants(
+ hsOfficeBankAccountADMIN(NEW),
+ permissions => array['UPDATE'],
+ incomingSuperRoles => array[hsOfficeBankAccountOWNER(NEW)]
+ );
+
+ perform createRoleWithGrants(
+ hsOfficeBankAccountREFERRER(NEW),
+ permissions => array['SELECT'],
+ incomingSuperRoles => array[hsOfficeBankAccountADMIN(NEW)]
+ );
+
+ call leaveTriggerForObjectUuid(NEW.uuid);
+end; $$;
+
+/*
+ AFTER INSERT TRIGGER to create the role+grant structure for a new hs_office_bankaccount row.
+ */
+
+create or replace function insertTriggerForHsOfficeBankAccount_tf()
+ returns trigger
+ language plpgsql
+ strict as $$
+begin
+ call buildRbacSystemForHsOfficeBankAccount(NEW);
+ return NEW;
+end; $$;
+
+create trigger insertTriggerForHsOfficeBankAccount_tg
+ after insert on hs_office_bankaccount
+ for each row
+execute procedure insertTriggerForHsOfficeBankAccount_tf();
+--//
+
+
+-- ============================================================================
+--changeset hs-office-bankaccount-rbac-INSERT:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates INSERT INTO hs_office_bankaccount permissions for the related global rows.
+ */
+do language plpgsql $$
+ declare
+ row global;
+ begin
+ call defineContext('create INSERT INTO hs_office_bankaccount permissions for the related global rows');
+
+ FOR row IN SELECT * FROM global
+ LOOP
+ call grantPermissionToRole(
+ createPermission(row.uuid, 'INSERT', 'hs_office_bankaccount'),
+ globalGUEST());
+ END LOOP;
+ END;
+$$;
+
+/**
+ Adds hs_office_bankaccount INSERT permission to specified role of new global rows.
+*/
+create or replace function hs_office_bankaccount_global_insert_tf()
+ returns trigger
+ language plpgsql
+ strict as $$
+begin
+ call grantPermissionToRole(
+ createPermission(NEW.uuid, 'INSERT', 'hs_office_bankaccount'),
+ globalGUEST());
+ return NEW;
+end; $$;
+
+-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
+create trigger z_hs_office_bankaccount_global_insert_tg
+ after insert on global
+ for each row
+execute procedure hs_office_bankaccount_global_insert_tf();
+--//
+
+-- ============================================================================
+--changeset hs-office-bankaccount-rbac-IDENTITY-VIEW:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+call generateRbacIdentityViewFromProjection('hs_office_bankaccount',
+ $idName$
+ iban
+ $idName$);
+--//
+
+-- ============================================================================
+--changeset hs-office-bankaccount-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRbacRestrictedView('hs_office_bankaccount',
+ $orderBy$
+ iban
+ $orderBy$,
+ $updates$
+ holder = new.holder,
+ iban = new.iban,
+ bic = new.bic
+ $updates$);
+--//
+
diff --git a/src/main/resources/db/changelog/248-hs-office-bankaccount-test-data.sql b/src/main/resources/db/changelog/5-hs-office/505-bankaccount/5058-hs-office-bankaccount-test-data.sql
similarity index 100%
rename from src/main/resources/db/changelog/248-hs-office-bankaccount-test-data.sql
rename to src/main/resources/db/changelog/5-hs-office/505-bankaccount/5058-hs-office-bankaccount-test-data.sql
diff --git a/src/main/resources/db/changelog/270-hs-office-debitor.sql b/src/main/resources/db/changelog/5-hs-office/506-debitor/5060-hs-office-debitor.sql
similarity index 51%
rename from src/main/resources/db/changelog/270-hs-office-debitor.sql
rename to src/main/resources/db/changelog/5-hs-office/506-debitor/5060-hs-office-debitor.sql
index fae4e90c..59ad01e0 100644
--- a/src/main/resources/db/changelog/270-hs-office-debitor.sql
+++ b/src/main/resources/db/changelog/5-hs-office/506-debitor/5060-hs-office-debitor.sql
@@ -7,10 +7,9 @@
create table hs_office_debitor
(
uuid uuid unique references RbacObject (uuid) initially deferred,
- partnerUuid uuid not null references hs_office_partner(uuid),
+ debitorNumberSuffix char(2) not null check (debitorNumberSuffix::text ~ '^[0-9][0-9]$'),
+ debitorRelUuid uuid not null references hs_office_relation(uuid),
billable boolean not null default true,
- debitorNumberSuffix numeric(2) not null,
- billingContactUuid uuid not null references hs_office_contact(uuid),
vatId varchar(24), -- TODO.spec: here or in person?
vatCountryCode varchar(2),
vatBusiness boolean not null,
@@ -20,11 +19,43 @@ create table hs_office_debitor
constraint check_default_prefix check (
defaultPrefix::text ~ '^([a-z]{3}|al0|bh1|c4s|f3k|k8i|l3d|mh1|o13|p2m|s80|t4w)$'
)
- -- TODO.impl: SEPA-mandate
);
--//
+-- ============================================================================
+--changeset hs-office-debitor-DELETE-DEPENDENTS-TRIGGER:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/**
+ Trigger function to delete related rows of a debitor to delete.
+ */
+create or replace function deleteHsOfficeDependentsOnDebitorDelete()
+ returns trigger
+ language PLPGSQL
+as $$
+declare
+ counter integer;
+begin
+ DELETE FROM hs_office_relation r WHERE r.uuid = OLD.debitorRelUuid;
+ GET DIAGNOSTICS counter = ROW_COUNT;
+ if counter = 0 then
+ raise exception 'debitor relation % could not be deleted', OLD.debitorRelUuid;
+ end if;
+
+ RETURN OLD;
+end; $$;
+
+/**
+ Triggers deletion of related details of a debitor to delete.
+ */
+create trigger hs_office_debitor_delete_dependents_trigger
+ after delete
+ on hs_office_debitor
+ for each row
+execute procedure deleteHsOfficeDependentsOnDebitorDelete();
+
+
-- ============================================================================
--changeset hs-office-debitor-MAIN-TABLE-JOURNAL:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
diff --git a/src/main/resources/db/changelog/5-hs-office/506-debitor/5063-hs-office-debitor-rbac.md b/src/main/resources/db/changelog/5-hs-office/506-debitor/5063-hs-office-debitor-rbac.md
new file mode 100644
index 00000000..5c43e03d
--- /dev/null
+++ b/src/main/resources/db/changelog/5-hs-office/506-debitor/5063-hs-office-debitor-rbac.md
@@ -0,0 +1,198 @@
+### rbac debitor
+
+This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually.
+
+```mermaid
+%%{init:{'flowchart':{'htmlLabels':false}}}%%
+flowchart TB
+
+subgraph debitorRel.anchorPerson["`**debitorRel.anchorPerson**`"]
+ direction TB
+ style debitorRel.anchorPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph debitorRel.anchorPerson:roles[ ]
+ style debitorRel.anchorPerson:roles fill:#99bcdb,stroke:white
+
+ role:debitorRel.anchorPerson:OWNER[[debitorRel.anchorPerson:OWNER]]
+ role:debitorRel.anchorPerson:ADMIN[[debitorRel.anchorPerson:ADMIN]]
+ role:debitorRel.anchorPerson:REFERRER[[debitorRel.anchorPerson:REFERRER]]
+ end
+end
+
+subgraph debitorRel.holderPerson["`**debitorRel.holderPerson**`"]
+ direction TB
+ style debitorRel.holderPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph debitorRel.holderPerson:roles[ ]
+ style debitorRel.holderPerson:roles fill:#99bcdb,stroke:white
+
+ role:debitorRel.holderPerson:OWNER[[debitorRel.holderPerson:OWNER]]
+ role:debitorRel.holderPerson:ADMIN[[debitorRel.holderPerson:ADMIN]]
+ role:debitorRel.holderPerson:REFERRER[[debitorRel.holderPerson:REFERRER]]
+ end
+end
+
+subgraph partnerRel.holderPerson["`**partnerRel.holderPerson**`"]
+ direction TB
+ style partnerRel.holderPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph partnerRel.holderPerson:roles[ ]
+ style partnerRel.holderPerson:roles fill:#99bcdb,stroke:white
+
+ role:partnerRel.holderPerson:OWNER[[partnerRel.holderPerson:OWNER]]
+ role:partnerRel.holderPerson:ADMIN[[partnerRel.holderPerson:ADMIN]]
+ role:partnerRel.holderPerson:REFERRER[[partnerRel.holderPerson:REFERRER]]
+ end
+end
+
+subgraph debitor["`**debitor**`"]
+ direction TB
+ style debitor fill:#dd4901,stroke:#274d6e,stroke-width:8px
+
+ subgraph debitor:permissions[ ]
+ style debitor:permissions fill:#dd4901,stroke:white
+
+ perm:debitor:INSERT{{debitor:INSERT}}
+ perm:debitor:DELETE{{debitor:DELETE}}
+ perm:debitor:UPDATE{{debitor:UPDATE}}
+ perm:debitor:SELECT{{debitor:SELECT}}
+ end
+
+ subgraph debitorRel["`**debitorRel**`"]
+ direction TB
+ style debitorRel fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph debitorRel:roles[ ]
+ style debitorRel:roles fill:#99bcdb,stroke:white
+
+ role:debitorRel:OWNER[[debitorRel:OWNER]]
+ role:debitorRel:ADMIN[[debitorRel:ADMIN]]
+ role:debitorRel:AGENT[[debitorRel:AGENT]]
+ role:debitorRel:TENANT[[debitorRel:TENANT]]
+ end
+ end
+end
+
+subgraph partnerRel["`**partnerRel**`"]
+ direction TB
+ style partnerRel fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph partnerRel:roles[ ]
+ style partnerRel:roles fill:#99bcdb,stroke:white
+
+ role:partnerRel:OWNER[[partnerRel:OWNER]]
+ role:partnerRel:ADMIN[[partnerRel:ADMIN]]
+ role:partnerRel:AGENT[[partnerRel:AGENT]]
+ role:partnerRel:TENANT[[partnerRel:TENANT]]
+ end
+end
+
+subgraph partnerRel.contact["`**partnerRel.contact**`"]
+ direction TB
+ style partnerRel.contact fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph partnerRel.contact:roles[ ]
+ style partnerRel.contact:roles fill:#99bcdb,stroke:white
+
+ role:partnerRel.contact:OWNER[[partnerRel.contact:OWNER]]
+ role:partnerRel.contact:ADMIN[[partnerRel.contact:ADMIN]]
+ role:partnerRel.contact:REFERRER[[partnerRel.contact:REFERRER]]
+ end
+end
+
+subgraph debitorRel.contact["`**debitorRel.contact**`"]
+ direction TB
+ style debitorRel.contact fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph debitorRel.contact:roles[ ]
+ style debitorRel.contact:roles fill:#99bcdb,stroke:white
+
+ role:debitorRel.contact:OWNER[[debitorRel.contact:OWNER]]
+ role:debitorRel.contact:ADMIN[[debitorRel.contact:ADMIN]]
+ role:debitorRel.contact:REFERRER[[debitorRel.contact:REFERRER]]
+ end
+end
+
+subgraph partnerRel.anchorPerson["`**partnerRel.anchorPerson**`"]
+ direction TB
+ style partnerRel.anchorPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph partnerRel.anchorPerson:roles[ ]
+ style partnerRel.anchorPerson:roles fill:#99bcdb,stroke:white
+
+ role:partnerRel.anchorPerson:OWNER[[partnerRel.anchorPerson:OWNER]]
+ role:partnerRel.anchorPerson:ADMIN[[partnerRel.anchorPerson:ADMIN]]
+ role:partnerRel.anchorPerson:REFERRER[[partnerRel.anchorPerson:REFERRER]]
+ end
+end
+
+subgraph refundBankAccount["`**refundBankAccount**`"]
+ direction TB
+ style refundBankAccount fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph refundBankAccount:roles[ ]
+ style refundBankAccount:roles fill:#99bcdb,stroke:white
+
+ role:refundBankAccount:OWNER[[refundBankAccount:OWNER]]
+ role:refundBankAccount:ADMIN[[refundBankAccount:ADMIN]]
+ role:refundBankAccount:REFERRER[[refundBankAccount:REFERRER]]
+ end
+end
+
+%% granting roles to roles
+role:global:ADMIN -.-> role:debitorRel.anchorPerson:OWNER
+role:debitorRel.anchorPerson:OWNER -.-> role:debitorRel.anchorPerson:ADMIN
+role:debitorRel.anchorPerson:ADMIN -.-> role:debitorRel.anchorPerson:REFERRER
+role:global:ADMIN -.-> role:debitorRel.holderPerson:OWNER
+role:debitorRel.holderPerson:OWNER -.-> role:debitorRel.holderPerson:ADMIN
+role:debitorRel.holderPerson:ADMIN -.-> role:debitorRel.holderPerson:REFERRER
+role:global:ADMIN -.-> role:debitorRel.contact:OWNER
+role:debitorRel.contact:OWNER -.-> role:debitorRel.contact:ADMIN
+role:debitorRel.contact:ADMIN -.-> role:debitorRel.contact:REFERRER
+role:global:ADMIN -.-> role:debitorRel:OWNER
+role:debitorRel:OWNER -.-> role:debitorRel:ADMIN
+role:debitorRel.anchorPerson:ADMIN -.-> role:debitorRel:ADMIN
+role:debitorRel:ADMIN -.-> role:debitorRel:AGENT
+role:debitorRel.holderPerson:ADMIN -.-> role:debitorRel:AGENT
+role:debitorRel:AGENT -.-> role:debitorRel:TENANT
+role:debitorRel.holderPerson:ADMIN -.-> role:debitorRel:TENANT
+role:debitorRel.contact:ADMIN -.-> role:debitorRel:TENANT
+role:debitorRel:TENANT -.-> role:debitorRel.anchorPerson:REFERRER
+role:debitorRel:TENANT -.-> role:debitorRel.holderPerson:REFERRER
+role:debitorRel:TENANT -.-> role:debitorRel.contact:REFERRER
+role:global:ADMIN -.-> role:refundBankAccount:OWNER
+role:refundBankAccount:OWNER -.-> role:refundBankAccount:ADMIN
+role:refundBankAccount:ADMIN -.-> role:refundBankAccount:REFERRER
+role:refundBankAccount:ADMIN ==> role:debitorRel:AGENT
+role:debitorRel:AGENT ==> role:refundBankAccount:REFERRER
+role:global:ADMIN -.-> role:partnerRel.anchorPerson:OWNER
+role:partnerRel.anchorPerson:OWNER -.-> role:partnerRel.anchorPerson:ADMIN
+role:partnerRel.anchorPerson:ADMIN -.-> role:partnerRel.anchorPerson:REFERRER
+role:global:ADMIN -.-> role:partnerRel.holderPerson:OWNER
+role:partnerRel.holderPerson:OWNER -.-> role:partnerRel.holderPerson:ADMIN
+role:partnerRel.holderPerson:ADMIN -.-> role:partnerRel.holderPerson:REFERRER
+role:global:ADMIN -.-> role:partnerRel.contact:OWNER
+role:partnerRel.contact:OWNER -.-> role:partnerRel.contact:ADMIN
+role:partnerRel.contact:ADMIN -.-> role:partnerRel.contact:REFERRER
+role:global:ADMIN -.-> role:partnerRel:OWNER
+role:partnerRel:OWNER -.-> role:partnerRel:ADMIN
+role:partnerRel.anchorPerson:ADMIN -.-> role:partnerRel:ADMIN
+role:partnerRel:ADMIN -.-> role:partnerRel:AGENT
+role:partnerRel.holderPerson:ADMIN -.-> role:partnerRel:AGENT
+role:partnerRel:AGENT -.-> role:partnerRel:TENANT
+role:partnerRel.holderPerson:ADMIN -.-> role:partnerRel:TENANT
+role:partnerRel.contact:ADMIN -.-> role:partnerRel:TENANT
+role:partnerRel:TENANT -.-> role:partnerRel.anchorPerson:REFERRER
+role:partnerRel:TENANT -.-> role:partnerRel.holderPerson:REFERRER
+role:partnerRel:TENANT -.-> role:partnerRel.contact:REFERRER
+role:partnerRel:ADMIN ==> role:debitorRel:ADMIN
+role:partnerRel:AGENT ==> role:debitorRel:AGENT
+role:debitorRel:AGENT ==> role:partnerRel:TENANT
+
+%% granting permissions to roles
+role:global:ADMIN ==> perm:debitor:INSERT
+role:debitorRel:OWNER ==> perm:debitor:DELETE
+role:debitorRel:ADMIN ==> perm:debitor:UPDATE
+role:debitorRel:TENANT ==> perm:debitor:SELECT
+
+```
diff --git a/src/main/resources/db/changelog/5-hs-office/506-debitor/5063-hs-office-debitor-rbac.sql b/src/main/resources/db/changelog/5-hs-office/506-debitor/5063-hs-office-debitor-rbac.sql
new file mode 100644
index 00000000..59ac43e8
--- /dev/null
+++ b/src/main/resources/db/changelog/5-hs-office/506-debitor/5063-hs-office-debitor-rbac.sql
@@ -0,0 +1,227 @@
+--liquibase formatted sql
+-- This code generated was by RbacViewPostgresGenerator, do not amend manually.
+
+
+-- ============================================================================
+--changeset hs-office-debitor-rbac-OBJECT:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRelatedRbacObject('hs_office_debitor');
+--//
+
+
+-- ============================================================================
+--changeset hs-office-debitor-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRbacRoleDescriptors('hsOfficeDebitor', 'hs_office_debitor');
+--//
+
+
+-- ============================================================================
+--changeset hs-office-debitor-rbac-insert-trigger:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates the roles, grants and permission for the AFTER INSERT TRIGGER.
+ */
+
+create or replace procedure buildRbacSystemForHsOfficeDebitor(
+ NEW hs_office_debitor
+)
+ language plpgsql as $$
+
+declare
+ newPartnerRel hs_office_relation;
+ newDebitorRel hs_office_relation;
+ newRefundBankAccount hs_office_bankaccount;
+
+begin
+ call enterTriggerForObjectUuid(NEW.uuid);
+
+ SELECT partnerRel.*
+ FROM hs_office_relation AS partnerRel
+ JOIN hs_office_relation AS debitorRel
+ ON debitorRel.type = 'DEBITOR' AND debitorRel.anchorUuid = partnerRel.holderUuid
+ WHERE partnerRel.type = 'PARTNER'
+ AND NEW.debitorRelUuid = debitorRel.uuid
+ INTO newPartnerRel;
+ assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.debitorRelUuid = %s', NEW.debitorRelUuid);
+
+ SELECT * FROM hs_office_relation WHERE uuid = NEW.debitorRelUuid INTO newDebitorRel;
+ assert newDebitorRel.uuid is not null, format('newDebitorRel must not be null for NEW.debitorRelUuid = %s', NEW.debitorRelUuid);
+
+ SELECT * FROM hs_office_bankaccount WHERE uuid = NEW.refundBankAccountUuid INTO newRefundBankAccount;
+
+ call grantRoleToRole(hsOfficeBankAccountREFERRER(newRefundBankAccount), hsOfficeRelationAGENT(newDebitorRel));
+ call grantRoleToRole(hsOfficeRelationADMIN(newDebitorRel), hsOfficeRelationADMIN(newPartnerRel));
+ call grantRoleToRole(hsOfficeRelationAGENT(newDebitorRel), hsOfficeBankAccountADMIN(newRefundBankAccount));
+ call grantRoleToRole(hsOfficeRelationAGENT(newDebitorRel), hsOfficeRelationAGENT(newPartnerRel));
+ call grantRoleToRole(hsOfficeRelationTENANT(newPartnerRel), hsOfficeRelationAGENT(newDebitorRel));
+
+ call grantPermissionToRole(createPermission(NEW.uuid, 'DELETE'), hsOfficeRelationOWNER(newDebitorRel));
+ call grantPermissionToRole(createPermission(NEW.uuid, 'SELECT'), hsOfficeRelationTENANT(newDebitorRel));
+ call grantPermissionToRole(createPermission(NEW.uuid, 'UPDATE'), hsOfficeRelationADMIN(newDebitorRel));
+
+ call leaveTriggerForObjectUuid(NEW.uuid);
+end; $$;
+
+/*
+ AFTER INSERT TRIGGER to create the role+grant structure for a new hs_office_debitor row.
+ */
+
+create or replace function insertTriggerForHsOfficeDebitor_tf()
+ returns trigger
+ language plpgsql
+ strict as $$
+begin
+ call buildRbacSystemForHsOfficeDebitor(NEW);
+ return NEW;
+end; $$;
+
+create trigger insertTriggerForHsOfficeDebitor_tg
+ after insert on hs_office_debitor
+ for each row
+execute procedure insertTriggerForHsOfficeDebitor_tf();
+--//
+
+
+-- ============================================================================
+--changeset hs-office-debitor-rbac-update-trigger:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Called from the AFTER UPDATE TRIGGER to re-wire the grants.
+ */
+
+create or replace procedure updateRbacRulesForHsOfficeDebitor(
+ OLD hs_office_debitor,
+ NEW hs_office_debitor
+)
+ language plpgsql as $$
+begin
+
+ if NEW.debitorRelUuid is distinct from OLD.debitorRelUuid
+ or NEW.refundBankAccountUuid is distinct from OLD.refundBankAccountUuid then
+ delete from rbacgrants g where g.grantedbytriggerof = OLD.uuid;
+ call buildRbacSystemForHsOfficeDebitor(NEW);
+ end if;
+end; $$;
+
+/*
+ AFTER INSERT TRIGGER to re-wire the grant structure for a new hs_office_debitor row.
+ */
+
+create or replace function updateTriggerForHsOfficeDebitor_tf()
+ returns trigger
+ language plpgsql
+ strict as $$
+begin
+ call updateRbacRulesForHsOfficeDebitor(OLD, NEW);
+ return NEW;
+end; $$;
+
+create trigger updateTriggerForHsOfficeDebitor_tg
+ after update on hs_office_debitor
+ for each row
+execute procedure updateTriggerForHsOfficeDebitor_tf();
+--//
+
+
+-- ============================================================================
+--changeset hs-office-debitor-rbac-INSERT:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates INSERT INTO hs_office_debitor permissions for the related global rows.
+ */
+do language plpgsql $$
+ declare
+ row global;
+ begin
+ call defineContext('create INSERT INTO hs_office_debitor permissions for the related global rows');
+
+ FOR row IN SELECT * FROM global
+ LOOP
+ call grantPermissionToRole(
+ createPermission(row.uuid, 'INSERT', 'hs_office_debitor'),
+ globalADMIN());
+ END LOOP;
+ END;
+$$;
+
+/**
+ Adds hs_office_debitor INSERT permission to specified role of new global rows.
+*/
+create or replace function hs_office_debitor_global_insert_tf()
+ returns trigger
+ language plpgsql
+ strict as $$
+begin
+ call grantPermissionToRole(
+ createPermission(NEW.uuid, 'INSERT', 'hs_office_debitor'),
+ globalADMIN());
+ return NEW;
+end; $$;
+
+-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
+create trigger z_hs_office_debitor_global_insert_tg
+ after insert on global
+ for each row
+execute procedure hs_office_debitor_global_insert_tf();
+
+/**
+ Checks if the user or assumed roles are allowed to insert a row to hs_office_debitor,
+ where only global-admin has that permission.
+*/
+create or replace function hs_office_debitor_insert_permission_missing_tf()
+ returns trigger
+ language plpgsql as $$
+begin
+ raise exception '[403] insert into hs_office_debitor not allowed for current subjects % (%)',
+ currentSubjects(), currentSubjectsUuids();
+end; $$;
+
+create trigger hs_office_debitor_insert_permission_check_tg
+ before insert on hs_office_debitor
+ for each row
+ when ( not isGlobalAdmin() )
+ execute procedure hs_office_debitor_insert_permission_missing_tf();
+--//
+
+-- ============================================================================
+--changeset hs-office-debitor-rbac-IDENTITY-VIEW:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+ call generateRbacIdentityViewFromQuery('hs_office_debitor',
+ $idName$
+ SELECT debitor.uuid AS uuid,
+ 'D-' || (SELECT partner.partnerNumber
+ FROM hs_office_partner partner
+ JOIN hs_office_relation partnerRel
+ ON partnerRel.uuid = partner.partnerRelUUid AND partnerRel.type = 'PARTNER'
+ JOIN hs_office_relation debitorRel
+ ON debitorRel.anchorUuid = partnerRel.holderUuid AND debitorRel.type = 'DEBITOR'
+ WHERE debitorRel.uuid = debitor.debitorRelUuid)
+ || debitorNumberSuffix as idName
+ FROM hs_office_debitor AS debitor
+ $idName$);
+--//
+
+-- ============================================================================
+--changeset hs-office-debitor-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRbacRestrictedView('hs_office_debitor',
+ $orderBy$
+ defaultPrefix
+ $orderBy$,
+ $updates$
+ debitorRelUuid = new.debitorRelUuid,
+ billable = new.billable,
+ refundBankAccountUuid = new.refundBankAccountUuid,
+ vatId = new.vatId,
+ vatCountryCode = new.vatCountryCode,
+ vatBusiness = new.vatBusiness,
+ vatReverseCharge = new.vatReverseCharge,
+ defaultPrefix = new.defaultPrefix
+ $updates$);
+--//
+
diff --git a/src/main/resources/db/changelog/5-hs-office/506-debitor/5068-hs-office-debitor-test-data.sql b/src/main/resources/db/changelog/5-hs-office/506-debitor/5068-hs-office-debitor-test-data.sql
new file mode 100644
index 00000000..ed965104
--- /dev/null
+++ b/src/main/resources/db/changelog/5-hs-office/506-debitor/5068-hs-office-debitor-test-data.sql
@@ -0,0 +1,62 @@
+--liquibase formatted sql
+
+
+-- ============================================================================
+--changeset hs-office-debitor-TEST-DATA-GENERATOR:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates a single debitor test record.
+ */
+create or replace procedure createHsOfficeDebitorTestData(
+ withDebitorNumberSuffix numeric(5),
+ forPartnerPersonName varchar,
+ forBillingContactLabel varchar,
+ withDefaultPrefix varchar
+ )
+ language plpgsql as $$
+declare
+ currentTask varchar;
+ idName varchar;
+ relatedDebitorRelUuid uuid;
+ relatedBankAccountUuid uuid;
+begin
+ idName := cleanIdentifier( forPartnerPersonName|| '-' || forBillingContactLabel);
+ currentTask := 'creating debitor test-data ' || idName;
+ call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global:ADMIN');
+ execute format('set local hsadminng.currentTask to %L', currentTask);
+
+ select debitorRel.uuid
+ into relatedDebitorRelUuid
+ from hs_office_relation debitorRel
+ join hs_office_person person on person.uuid = debitorRel.holderUuid
+ and (person.tradeName = forPartnerPersonName or person.familyName = forPartnerPersonName)
+ where debitorRel.type = 'DEBITOR';
+
+ select b.uuid
+ into relatedBankAccountUuid
+ from hs_office_bankaccount b
+ where b.holder = forPartnerPersonName;
+
+ raise notice 'creating test debitor: % (#%)', idName, withDebitorNumberSuffix;
+ -- raise exception 'creating test debitor: (uuid=%, debitorRelUuid=%, debitornumbersuffix=%, billable=%, vatbusiness=%, vatreversecharge=%, refundbankaccountuuid=%, defaultprefix=%)',
+ -- uuid_generate_v4(), relatedDebitorRelUuid, withDebitorNumberSuffix, true, true, false, relatedBankAccountUuid, withDefaultPrefix;
+ insert
+ into hs_office_debitor (uuid, debitorRelUuid, debitornumbersuffix, billable, vatbusiness, vatreversecharge, refundbankaccountuuid, defaultprefix)
+ values (uuid_generate_v4(), relatedDebitorRelUuid, withDebitorNumberSuffix, true, true, false, relatedBankAccountUuid, withDefaultPrefix);
+end; $$;
+--//
+
+
+-- ============================================================================
+--changeset hs-office-debitor-TEST-DATA-GENERATION:1 –context=dev,tc endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+do language plpgsql $$
+ begin
+ call createHsOfficeDebitorTestData(11, 'First GmbH', 'first contact', 'fir');
+ call createHsOfficeDebitorTestData(12, 'Second e.K.', 'second contact', 'sec');
+ call createHsOfficeDebitorTestData(13, 'Third OHG', 'third contact', 'thi');
+ end;
+$$;
+--//
diff --git a/src/main/resources/db/changelog/250-hs-office-sepamandate.sql b/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5070-hs-office-sepamandate.sql
similarity index 100%
rename from src/main/resources/db/changelog/250-hs-office-sepamandate.sql
rename to src/main/resources/db/changelog/5-hs-office/507-sepamandate/5070-hs-office-sepamandate.sql
diff --git a/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5073-hs-office-sepamandate-rbac.md b/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5073-hs-office-sepamandate-rbac.md
new file mode 100644
index 00000000..aa3059f9
--- /dev/null
+++ b/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5073-hs-office-sepamandate-rbac.md
@@ -0,0 +1,141 @@
+### rbac sepaMandate
+
+This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually.
+
+```mermaid
+%%{init:{'flowchart':{'htmlLabels':false}}}%%
+flowchart TB
+
+subgraph bankAccount["`**bankAccount**`"]
+ direction TB
+ style bankAccount fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph bankAccount:roles[ ]
+ style bankAccount:roles fill:#99bcdb,stroke:white
+
+ role:bankAccount:OWNER[[bankAccount:OWNER]]
+ role:bankAccount:ADMIN[[bankAccount:ADMIN]]
+ role:bankAccount:REFERRER[[bankAccount:REFERRER]]
+ end
+end
+
+subgraph debitorRel.contact["`**debitorRel.contact**`"]
+ direction TB
+ style debitorRel.contact fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph debitorRel.contact:roles[ ]
+ style debitorRel.contact:roles fill:#99bcdb,stroke:white
+
+ role:debitorRel.contact:OWNER[[debitorRel.contact:OWNER]]
+ role:debitorRel.contact:ADMIN[[debitorRel.contact:ADMIN]]
+ role:debitorRel.contact:REFERRER[[debitorRel.contact:REFERRER]]
+ end
+end
+
+subgraph debitorRel.anchorPerson["`**debitorRel.anchorPerson**`"]
+ direction TB
+ style debitorRel.anchorPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph debitorRel.anchorPerson:roles[ ]
+ style debitorRel.anchorPerson:roles fill:#99bcdb,stroke:white
+
+ role:debitorRel.anchorPerson:OWNER[[debitorRel.anchorPerson:OWNER]]
+ role:debitorRel.anchorPerson:ADMIN[[debitorRel.anchorPerson:ADMIN]]
+ role:debitorRel.anchorPerson:REFERRER[[debitorRel.anchorPerson:REFERRER]]
+ end
+end
+
+subgraph debitorRel.holderPerson["`**debitorRel.holderPerson**`"]
+ direction TB
+ style debitorRel.holderPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph debitorRel.holderPerson:roles[ ]
+ style debitorRel.holderPerson:roles fill:#99bcdb,stroke:white
+
+ role:debitorRel.holderPerson:OWNER[[debitorRel.holderPerson:OWNER]]
+ role:debitorRel.holderPerson:ADMIN[[debitorRel.holderPerson:ADMIN]]
+ role:debitorRel.holderPerson:REFERRER[[debitorRel.holderPerson:REFERRER]]
+ end
+end
+
+subgraph sepaMandate["`**sepaMandate**`"]
+ direction TB
+ style sepaMandate fill:#dd4901,stroke:#274d6e,stroke-width:8px
+
+ subgraph sepaMandate:roles[ ]
+ style sepaMandate:roles fill:#dd4901,stroke:white
+
+ role:sepaMandate:OWNER[[sepaMandate:OWNER]]
+ role:sepaMandate:ADMIN[[sepaMandate:ADMIN]]
+ role:sepaMandate:AGENT[[sepaMandate:AGENT]]
+ role:sepaMandate:REFERRER[[sepaMandate:REFERRER]]
+ end
+
+ subgraph sepaMandate:permissions[ ]
+ style sepaMandate:permissions fill:#dd4901,stroke:white
+
+ perm:sepaMandate:DELETE{{sepaMandate:DELETE}}
+ perm:sepaMandate:UPDATE{{sepaMandate:UPDATE}}
+ perm:sepaMandate:SELECT{{sepaMandate:SELECT}}
+ perm:sepaMandate:INSERT{{sepaMandate:INSERT}}
+ end
+end
+
+subgraph debitorRel["`**debitorRel**`"]
+ direction TB
+ style debitorRel fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph debitorRel:roles[ ]
+ style debitorRel:roles fill:#99bcdb,stroke:white
+
+ role:debitorRel:OWNER[[debitorRel:OWNER]]
+ role:debitorRel:ADMIN[[debitorRel:ADMIN]]
+ role:debitorRel:AGENT[[debitorRel:AGENT]]
+ role:debitorRel:TENANT[[debitorRel:TENANT]]
+ end
+end
+
+%% granting roles to users
+user:creator ==> role:sepaMandate:OWNER
+
+%% granting roles to roles
+role:global:ADMIN -.-> role:debitorRel.anchorPerson:OWNER
+role:debitorRel.anchorPerson:OWNER -.-> role:debitorRel.anchorPerson:ADMIN
+role:debitorRel.anchorPerson:ADMIN -.-> role:debitorRel.anchorPerson:REFERRER
+role:global:ADMIN -.-> role:debitorRel.holderPerson:OWNER
+role:debitorRel.holderPerson:OWNER -.-> role:debitorRel.holderPerson:ADMIN
+role:debitorRel.holderPerson:ADMIN -.-> role:debitorRel.holderPerson:REFERRER
+role:global:ADMIN -.-> role:debitorRel.contact:OWNER
+role:debitorRel.contact:OWNER -.-> role:debitorRel.contact:ADMIN
+role:debitorRel.contact:ADMIN -.-> role:debitorRel.contact:REFERRER
+role:global:ADMIN -.-> role:debitorRel:OWNER
+role:debitorRel:OWNER -.-> role:debitorRel:ADMIN
+role:debitorRel.anchorPerson:ADMIN -.-> role:debitorRel:ADMIN
+role:debitorRel:ADMIN -.-> role:debitorRel:AGENT
+role:debitorRel.holderPerson:ADMIN -.-> role:debitorRel:AGENT
+role:debitorRel:AGENT -.-> role:debitorRel:TENANT
+role:debitorRel.holderPerson:ADMIN -.-> role:debitorRel:TENANT
+role:debitorRel.contact:ADMIN -.-> role:debitorRel:TENANT
+role:debitorRel:TENANT -.-> role:debitorRel.anchorPerson:REFERRER
+role:debitorRel:TENANT -.-> role:debitorRel.holderPerson:REFERRER
+role:debitorRel:TENANT -.-> role:debitorRel.contact:REFERRER
+role:global:ADMIN -.-> role:bankAccount:OWNER
+role:bankAccount:OWNER -.-> role:bankAccount:ADMIN
+role:bankAccount:ADMIN -.-> role:bankAccount:REFERRER
+role:global:ADMIN ==> role:sepaMandate:OWNER
+role:sepaMandate:OWNER ==> role:sepaMandate:ADMIN
+role:sepaMandate:ADMIN ==> role:sepaMandate:AGENT
+role:sepaMandate:AGENT ==> role:bankAccount:REFERRER
+role:sepaMandate:AGENT ==> role:debitorRel:AGENT
+role:sepaMandate:AGENT ==> role:sepaMandate:REFERRER
+role:bankAccount:ADMIN ==> role:sepaMandate:REFERRER
+role:debitorRel:AGENT ==> role:sepaMandate:REFERRER
+role:sepaMandate:REFERRER ==> role:debitorRel:TENANT
+
+%% granting permissions to roles
+role:sepaMandate:OWNER ==> perm:sepaMandate:DELETE
+role:sepaMandate:ADMIN ==> perm:sepaMandate:UPDATE
+role:sepaMandate:REFERRER ==> perm:sepaMandate:SELECT
+role:debitorRel:ADMIN ==> perm:sepaMandate:INSERT
+
+```
diff --git a/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5073-hs-office-sepamandate-rbac.sql b/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5073-hs-office-sepamandate-rbac.sql
new file mode 100644
index 00000000..9f126a22
--- /dev/null
+++ b/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5073-hs-office-sepamandate-rbac.sql
@@ -0,0 +1,207 @@
+--liquibase formatted sql
+-- This code generated was by RbacViewPostgresGenerator, do not amend manually.
+
+
+-- ============================================================================
+--changeset hs-office-sepamandate-rbac-OBJECT:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRelatedRbacObject('hs_office_sepamandate');
+--//
+
+
+-- ============================================================================
+--changeset hs-office-sepamandate-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRbacRoleDescriptors('hsOfficeSepaMandate', 'hs_office_sepamandate');
+--//
+
+
+-- ============================================================================
+--changeset hs-office-sepamandate-rbac-insert-trigger:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates the roles, grants and permission for the AFTER INSERT TRIGGER.
+ */
+
+create or replace procedure buildRbacSystemForHsOfficeSepaMandate(
+ NEW hs_office_sepamandate
+)
+ language plpgsql as $$
+
+declare
+ newBankAccount hs_office_bankaccount;
+ newDebitorRel hs_office_relation;
+
+begin
+ call enterTriggerForObjectUuid(NEW.uuid);
+
+ SELECT * FROM hs_office_bankaccount WHERE uuid = NEW.bankAccountUuid INTO newBankAccount;
+ assert newBankAccount.uuid is not null, format('newBankAccount must not be null for NEW.bankAccountUuid = %s', NEW.bankAccountUuid);
+
+ SELECT debitorRel.*
+ FROM hs_office_relation debitorRel
+ JOIN hs_office_debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid
+ WHERE debitor.uuid = NEW.debitorUuid
+ INTO newDebitorRel;
+ assert newDebitorRel.uuid is not null, format('newDebitorRel must not be null for NEW.debitorUuid = %s', NEW.debitorUuid);
+
+
+ perform createRoleWithGrants(
+ hsOfficeSepaMandateOWNER(NEW),
+ permissions => array['DELETE'],
+ incomingSuperRoles => array[globalADMIN()],
+ userUuids => array[currentUserUuid()]
+ );
+
+ perform createRoleWithGrants(
+ hsOfficeSepaMandateADMIN(NEW),
+ permissions => array['UPDATE'],
+ incomingSuperRoles => array[hsOfficeSepaMandateOWNER(NEW)]
+ );
+
+ perform createRoleWithGrants(
+ hsOfficeSepaMandateAGENT(NEW),
+ incomingSuperRoles => array[hsOfficeSepaMandateADMIN(NEW)],
+ outgoingSubRoles => array[
+ hsOfficeBankAccountREFERRER(newBankAccount),
+ hsOfficeRelationAGENT(newDebitorRel)]
+ );
+
+ perform createRoleWithGrants(
+ hsOfficeSepaMandateREFERRER(NEW),
+ permissions => array['SELECT'],
+ incomingSuperRoles => array[
+ hsOfficeBankAccountADMIN(newBankAccount),
+ hsOfficeRelationAGENT(newDebitorRel),
+ hsOfficeSepaMandateAGENT(NEW)],
+ outgoingSubRoles => array[hsOfficeRelationTENANT(newDebitorRel)]
+ );
+
+ call leaveTriggerForObjectUuid(NEW.uuid);
+end; $$;
+
+/*
+ AFTER INSERT TRIGGER to create the role+grant structure for a new hs_office_sepamandate row.
+ */
+
+create or replace function insertTriggerForHsOfficeSepaMandate_tf()
+ returns trigger
+ language plpgsql
+ strict as $$
+begin
+ call buildRbacSystemForHsOfficeSepaMandate(NEW);
+ return NEW;
+end; $$;
+
+create trigger insertTriggerForHsOfficeSepaMandate_tg
+ after insert on hs_office_sepamandate
+ for each row
+execute procedure insertTriggerForHsOfficeSepaMandate_tf();
+--//
+
+
+-- ============================================================================
+--changeset hs-office-sepamandate-rbac-INSERT:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates INSERT INTO hs_office_sepamandate permissions for the related hs_office_relation rows.
+ */
+do language plpgsql $$
+ declare
+ row hs_office_relation;
+ begin
+ call defineContext('create INSERT INTO hs_office_sepamandate permissions for the related hs_office_relation rows');
+
+ FOR row IN SELECT * FROM hs_office_relation
+ LOOP
+ call grantPermissionToRole(
+ createPermission(row.uuid, 'INSERT', 'hs_office_sepamandate'),
+ hsOfficeRelationADMIN(row));
+ END LOOP;
+ END;
+$$;
+
+/**
+ Adds hs_office_sepamandate INSERT permission to specified role of new hs_office_relation rows.
+*/
+create or replace function hs_office_sepamandate_hs_office_relation_insert_tf()
+ returns trigger
+ language plpgsql
+ strict as $$
+begin
+ call grantPermissionToRole(
+ createPermission(NEW.uuid, 'INSERT', 'hs_office_sepamandate'),
+ hsOfficeRelationADMIN(NEW));
+ return NEW;
+end; $$;
+
+-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
+create trigger z_hs_office_sepamandate_hs_office_relation_insert_tg
+ after insert on hs_office_relation
+ for each row
+execute procedure hs_office_sepamandate_hs_office_relation_insert_tf();
+
+/**
+ Checks if the user or assumed roles are allowed to insert a row to hs_office_sepamandate,
+ where the check is performed by an indirect role.
+
+ An indirect role is a role which depends on an object uuid which is not a direct foreign key
+ of the source entity, but needs to be fetched via joined tables.
+*/
+create or replace function hs_office_sepamandate_insert_permission_check_tf()
+ returns trigger
+ language plpgsql as $$
+
+declare
+ superRoleObjectUuid uuid;
+
+begin
+ superRoleObjectUuid := (SELECT debitorRel.uuid
+ FROM hs_office_relation debitorRel
+ JOIN hs_office_debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid
+ WHERE debitor.uuid = NEW.debitorUuid
+ );
+ assert superRoleObjectUuid is not null, 'superRoleObjectUuid must not be null';
+
+ if ( not hasInsertPermission(superRoleObjectUuid, 'INSERT', 'hs_office_sepamandate') ) then
+ raise exception
+ '[403] insert into hs_office_sepamandate not allowed for current subjects % (%)',
+ currentSubjects(), currentSubjectsUuids();
+ end if;
+ return NEW;
+end; $$;
+
+create trigger hs_office_sepamandate_insert_permission_check_tg
+ before insert on hs_office_sepamandate
+ for each row
+ execute procedure hs_office_sepamandate_insert_permission_check_tf();
+--//
+
+-- ============================================================================
+--changeset hs-office-sepamandate-rbac-IDENTITY-VIEW:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+ call generateRbacIdentityViewFromQuery('hs_office_sepamandate',
+ $idName$
+ select sm.uuid as uuid, ba.iban || '-' || sm.validity as idName
+ from hs_office_sepamandate sm
+ join hs_office_bankaccount ba on ba.uuid = sm.bankAccountUuid
+ $idName$);
+--//
+
+-- ============================================================================
+--changeset hs-office-sepamandate-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRbacRestrictedView('hs_office_sepamandate',
+ $orderBy$
+ validity
+ $orderBy$,
+ $updates$
+ reference = new.reference,
+ agreement = new.agreement,
+ validity = new.validity
+ $updates$);
+--//
+
diff --git a/src/main/resources/db/changelog/256-hs-office-sepamandate-migration.sql b/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5076-hs-office-sepamandate-migration.sql
similarity index 100%
rename from src/main/resources/db/changelog/256-hs-office-sepamandate-migration.sql
rename to src/main/resources/db/changelog/5-hs-office/507-sepamandate/5076-hs-office-sepamandate-migration.sql
diff --git a/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5078-hs-office-sepamandate-test-data.sql b/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5078-hs-office-sepamandate-test-data.sql
new file mode 100644
index 00000000..e664d8c5
--- /dev/null
+++ b/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5078-hs-office-sepamandate-test-data.sql
@@ -0,0 +1,56 @@
+--liquibase formatted sql
+
+
+-- ============================================================================
+--changeset hs-office-sepaMandate-TEST-DATA-GENERATOR:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates a single sepaMandate test record.
+ */
+create or replace procedure createHsOfficeSepaMandateTestData(
+ forPartnerNumber numeric(5),
+ forDebitorSuffix char(2),
+ forIban varchar,
+ withReference varchar)
+ language plpgsql as $$
+declare
+ currentTask varchar;
+ relatedDebitor hs_office_debitor;
+ relatedBankAccount hs_office_bankAccount;
+begin
+ currentTask := 'creating SEPA-mandate test-data ' || forPartnerNumber::text || forDebitorSuffix::text;
+ call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global:ADMIN');
+ execute format('set local hsadminng.currentTask to %L', currentTask);
+
+ select debitor.* into relatedDebitor
+ from hs_office_debitor debitor
+ join hs_office_relation debitorRel on debitorRel.uuid = debitor.debitorRelUuid
+ join hs_office_relation partnerRel on partnerRel.holderUuid = debitorRel.anchorUuid
+ join hs_office_partner partner on partner.partnerRelUuid = partnerRel.uuid
+ where partner.partnerNumber = forPartnerNumber and debitor.debitorNumberSuffix = forDebitorSuffix;
+ select b.* into relatedBankAccount
+ from hs_office_bankAccount b where b.iban = forIban;
+
+ raise notice 'creating test SEPA-mandate: %', forPartnerNumber::text || forDebitorSuffix::text;
+ raise notice '- using debitor (%): %', relatedDebitor.uuid, relatedDebitor;
+ raise notice '- using bankAccount (%): %', relatedBankAccount.uuid, relatedBankAccount;
+ insert
+ into hs_office_sepamandate (uuid, debitoruuid, bankAccountuuid, reference, agreement, validity)
+ values (uuid_generate_v4(), relatedDebitor.uuid, relatedBankAccount.uuid, withReference, '20220930', daterange('20221001' , '20261231', '[]'));
+end; $$;
+--//
+
+
+-- ============================================================================
+--changeset hs-office-sepaMandate-TEST-DATA-GENERATION:1 –context=dev,tc endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+do language plpgsql $$
+ begin
+ call createHsOfficeSepaMandateTestData(10001, '11', 'DE02120300000000202051', 'ref-10001-11');
+ call createHsOfficeSepaMandateTestData(10002, '12', 'DE02100500000054540402', 'ref-10002-12');
+ call createHsOfficeSepaMandateTestData(10003, '13', 'DE02300209000106531065', 'ref-10003-13');
+ end;
+$$;
+--//
diff --git a/src/main/resources/db/changelog/300-hs-office-membership.sql b/src/main/resources/db/changelog/5-hs-office/510-membership/5100-hs-office-membership.sql
similarity index 86%
rename from src/main/resources/db/changelog/300-hs-office-membership.sql
rename to src/main/resources/db/changelog/5-hs-office/510-membership/5100-hs-office-membership.sql
index acc0651a..28ec1249 100644
--- a/src/main/resources/db/changelog/300-hs-office-membership.sql
+++ b/src/main/resources/db/changelog/5-hs-office/510-membership/5100-hs-office-membership.sql
@@ -12,9 +12,7 @@ create table if not exists hs_office_membership
(
uuid uuid unique references RbacObject (uuid) initially deferred,
partnerUuid uuid not null references hs_office_partner(uuid),
- mainDebitorUuid uuid not null references hs_office_debitor(uuid),
- memberNumberSuffix char(2) not null check (
- memberNumberSuffix::text ~ '^[0-9][0-9]$'),
+ memberNumberSuffix char(2) not null check (memberNumberSuffix::text ~ '^[0-9][0-9]$'),
validity daterange not null,
reasonForTermination HsOfficeReasonForTermination not null default 'NONE',
membershipFeeBillable boolean not null default true,
diff --git a/src/main/resources/db/changelog/5-hs-office/510-membership/5103-hs-office-membership-rbac.md b/src/main/resources/db/changelog/5-hs-office/510-membership/5103-hs-office-membership-rbac.md
new file mode 100644
index 00000000..3681b8e6
--- /dev/null
+++ b/src/main/resources/db/changelog/5-hs-office/510-membership/5103-hs-office-membership-rbac.md
@@ -0,0 +1,120 @@
+### rbac membership
+
+This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually.
+
+```mermaid
+%%{init:{'flowchart':{'htmlLabels':false}}}%%
+flowchart TB
+
+subgraph partnerRel["`**partnerRel**`"]
+ direction TB
+ style partnerRel fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph partnerRel:roles[ ]
+ style partnerRel:roles fill:#99bcdb,stroke:white
+
+ role:partnerRel:OWNER[[partnerRel:OWNER]]
+ role:partnerRel:ADMIN[[partnerRel:ADMIN]]
+ role:partnerRel:AGENT[[partnerRel:AGENT]]
+ role:partnerRel:TENANT[[partnerRel:TENANT]]
+ end
+end
+
+subgraph partnerRel.contact["`**partnerRel.contact**`"]
+ direction TB
+ style partnerRel.contact fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph partnerRel.contact:roles[ ]
+ style partnerRel.contact:roles fill:#99bcdb,stroke:white
+
+ role:partnerRel.contact:OWNER[[partnerRel.contact:OWNER]]
+ role:partnerRel.contact:ADMIN[[partnerRel.contact:ADMIN]]
+ role:partnerRel.contact:REFERRER[[partnerRel.contact:REFERRER]]
+ end
+end
+
+subgraph membership["`**membership**`"]
+ direction TB
+ style membership fill:#dd4901,stroke:#274d6e,stroke-width:8px
+
+ subgraph membership:roles[ ]
+ style membership:roles fill:#dd4901,stroke:white
+
+ role:membership:OWNER[[membership:OWNER]]
+ role:membership:ADMIN[[membership:ADMIN]]
+ role:membership:AGENT[[membership:AGENT]]
+ end
+
+ subgraph membership:permissions[ ]
+ style membership:permissions fill:#dd4901,stroke:white
+
+ perm:membership:INSERT{{membership:INSERT}}
+ perm:membership:DELETE{{membership:DELETE}}
+ perm:membership:UPDATE{{membership:UPDATE}}
+ perm:membership:SELECT{{membership:SELECT}}
+ end
+end
+
+subgraph partnerRel.anchorPerson["`**partnerRel.anchorPerson**`"]
+ direction TB
+ style partnerRel.anchorPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph partnerRel.anchorPerson:roles[ ]
+ style partnerRel.anchorPerson:roles fill:#99bcdb,stroke:white
+
+ role:partnerRel.anchorPerson:OWNER[[partnerRel.anchorPerson:OWNER]]
+ role:partnerRel.anchorPerson:ADMIN[[partnerRel.anchorPerson:ADMIN]]
+ role:partnerRel.anchorPerson:REFERRER[[partnerRel.anchorPerson:REFERRER]]
+ end
+end
+
+subgraph partnerRel.holderPerson["`**partnerRel.holderPerson**`"]
+ direction TB
+ style partnerRel.holderPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph partnerRel.holderPerson:roles[ ]
+ style partnerRel.holderPerson:roles fill:#99bcdb,stroke:white
+
+ role:partnerRel.holderPerson:OWNER[[partnerRel.holderPerson:OWNER]]
+ role:partnerRel.holderPerson:ADMIN[[partnerRel.holderPerson:ADMIN]]
+ role:partnerRel.holderPerson:REFERRER[[partnerRel.holderPerson:REFERRER]]
+ end
+end
+
+%% granting roles to users
+user:creator ==> role:membership:OWNER
+
+%% granting roles to roles
+role:global:ADMIN -.-> role:partnerRel.anchorPerson:OWNER
+role:partnerRel.anchorPerson:OWNER -.-> role:partnerRel.anchorPerson:ADMIN
+role:partnerRel.anchorPerson:ADMIN -.-> role:partnerRel.anchorPerson:REFERRER
+role:global:ADMIN -.-> role:partnerRel.holderPerson:OWNER
+role:partnerRel.holderPerson:OWNER -.-> role:partnerRel.holderPerson:ADMIN
+role:partnerRel.holderPerson:ADMIN -.-> role:partnerRel.holderPerson:REFERRER
+role:global:ADMIN -.-> role:partnerRel.contact:OWNER
+role:partnerRel.contact:OWNER -.-> role:partnerRel.contact:ADMIN
+role:partnerRel.contact:ADMIN -.-> role:partnerRel.contact:REFERRER
+role:global:ADMIN -.-> role:partnerRel:OWNER
+role:partnerRel:OWNER -.-> role:partnerRel:ADMIN
+role:partnerRel.anchorPerson:ADMIN -.-> role:partnerRel:ADMIN
+role:partnerRel:ADMIN -.-> role:partnerRel:AGENT
+role:partnerRel.holderPerson:ADMIN -.-> role:partnerRel:AGENT
+role:partnerRel:AGENT -.-> role:partnerRel:TENANT
+role:partnerRel.holderPerson:ADMIN -.-> role:partnerRel:TENANT
+role:partnerRel.contact:ADMIN -.-> role:partnerRel:TENANT
+role:partnerRel:TENANT -.-> role:partnerRel.anchorPerson:REFERRER
+role:partnerRel:TENANT -.-> role:partnerRel.holderPerson:REFERRER
+role:partnerRel:TENANT -.-> role:partnerRel.contact:REFERRER
+role:membership:OWNER ==> role:membership:ADMIN
+role:partnerRel:ADMIN ==> role:membership:ADMIN
+role:membership:ADMIN ==> role:membership:AGENT
+role:partnerRel:AGENT ==> role:membership:AGENT
+role:membership:AGENT ==> role:partnerRel:TENANT
+
+%% granting permissions to roles
+role:global:ADMIN ==> perm:membership:INSERT
+role:membership:ADMIN ==> perm:membership:DELETE
+role:membership:ADMIN ==> perm:membership:UPDATE
+role:membership:AGENT ==> perm:membership:SELECT
+
+```
diff --git a/src/main/resources/db/changelog/5-hs-office/510-membership/5103-hs-office-membership-rbac.sql b/src/main/resources/db/changelog/5-hs-office/510-membership/5103-hs-office-membership-rbac.sql
new file mode 100644
index 00000000..7f8de66b
--- /dev/null
+++ b/src/main/resources/db/changelog/5-hs-office/510-membership/5103-hs-office-membership-rbac.sql
@@ -0,0 +1,178 @@
+--liquibase formatted sql
+-- This code generated was by RbacViewPostgresGenerator, do not amend manually.
+
+
+-- ============================================================================
+--changeset hs-office-membership-rbac-OBJECT:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRelatedRbacObject('hs_office_membership');
+--//
+
+
+-- ============================================================================
+--changeset hs-office-membership-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRbacRoleDescriptors('hsOfficeMembership', 'hs_office_membership');
+--//
+
+
+-- ============================================================================
+--changeset hs-office-membership-rbac-insert-trigger:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates the roles, grants and permission for the AFTER INSERT TRIGGER.
+ */
+
+create or replace procedure buildRbacSystemForHsOfficeMembership(
+ NEW hs_office_membership
+)
+ language plpgsql as $$
+
+declare
+ newPartnerRel hs_office_relation;
+
+begin
+ call enterTriggerForObjectUuid(NEW.uuid);
+
+ SELECT partnerRel.*
+ FROM hs_office_partner AS partner
+ JOIN hs_office_relation AS partnerRel ON partnerRel.uuid = partner.partnerRelUuid
+ WHERE partner.uuid = NEW.partnerUuid
+ INTO newPartnerRel;
+ assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.partnerUuid = %s', NEW.partnerUuid);
+
+
+ perform createRoleWithGrants(
+ hsOfficeMembershipOWNER(NEW),
+ userUuids => array[currentUserUuid()]
+ );
+
+ perform createRoleWithGrants(
+ hsOfficeMembershipADMIN(NEW),
+ permissions => array['DELETE', 'UPDATE'],
+ incomingSuperRoles => array[
+ hsOfficeMembershipOWNER(NEW),
+ hsOfficeRelationADMIN(newPartnerRel)]
+ );
+
+ perform createRoleWithGrants(
+ hsOfficeMembershipAGENT(NEW),
+ permissions => array['SELECT'],
+ incomingSuperRoles => array[
+ hsOfficeMembershipADMIN(NEW),
+ hsOfficeRelationAGENT(newPartnerRel)],
+ outgoingSubRoles => array[hsOfficeRelationTENANT(newPartnerRel)]
+ );
+
+ call leaveTriggerForObjectUuid(NEW.uuid);
+end; $$;
+
+/*
+ AFTER INSERT TRIGGER to create the role+grant structure for a new hs_office_membership row.
+ */
+
+create or replace function insertTriggerForHsOfficeMembership_tf()
+ returns trigger
+ language plpgsql
+ strict as $$
+begin
+ call buildRbacSystemForHsOfficeMembership(NEW);
+ return NEW;
+end; $$;
+
+create trigger insertTriggerForHsOfficeMembership_tg
+ after insert on hs_office_membership
+ for each row
+execute procedure insertTriggerForHsOfficeMembership_tf();
+--//
+
+
+-- ============================================================================
+--changeset hs-office-membership-rbac-INSERT:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates INSERT INTO hs_office_membership permissions for the related global rows.
+ */
+do language plpgsql $$
+ declare
+ row global;
+ begin
+ call defineContext('create INSERT INTO hs_office_membership permissions for the related global rows');
+
+ FOR row IN SELECT * FROM global
+ LOOP
+ call grantPermissionToRole(
+ createPermission(row.uuid, 'INSERT', 'hs_office_membership'),
+ globalADMIN());
+ END LOOP;
+ END;
+$$;
+
+/**
+ Adds hs_office_membership INSERT permission to specified role of new global rows.
+*/
+create or replace function hs_office_membership_global_insert_tf()
+ returns trigger
+ language plpgsql
+ strict as $$
+begin
+ call grantPermissionToRole(
+ createPermission(NEW.uuid, 'INSERT', 'hs_office_membership'),
+ globalADMIN());
+ return NEW;
+end; $$;
+
+-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
+create trigger z_hs_office_membership_global_insert_tg
+ after insert on global
+ for each row
+execute procedure hs_office_membership_global_insert_tf();
+
+/**
+ Checks if the user or assumed roles are allowed to insert a row to hs_office_membership,
+ where only global-admin has that permission.
+*/
+create or replace function hs_office_membership_insert_permission_missing_tf()
+ returns trigger
+ language plpgsql as $$
+begin
+ raise exception '[403] insert into hs_office_membership not allowed for current subjects % (%)',
+ currentSubjects(), currentSubjectsUuids();
+end; $$;
+
+create trigger hs_office_membership_insert_permission_check_tg
+ before insert on hs_office_membership
+ for each row
+ when ( not isGlobalAdmin() )
+ execute procedure hs_office_membership_insert_permission_missing_tf();
+--//
+
+-- ============================================================================
+--changeset hs-office-membership-rbac-IDENTITY-VIEW:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+ call generateRbacIdentityViewFromQuery('hs_office_membership',
+ $idName$
+ SELECT m.uuid AS uuid,
+ 'M-' || p.partnerNumber || m.memberNumberSuffix as idName
+ FROM hs_office_membership AS m
+ JOIN hs_office_partner AS p ON p.uuid = m.partnerUuid
+ $idName$);
+--//
+
+-- ============================================================================
+--changeset hs-office-membership-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRbacRestrictedView('hs_office_membership',
+ $orderBy$
+ validity
+ $orderBy$,
+ $updates$
+ validity = new.validity,
+ membershipFeeBillable = new.membershipFeeBillable,
+ reasonForTermination = new.reasonForTermination
+ $updates$);
+--//
+
diff --git a/src/main/resources/db/changelog/5-hs-office/510-membership/5108-hs-office-membership-test-data.sql b/src/main/resources/db/changelog/5-hs-office/510-membership/5108-hs-office-membership-test-data.sql
new file mode 100644
index 00000000..d49a5344
--- /dev/null
+++ b/src/main/resources/db/changelog/5-hs-office/510-membership/5108-hs-office-membership-test-data.sql
@@ -0,0 +1,48 @@
+--liquibase formatted sql
+
+
+-- ============================================================================
+--changeset hs-office-membership-TEST-DATA-GENERATOR:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates a single membership test record.
+ */
+create or replace procedure createHsOfficeMembershipTestData(
+ forPartnerNumber numeric(5),
+ newMemberNumberSuffix char(2) )
+ language plpgsql as $$
+declare
+ currentTask varchar;
+ relatedPartner hs_office_partner;
+begin
+ currentTask := 'creating Membership test-data ' ||
+ 'P-' || forPartnerNumber::text ||
+ 'M-...' || newMemberNumberSuffix;
+ call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global:ADMIN');
+ execute format('set local hsadminng.currentTask to %L', currentTask);
+
+ select partner.* from hs_office_partner partner
+ where partner.partnerNumber = forPartnerNumber into relatedPartner;
+
+ raise notice 'creating test Membership: M-% %', forPartnerNumber, newMemberNumberSuffix;
+ raise notice '- using partner (%): %', relatedPartner.uuid, relatedPartner;
+ insert
+ into hs_office_membership (uuid, partneruuid, memberNumberSuffix, validity, reasonfortermination)
+ values (uuid_generate_v4(), relatedPartner.uuid, newMemberNumberSuffix, daterange('20221001' , null, '[]'), 'NONE');
+end; $$;
+--//
+
+
+-- ============================================================================
+--changeset hs-office-membership-TEST-DATA-GENERATION:1 –context=dev,tc endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+do language plpgsql $$
+ begin
+ call createHsOfficeMembershipTestData(10001, '01');
+ call createHsOfficeMembershipTestData(10002, '02');
+ call createHsOfficeMembershipTestData(10003, '03');
+ end;
+$$;
+--//
diff --git a/src/main/resources/db/changelog/310-hs-office-coopshares.sql b/src/main/resources/db/changelog/5-hs-office/511-coopshares/5110-hs-office-coopshares.sql
similarity index 100%
rename from src/main/resources/db/changelog/310-hs-office-coopshares.sql
rename to src/main/resources/db/changelog/5-hs-office/511-coopshares/5110-hs-office-coopshares.sql
diff --git a/src/main/resources/db/changelog/5-hs-office/511-coopshares/5113-hs-office-coopshares-rbac.md b/src/main/resources/db/changelog/5-hs-office/511-coopshares/5113-hs-office-coopshares-rbac.md
new file mode 100644
index 00000000..26ff3d5c
--- /dev/null
+++ b/src/main/resources/db/changelog/5-hs-office/511-coopshares/5113-hs-office-coopshares-rbac.md
@@ -0,0 +1,120 @@
+### rbac coopSharesTransaction
+
+This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually.
+
+```mermaid
+%%{init:{'flowchart':{'htmlLabels':false}}}%%
+flowchart TB
+
+subgraph membership.partnerRel.holderPerson["`**membership.partnerRel.holderPerson**`"]
+ direction TB
+ style membership.partnerRel.holderPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph membership.partnerRel.holderPerson:roles[ ]
+ style membership.partnerRel.holderPerson:roles fill:#99bcdb,stroke:white
+
+ role:membership.partnerRel.holderPerson:OWNER[[membership.partnerRel.holderPerson:OWNER]]
+ role:membership.partnerRel.holderPerson:ADMIN[[membership.partnerRel.holderPerson:ADMIN]]
+ role:membership.partnerRel.holderPerson:REFERRER[[membership.partnerRel.holderPerson:REFERRER]]
+ end
+end
+
+subgraph membership.partnerRel.anchorPerson["`**membership.partnerRel.anchorPerson**`"]
+ direction TB
+ style membership.partnerRel.anchorPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph membership.partnerRel.anchorPerson:roles[ ]
+ style membership.partnerRel.anchorPerson:roles fill:#99bcdb,stroke:white
+
+ role:membership.partnerRel.anchorPerson:OWNER[[membership.partnerRel.anchorPerson:OWNER]]
+ role:membership.partnerRel.anchorPerson:ADMIN[[membership.partnerRel.anchorPerson:ADMIN]]
+ role:membership.partnerRel.anchorPerson:REFERRER[[membership.partnerRel.anchorPerson:REFERRER]]
+ end
+end
+
+subgraph coopSharesTransaction["`**coopSharesTransaction**`"]
+ direction TB
+ style coopSharesTransaction fill:#dd4901,stroke:#274d6e,stroke-width:8px
+
+ subgraph coopSharesTransaction:permissions[ ]
+ style coopSharesTransaction:permissions fill:#dd4901,stroke:white
+
+ perm:coopSharesTransaction:INSERT{{coopSharesTransaction:INSERT}}
+ perm:coopSharesTransaction:UPDATE{{coopSharesTransaction:UPDATE}}
+ perm:coopSharesTransaction:SELECT{{coopSharesTransaction:SELECT}}
+ end
+end
+
+subgraph membership["`**membership**`"]
+ direction TB
+ style membership fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph membership:roles[ ]
+ style membership:roles fill:#99bcdb,stroke:white
+
+ role:membership:OWNER[[membership:OWNER]]
+ role:membership:ADMIN[[membership:ADMIN]]
+ role:membership:AGENT[[membership:AGENT]]
+ end
+end
+
+subgraph membership.partnerRel["`**membership.partnerRel**`"]
+ direction TB
+ style membership.partnerRel fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph membership.partnerRel:roles[ ]
+ style membership.partnerRel:roles fill:#99bcdb,stroke:white
+
+ role:membership.partnerRel:OWNER[[membership.partnerRel:OWNER]]
+ role:membership.partnerRel:ADMIN[[membership.partnerRel:ADMIN]]
+ role:membership.partnerRel:AGENT[[membership.partnerRel:AGENT]]
+ role:membership.partnerRel:TENANT[[membership.partnerRel:TENANT]]
+ end
+end
+
+subgraph membership.partnerRel.contact["`**membership.partnerRel.contact**`"]
+ direction TB
+ style membership.partnerRel.contact fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph membership.partnerRel.contact:roles[ ]
+ style membership.partnerRel.contact:roles fill:#99bcdb,stroke:white
+
+ role:membership.partnerRel.contact:OWNER[[membership.partnerRel.contact:OWNER]]
+ role:membership.partnerRel.contact:ADMIN[[membership.partnerRel.contact:ADMIN]]
+ role:membership.partnerRel.contact:REFERRER[[membership.partnerRel.contact:REFERRER]]
+ end
+end
+
+%% granting roles to roles
+role:global:ADMIN -.-> role:membership.partnerRel.anchorPerson:OWNER
+role:membership.partnerRel.anchorPerson:OWNER -.-> role:membership.partnerRel.anchorPerson:ADMIN
+role:membership.partnerRel.anchorPerson:ADMIN -.-> role:membership.partnerRel.anchorPerson:REFERRER
+role:global:ADMIN -.-> role:membership.partnerRel.holderPerson:OWNER
+role:membership.partnerRel.holderPerson:OWNER -.-> role:membership.partnerRel.holderPerson:ADMIN
+role:membership.partnerRel.holderPerson:ADMIN -.-> role:membership.partnerRel.holderPerson:REFERRER
+role:global:ADMIN -.-> role:membership.partnerRel.contact:OWNER
+role:membership.partnerRel.contact:OWNER -.-> role:membership.partnerRel.contact:ADMIN
+role:membership.partnerRel.contact:ADMIN -.-> role:membership.partnerRel.contact:REFERRER
+role:global:ADMIN -.-> role:membership.partnerRel:OWNER
+role:membership.partnerRel:OWNER -.-> role:membership.partnerRel:ADMIN
+role:membership.partnerRel.anchorPerson:ADMIN -.-> role:membership.partnerRel:ADMIN
+role:membership.partnerRel:ADMIN -.-> role:membership.partnerRel:AGENT
+role:membership.partnerRel.holderPerson:ADMIN -.-> role:membership.partnerRel:AGENT
+role:membership.partnerRel:AGENT -.-> role:membership.partnerRel:TENANT
+role:membership.partnerRel.holderPerson:ADMIN -.-> role:membership.partnerRel:TENANT
+role:membership.partnerRel.contact:ADMIN -.-> role:membership.partnerRel:TENANT
+role:membership.partnerRel:TENANT -.-> role:membership.partnerRel.anchorPerson:REFERRER
+role:membership.partnerRel:TENANT -.-> role:membership.partnerRel.holderPerson:REFERRER
+role:membership.partnerRel:TENANT -.-> role:membership.partnerRel.contact:REFERRER
+role:membership:OWNER -.-> role:membership:ADMIN
+role:membership.partnerRel:ADMIN -.-> role:membership:ADMIN
+role:membership:ADMIN -.-> role:membership:AGENT
+role:membership.partnerRel:AGENT -.-> role:membership:AGENT
+role:membership:AGENT -.-> role:membership.partnerRel:TENANT
+
+%% granting permissions to roles
+role:membership:ADMIN ==> perm:coopSharesTransaction:INSERT
+role:membership:ADMIN ==> perm:coopSharesTransaction:UPDATE
+role:membership:AGENT ==> perm:coopSharesTransaction:SELECT
+
+```
diff --git a/src/main/resources/db/changelog/5-hs-office/511-coopshares/5113-hs-office-coopshares-rbac.sql b/src/main/resources/db/changelog/5-hs-office/511-coopshares/5113-hs-office-coopshares-rbac.sql
new file mode 100644
index 00000000..f4856f0a
--- /dev/null
+++ b/src/main/resources/db/changelog/5-hs-office/511-coopshares/5113-hs-office-coopshares-rbac.sql
@@ -0,0 +1,151 @@
+--liquibase formatted sql
+-- This code generated was by RbacViewPostgresGenerator, do not amend manually.
+
+
+-- ============================================================================
+--changeset hs-office-coopsharestransaction-rbac-OBJECT:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRelatedRbacObject('hs_office_coopsharestransaction');
+--//
+
+
+-- ============================================================================
+--changeset hs-office-coopsharestransaction-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRbacRoleDescriptors('hsOfficeCoopSharesTransaction', 'hs_office_coopsharestransaction');
+--//
+
+
+-- ============================================================================
+--changeset hs-office-coopsharestransaction-rbac-insert-trigger:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates the roles, grants and permission for the AFTER INSERT TRIGGER.
+ */
+
+create or replace procedure buildRbacSystemForHsOfficeCoopSharesTransaction(
+ NEW hs_office_coopsharestransaction
+)
+ language plpgsql as $$
+
+declare
+ newMembership hs_office_membership;
+
+begin
+ call enterTriggerForObjectUuid(NEW.uuid);
+
+ SELECT * FROM hs_office_membership WHERE uuid = NEW.membershipUuid INTO newMembership;
+ assert newMembership.uuid is not null, format('newMembership must not be null for NEW.membershipUuid = %s', NEW.membershipUuid);
+
+ call grantPermissionToRole(createPermission(NEW.uuid, 'SELECT'), hsOfficeMembershipAGENT(newMembership));
+ call grantPermissionToRole(createPermission(NEW.uuid, 'UPDATE'), hsOfficeMembershipADMIN(newMembership));
+
+ call leaveTriggerForObjectUuid(NEW.uuid);
+end; $$;
+
+/*
+ AFTER INSERT TRIGGER to create the role+grant structure for a new hs_office_coopsharestransaction row.
+ */
+
+create or replace function insertTriggerForHsOfficeCoopSharesTransaction_tf()
+ returns trigger
+ language plpgsql
+ strict as $$
+begin
+ call buildRbacSystemForHsOfficeCoopSharesTransaction(NEW);
+ return NEW;
+end; $$;
+
+create trigger insertTriggerForHsOfficeCoopSharesTransaction_tg
+ after insert on hs_office_coopsharestransaction
+ for each row
+execute procedure insertTriggerForHsOfficeCoopSharesTransaction_tf();
+--//
+
+
+-- ============================================================================
+--changeset hs-office-coopsharestransaction-rbac-INSERT:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates INSERT INTO hs_office_coopsharestransaction permissions for the related hs_office_membership rows.
+ */
+do language plpgsql $$
+ declare
+ row hs_office_membership;
+ begin
+ call defineContext('create INSERT INTO hs_office_coopsharestransaction permissions for the related hs_office_membership rows');
+
+ FOR row IN SELECT * FROM hs_office_membership
+ LOOP
+ call grantPermissionToRole(
+ createPermission(row.uuid, 'INSERT', 'hs_office_coopsharestransaction'),
+ hsOfficeMembershipADMIN(row));
+ END LOOP;
+ END;
+$$;
+
+/**
+ Adds hs_office_coopsharestransaction INSERT permission to specified role of new hs_office_membership rows.
+*/
+create or replace function hs_office_coopsharestransaction_hs_office_membership_insert_tf()
+ returns trigger
+ language plpgsql
+ strict as $$
+begin
+ call grantPermissionToRole(
+ createPermission(NEW.uuid, 'INSERT', 'hs_office_coopsharestransaction'),
+ hsOfficeMembershipADMIN(NEW));
+ return NEW;
+end; $$;
+
+-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
+create trigger z_hs_office_coopsharestransaction_hs_office_membership_insert_tg
+ after insert on hs_office_membership
+ for each row
+execute procedure hs_office_coopsharestransaction_hs_office_membership_insert_tf();
+
+/**
+ Checks if the user or assumed roles are allowed to insert a row to hs_office_coopsharestransaction,
+ where the check is performed by a direct role.
+
+ A direct role is a role depending on a foreign key directly available in the NEW row.
+*/
+create or replace function hs_office_coopsharestransaction_insert_permission_missing_tf()
+ returns trigger
+ language plpgsql as $$
+begin
+ raise exception '[403] insert into hs_office_coopsharestransaction not allowed for current subjects % (%)',
+ currentSubjects(), currentSubjectsUuids();
+end; $$;
+
+create trigger hs_office_coopsharestransaction_insert_permission_check_tg
+ before insert on hs_office_coopsharestransaction
+ for each row
+ when ( not hasInsertPermission(NEW.membershipUuid, 'INSERT', 'hs_office_coopsharestransaction') )
+ execute procedure hs_office_coopsharestransaction_insert_permission_missing_tf();
+--//
+
+-- ============================================================================
+--changeset hs-office-coopsharestransaction-rbac-IDENTITY-VIEW:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+call generateRbacIdentityViewFromProjection('hs_office_coopsharestransaction',
+ $idName$
+ reference
+ $idName$);
+--//
+
+-- ============================================================================
+--changeset hs-office-coopsharestransaction-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRbacRestrictedView('hs_office_coopsharestransaction',
+ $orderBy$
+ reference
+ $orderBy$,
+ $updates$
+ comment = new.comment
+ $updates$);
+--//
+
diff --git a/src/main/resources/db/changelog/316-hs-office-coopshares-migration.sql b/src/main/resources/db/changelog/5-hs-office/511-coopshares/5116-hs-office-coopshares-migration.sql
similarity index 100%
rename from src/main/resources/db/changelog/316-hs-office-coopshares-migration.sql
rename to src/main/resources/db/changelog/5-hs-office/511-coopshares/5116-hs-office-coopshares-migration.sql
diff --git a/src/main/resources/db/changelog/318-hs-office-coopshares-test-data.sql b/src/main/resources/db/changelog/5-hs-office/511-coopshares/5118-hs-office-coopshares-test-data.sql
similarity index 100%
rename from src/main/resources/db/changelog/318-hs-office-coopshares-test-data.sql
rename to src/main/resources/db/changelog/5-hs-office/511-coopshares/5118-hs-office-coopshares-test-data.sql
diff --git a/src/main/resources/db/changelog/320-hs-office-coopassets.sql b/src/main/resources/db/changelog/5-hs-office/512-coopassets/5120-hs-office-coopassets.sql
similarity index 100%
rename from src/main/resources/db/changelog/320-hs-office-coopassets.sql
rename to src/main/resources/db/changelog/5-hs-office/512-coopassets/5120-hs-office-coopassets.sql
diff --git a/src/main/resources/db/changelog/5-hs-office/512-coopassets/5123-hs-office-coopassets-rbac.md b/src/main/resources/db/changelog/5-hs-office/512-coopassets/5123-hs-office-coopassets-rbac.md
new file mode 100644
index 00000000..d220a38c
--- /dev/null
+++ b/src/main/resources/db/changelog/5-hs-office/512-coopassets/5123-hs-office-coopassets-rbac.md
@@ -0,0 +1,120 @@
+### rbac coopAssetsTransaction
+
+This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually.
+
+```mermaid
+%%{init:{'flowchart':{'htmlLabels':false}}}%%
+flowchart TB
+
+subgraph membership.partnerRel.holderPerson["`**membership.partnerRel.holderPerson**`"]
+ direction TB
+ style membership.partnerRel.holderPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph membership.partnerRel.holderPerson:roles[ ]
+ style membership.partnerRel.holderPerson:roles fill:#99bcdb,stroke:white
+
+ role:membership.partnerRel.holderPerson:OWNER[[membership.partnerRel.holderPerson:OWNER]]
+ role:membership.partnerRel.holderPerson:ADMIN[[membership.partnerRel.holderPerson:ADMIN]]
+ role:membership.partnerRel.holderPerson:REFERRER[[membership.partnerRel.holderPerson:REFERRER]]
+ end
+end
+
+subgraph membership.partnerRel.anchorPerson["`**membership.partnerRel.anchorPerson**`"]
+ direction TB
+ style membership.partnerRel.anchorPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph membership.partnerRel.anchorPerson:roles[ ]
+ style membership.partnerRel.anchorPerson:roles fill:#99bcdb,stroke:white
+
+ role:membership.partnerRel.anchorPerson:OWNER[[membership.partnerRel.anchorPerson:OWNER]]
+ role:membership.partnerRel.anchorPerson:ADMIN[[membership.partnerRel.anchorPerson:ADMIN]]
+ role:membership.partnerRel.anchorPerson:REFERRER[[membership.partnerRel.anchorPerson:REFERRER]]
+ end
+end
+
+subgraph coopAssetsTransaction["`**coopAssetsTransaction**`"]
+ direction TB
+ style coopAssetsTransaction fill:#dd4901,stroke:#274d6e,stroke-width:8px
+
+ subgraph coopAssetsTransaction:permissions[ ]
+ style coopAssetsTransaction:permissions fill:#dd4901,stroke:white
+
+ perm:coopAssetsTransaction:INSERT{{coopAssetsTransaction:INSERT}}
+ perm:coopAssetsTransaction:UPDATE{{coopAssetsTransaction:UPDATE}}
+ perm:coopAssetsTransaction:SELECT{{coopAssetsTransaction:SELECT}}
+ end
+end
+
+subgraph membership["`**membership**`"]
+ direction TB
+ style membership fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph membership:roles[ ]
+ style membership:roles fill:#99bcdb,stroke:white
+
+ role:membership:OWNER[[membership:OWNER]]
+ role:membership:ADMIN[[membership:ADMIN]]
+ role:membership:AGENT[[membership:AGENT]]
+ end
+end
+
+subgraph membership.partnerRel["`**membership.partnerRel**`"]
+ direction TB
+ style membership.partnerRel fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph membership.partnerRel:roles[ ]
+ style membership.partnerRel:roles fill:#99bcdb,stroke:white
+
+ role:membership.partnerRel:OWNER[[membership.partnerRel:OWNER]]
+ role:membership.partnerRel:ADMIN[[membership.partnerRel:ADMIN]]
+ role:membership.partnerRel:AGENT[[membership.partnerRel:AGENT]]
+ role:membership.partnerRel:TENANT[[membership.partnerRel:TENANT]]
+ end
+end
+
+subgraph membership.partnerRel.contact["`**membership.partnerRel.contact**`"]
+ direction TB
+ style membership.partnerRel.contact fill:#99bcdb,stroke:#274d6e,stroke-width:8px
+
+ subgraph membership.partnerRel.contact:roles[ ]
+ style membership.partnerRel.contact:roles fill:#99bcdb,stroke:white
+
+ role:membership.partnerRel.contact:OWNER[[membership.partnerRel.contact:OWNER]]
+ role:membership.partnerRel.contact:ADMIN[[membership.partnerRel.contact:ADMIN]]
+ role:membership.partnerRel.contact:REFERRER[[membership.partnerRel.contact:REFERRER]]
+ end
+end
+
+%% granting roles to roles
+role:global:ADMIN -.-> role:membership.partnerRel.anchorPerson:OWNER
+role:membership.partnerRel.anchorPerson:OWNER -.-> role:membership.partnerRel.anchorPerson:ADMIN
+role:membership.partnerRel.anchorPerson:ADMIN -.-> role:membership.partnerRel.anchorPerson:REFERRER
+role:global:ADMIN -.-> role:membership.partnerRel.holderPerson:OWNER
+role:membership.partnerRel.holderPerson:OWNER -.-> role:membership.partnerRel.holderPerson:ADMIN
+role:membership.partnerRel.holderPerson:ADMIN -.-> role:membership.partnerRel.holderPerson:REFERRER
+role:global:ADMIN -.-> role:membership.partnerRel.contact:OWNER
+role:membership.partnerRel.contact:OWNER -.-> role:membership.partnerRel.contact:ADMIN
+role:membership.partnerRel.contact:ADMIN -.-> role:membership.partnerRel.contact:REFERRER
+role:global:ADMIN -.-> role:membership.partnerRel:OWNER
+role:membership.partnerRel:OWNER -.-> role:membership.partnerRel:ADMIN
+role:membership.partnerRel.anchorPerson:ADMIN -.-> role:membership.partnerRel:ADMIN
+role:membership.partnerRel:ADMIN -.-> role:membership.partnerRel:AGENT
+role:membership.partnerRel.holderPerson:ADMIN -.-> role:membership.partnerRel:AGENT
+role:membership.partnerRel:AGENT -.-> role:membership.partnerRel:TENANT
+role:membership.partnerRel.holderPerson:ADMIN -.-> role:membership.partnerRel:TENANT
+role:membership.partnerRel.contact:ADMIN -.-> role:membership.partnerRel:TENANT
+role:membership.partnerRel:TENANT -.-> role:membership.partnerRel.anchorPerson:REFERRER
+role:membership.partnerRel:TENANT -.-> role:membership.partnerRel.holderPerson:REFERRER
+role:membership.partnerRel:TENANT -.-> role:membership.partnerRel.contact:REFERRER
+role:membership:OWNER -.-> role:membership:ADMIN
+role:membership.partnerRel:ADMIN -.-> role:membership:ADMIN
+role:membership:ADMIN -.-> role:membership:AGENT
+role:membership.partnerRel:AGENT -.-> role:membership:AGENT
+role:membership:AGENT -.-> role:membership.partnerRel:TENANT
+
+%% granting permissions to roles
+role:membership:ADMIN ==> perm:coopAssetsTransaction:INSERT
+role:membership:ADMIN ==> perm:coopAssetsTransaction:UPDATE
+role:membership:AGENT ==> perm:coopAssetsTransaction:SELECT
+
+```
diff --git a/src/main/resources/db/changelog/5-hs-office/512-coopassets/5123-hs-office-coopassets-rbac.sql b/src/main/resources/db/changelog/5-hs-office/512-coopassets/5123-hs-office-coopassets-rbac.sql
new file mode 100644
index 00000000..df1fdd3b
--- /dev/null
+++ b/src/main/resources/db/changelog/5-hs-office/512-coopassets/5123-hs-office-coopassets-rbac.sql
@@ -0,0 +1,151 @@
+--liquibase formatted sql
+-- This code generated was by RbacViewPostgresGenerator, do not amend manually.
+
+
+-- ============================================================================
+--changeset hs-office-coopassetstransaction-rbac-OBJECT:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRelatedRbacObject('hs_office_coopassetstransaction');
+--//
+
+
+-- ============================================================================
+--changeset hs-office-coopassetstransaction-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRbacRoleDescriptors('hsOfficeCoopAssetsTransaction', 'hs_office_coopassetstransaction');
+--//
+
+
+-- ============================================================================
+--changeset hs-office-coopassetstransaction-rbac-insert-trigger:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates the roles, grants and permission for the AFTER INSERT TRIGGER.
+ */
+
+create or replace procedure buildRbacSystemForHsOfficeCoopAssetsTransaction(
+ NEW hs_office_coopassetstransaction
+)
+ language plpgsql as $$
+
+declare
+ newMembership hs_office_membership;
+
+begin
+ call enterTriggerForObjectUuid(NEW.uuid);
+
+ SELECT * FROM hs_office_membership WHERE uuid = NEW.membershipUuid INTO newMembership;
+ assert newMembership.uuid is not null, format('newMembership must not be null for NEW.membershipUuid = %s', NEW.membershipUuid);
+
+ call grantPermissionToRole(createPermission(NEW.uuid, 'SELECT'), hsOfficeMembershipAGENT(newMembership));
+ call grantPermissionToRole(createPermission(NEW.uuid, 'UPDATE'), hsOfficeMembershipADMIN(newMembership));
+
+ call leaveTriggerForObjectUuid(NEW.uuid);
+end; $$;
+
+/*
+ AFTER INSERT TRIGGER to create the role+grant structure for a new hs_office_coopassetstransaction row.
+ */
+
+create or replace function insertTriggerForHsOfficeCoopAssetsTransaction_tf()
+ returns trigger
+ language plpgsql
+ strict as $$
+begin
+ call buildRbacSystemForHsOfficeCoopAssetsTransaction(NEW);
+ return NEW;
+end; $$;
+
+create trigger insertTriggerForHsOfficeCoopAssetsTransaction_tg
+ after insert on hs_office_coopassetstransaction
+ for each row
+execute procedure insertTriggerForHsOfficeCoopAssetsTransaction_tf();
+--//
+
+
+-- ============================================================================
+--changeset hs-office-coopassetstransaction-rbac-INSERT:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+/*
+ Creates INSERT INTO hs_office_coopassetstransaction permissions for the related hs_office_membership rows.
+ */
+do language plpgsql $$
+ declare
+ row hs_office_membership;
+ begin
+ call defineContext('create INSERT INTO hs_office_coopassetstransaction permissions for the related hs_office_membership rows');
+
+ FOR row IN SELECT * FROM hs_office_membership
+ LOOP
+ call grantPermissionToRole(
+ createPermission(row.uuid, 'INSERT', 'hs_office_coopassetstransaction'),
+ hsOfficeMembershipADMIN(row));
+ END LOOP;
+ END;
+$$;
+
+/**
+ Adds hs_office_coopassetstransaction INSERT permission to specified role of new hs_office_membership rows.
+*/
+create or replace function hs_office_coopassetstransaction_hs_office_membership_insert_tf()
+ returns trigger
+ language plpgsql
+ strict as $$
+begin
+ call grantPermissionToRole(
+ createPermission(NEW.uuid, 'INSERT', 'hs_office_coopassetstransaction'),
+ hsOfficeMembershipADMIN(NEW));
+ return NEW;
+end; $$;
+
+-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
+create trigger z_hs_office_coopassetstransaction_hs_office_membership_insert_tg
+ after insert on hs_office_membership
+ for each row
+execute procedure hs_office_coopassetstransaction_hs_office_membership_insert_tf();
+
+/**
+ Checks if the user or assumed roles are allowed to insert a row to hs_office_coopassetstransaction,
+ where the check is performed by a direct role.
+
+ A direct role is a role depending on a foreign key directly available in the NEW row.
+*/
+create or replace function hs_office_coopassetstransaction_insert_permission_missing_tf()
+ returns trigger
+ language plpgsql as $$
+begin
+ raise exception '[403] insert into hs_office_coopassetstransaction not allowed for current subjects % (%)',
+ currentSubjects(), currentSubjectsUuids();
+end; $$;
+
+create trigger hs_office_coopassetstransaction_insert_permission_check_tg
+ before insert on hs_office_coopassetstransaction
+ for each row
+ when ( not hasInsertPermission(NEW.membershipUuid, 'INSERT', 'hs_office_coopassetstransaction') )
+ execute procedure hs_office_coopassetstransaction_insert_permission_missing_tf();
+--//
+
+-- ============================================================================
+--changeset hs-office-coopassetstransaction-rbac-IDENTITY-VIEW:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+call generateRbacIdentityViewFromProjection('hs_office_coopassetstransaction',
+ $idName$
+ reference
+ $idName$);
+--//
+
+-- ============================================================================
+--changeset hs-office-coopassetstransaction-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+call generateRbacRestrictedView('hs_office_coopassetstransaction',
+ $orderBy$
+ reference
+ $orderBy$,
+ $updates$
+ comment = new.comment
+ $updates$);
+--//
+
diff --git a/src/main/resources/db/changelog/326-hs-office-coopassets-migration.sql b/src/main/resources/db/changelog/5-hs-office/512-coopassets/5126-hs-office-coopassets-migration.sql
similarity index 100%
rename from src/main/resources/db/changelog/326-hs-office-coopassets-migration.sql
rename to src/main/resources/db/changelog/5-hs-office/512-coopassets/5126-hs-office-coopassets-migration.sql
diff --git a/src/main/resources/db/changelog/328-hs-office-coopassets-test-data.sql b/src/main/resources/db/changelog/5-hs-office/512-coopassets/5128-hs-office-coopassets-test-data.sql
similarity index 100%
rename from src/main/resources/db/changelog/328-hs-office-coopassets-test-data.sql
rename to src/main/resources/db/changelog/5-hs-office/512-coopassets/5128-hs-office-coopassets-test-data.sql
diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml
index 2b8417c3..11a5f956 100644
--- a/src/main/resources/db/changelog/db.changelog-master.yaml
+++ b/src/main/resources/db/changelog/db.changelog-master.yaml
@@ -1,127 +1,129 @@
databaseChangeLog:
- include:
- file: db/changelog/001-last-row-count.sql
+ file: db/changelog/0-basis/001-last-row-count.sql
- include:
- file: db/changelog/002-int-to-var.sql
+ file: db/changelog/0-basis/002-int-to-var.sql
- include:
- file: db/changelog/003-random-in-range.sql
+ file: db/changelog/0-basis/003-random-in-range.sql
- include:
- file: db/changelog/004-jsonb-changes-delta.sql
+ file: db/changelog/0-basis/004-jsonb-changes-delta.sql
- include:
- file: db/changelog/005-uuid-ossp-extension.sql
+ file: db/changelog/0-basis/005-uuid-ossp-extension.sql
- include:
- file: db/changelog/006-numeric-hash-functions.sql
+ file: db/changelog/0-basis/006-numeric-hash-functions.sql
- include:
- file: db/changelog/009-check-environment.sql
+ file: db/changelog/0-basis/007-table-columns.sql
- include:
- file: db/changelog/010-context.sql
+ file: db/changelog/0-basis/009-check-environment.sql
- include:
- file: db/changelog/020-audit-log.sql
+ file: db/changelog/0-basis/010-context.sql
- include:
- file: db/changelog/050-rbac-base.sql
+ file: db/changelog/0-basis/020-audit-log.sql
- include:
- file: db/changelog/051-rbac-user-grant.sql
+ file: db/changelog/1-rbac/1050-rbac-base.sql
- include:
- file: db/changelog/054-rbac-context.sql
+ file: db/changelog/1-rbac/1051-rbac-user-grant.sql
- include:
- file: db/changelog/055-rbac-views.sql
+ file: db/changelog/1-rbac/1054-rbac-context.sql
- include:
- file: db/changelog/056-rbac-trigger-context.sql
+ file: db/changelog/1-rbac/1055-rbac-views.sql
- include:
- file: db/changelog/057-rbac-role-builder.sql
+ file: db/changelog/1-rbac/1056-rbac-trigger-context.sql
- include:
- file: db/changelog/058-rbac-generators.sql
+ file: db/changelog/1-rbac/1057-rbac-role-builder.sql
- include:
- file: db/changelog/059-rbac-statistics.sql
+ file: db/changelog/1-rbac/1058-rbac-generators.sql
- include:
- file: db/changelog/080-rbac-global.sql
+ file: db/changelog/1-rbac/1059-rbac-statistics.sql
- include:
- file: db/changelog/110-test-customer.sql
+ file: db/changelog/1-rbac/1080-rbac-global.sql
- include:
- file: db/changelog/113-test-customer-rbac.sql
+ file: db/changelog/2-test/201-test-customer/2010-test-customer.sql
- include:
- file: db/changelog/118-test-customer-test-data.sql
+ file: db/changelog/2-test/201-test-customer/2013-test-customer-rbac.sql
- include:
- file: db/changelog/120-test-package.sql
+ file: db/changelog/2-test/201-test-customer/2018-test-customer-test-data.sql
- include:
- file: db/changelog/123-test-package-rbac.sql
+ file: db/changelog/2-test/202-test-package/2020-test-package.sql
- include:
- file: db/changelog/128-test-package-test-data.sql
+ file: db/changelog/2-test/202-test-package/2023-test-package-rbac.sql
- include:
- file: db/changelog/130-test-domain.sql
+ file: db/changelog/2-test/202-test-package/2028-test-package-test-data.sql
- include:
- file: db/changelog/133-test-domain-rbac.sql
+ file: db/changelog/2-test/203-test-domain/2030-test-domain.sql
- include:
- file: db/changelog/138-test-domain-test-data.sql
+ file: db/changelog/2-test/203-test-domain/2033-test-domain-rbac.sql
- include:
- file: db/changelog/200-hs-office-contact.sql
+ file: db/changelog/2-test/203-test-domain/2038-test-domain-test-data.sql
- include:
- file: db/changelog/203-hs-office-contact-rbac.sql
+ file: db/changelog/5-hs-office/501-contact/5010-hs-office-contact.sql
- include:
- file: db/changelog/206-hs-office-contact-migration.sql
+ file: db/changelog/5-hs-office/501-contact/5013-hs-office-contact-rbac.sql
- include:
- file: db/changelog/208-hs-office-contact-test-data.sql
+ file: db/changelog/5-hs-office/501-contact/5016-hs-office-contact-migration.sql
- include:
- file: db/changelog/210-hs-office-person.sql
+ file: db/changelog/5-hs-office/501-contact/5018-hs-office-contact-test-data.sql
- include:
- file: db/changelog/213-hs-office-person-rbac.sql
+ file: db/changelog/5-hs-office/502-person/5020-hs-office-person.sql
- include:
- file: db/changelog/218-hs-office-person-test-data.sql
+ file: db/changelog/5-hs-office/502-person/5023-hs-office-person-rbac.sql
- include:
- file: db/changelog/220-hs-office-relationship.sql
+ file: db/changelog/5-hs-office/502-person/5028-hs-office-person-test-data.sql
- include:
- file: db/changelog/223-hs-office-relationship-rbac.sql
+ file: db/changelog/5-hs-office/503-relation/5030-hs-office-relation.sql
- include:
- file: db/changelog/228-hs-office-relationship-test-data.sql
+ file: db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.sql
- include:
- file: db/changelog/230-hs-office-partner.sql
+ file: db/changelog/5-hs-office/503-relation/5038-hs-office-relation-test-data.sql
- include:
- file: db/changelog/233-hs-office-partner-rbac.sql
+ file: db/changelog/5-hs-office/504-partner/5040-hs-office-partner.sql
- include:
- file: db/changelog/234-hs-office-partner-details-rbac.sql
+ file: db/changelog/5-hs-office/504-partner/5043-hs-office-partner-rbac.sql
- include:
- file: db/changelog/236-hs-office-partner-migration.sql
+ file: db/changelog/5-hs-office/504-partner/5044-hs-office-partner-details-rbac.sql
- include:
- file: db/changelog/238-hs-office-partner-test-data.sql
+ file: db/changelog/5-hs-office/504-partner/5046-hs-office-partner-migration.sql
- include:
- file: db/changelog/240-hs-office-bankaccount.sql
+ file: db/changelog/5-hs-office/504-partner/5048-hs-office-partner-test-data.sql
- include:
- file: db/changelog/243-hs-office-bankaccount-rbac.sql
+ file: db/changelog/5-hs-office/505-bankaccount/5050-hs-office-bankaccount.sql
- include:
- file: db/changelog/248-hs-office-bankaccount-test-data.sql
+ file: db/changelog/5-hs-office/505-bankaccount/5053-hs-office-bankaccount-rbac.sql
- include:
- file: db/changelog/270-hs-office-debitor.sql
+ file: db/changelog/5-hs-office/505-bankaccount/5058-hs-office-bankaccount-test-data.sql
- include:
- file: db/changelog/273-hs-office-debitor-rbac.sql
+ file: db/changelog/5-hs-office/506-debitor/5060-hs-office-debitor.sql
- include:
- file: db/changelog/278-hs-office-debitor-test-data.sql
+ file: db/changelog/5-hs-office/506-debitor/5063-hs-office-debitor-rbac.sql
- include:
- file: db/changelog/250-hs-office-sepamandate.sql
+ file: db/changelog/5-hs-office/506-debitor/5068-hs-office-debitor-test-data.sql
- include:
- file: db/changelog/253-hs-office-sepamandate-rbac.sql
+ file: db/changelog/5-hs-office/507-sepamandate/5070-hs-office-sepamandate.sql
- include:
- file: db/changelog/256-hs-office-sepamandate-migration.sql
+ file: db/changelog/5-hs-office/507-sepamandate/5073-hs-office-sepamandate-rbac.sql
- include:
- file: db/changelog/258-hs-office-sepamandate-test-data.sql
+ file: db/changelog/5-hs-office/507-sepamandate/5076-hs-office-sepamandate-migration.sql
- include:
- file: db/changelog/300-hs-office-membership.sql
+ file: db/changelog/5-hs-office/507-sepamandate/5078-hs-office-sepamandate-test-data.sql
- include:
- file: db/changelog/303-hs-office-membership-rbac.sql
+ file: db/changelog/5-hs-office/510-membership/5100-hs-office-membership.sql
- include:
- file: db/changelog/308-hs-office-membership-test-data.sql
+ file: db/changelog/5-hs-office/510-membership/5103-hs-office-membership-rbac.sql
- include:
- file: db/changelog/310-hs-office-coopshares.sql
+ file: db/changelog/5-hs-office/510-membership/5108-hs-office-membership-test-data.sql
- include:
- file: db/changelog/313-hs-office-coopshares-rbac.sql
+ file: db/changelog/5-hs-office/511-coopshares/5110-hs-office-coopshares.sql
- include:
- file: db/changelog/316-hs-office-coopshares-migration.sql
+ file: db/changelog/5-hs-office/511-coopshares/5113-hs-office-coopshares-rbac.sql
- include:
- file: db/changelog/318-hs-office-coopshares-test-data.sql
+ file: db/changelog/5-hs-office/511-coopshares/5116-hs-office-coopshares-migration.sql
- include:
- file: db/changelog/320-hs-office-coopassets.sql
+ file: db/changelog/5-hs-office/511-coopshares/5118-hs-office-coopshares-test-data.sql
- include:
- file: db/changelog/323-hs-office-coopassets-rbac.sql
+ file: db/changelog/5-hs-office/512-coopassets/5120-hs-office-coopassets.sql
- include:
- file: db/changelog/326-hs-office-coopassets-migration.sql
+ file: db/changelog/5-hs-office/512-coopassets/5123-hs-office-coopassets-rbac.sql
- include:
- file: db/changelog/328-hs-office-coopassets-test-data.sql
+ file: db/changelog/5-hs-office/512-coopassets/5126-hs-office-coopassets-migration.sql
+ - include:
+ file: db/changelog/5-hs-office/512-coopassets/5128-hs-office-coopassets-test-data.sql
diff --git a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java
index fe50ccf1..497c60de 100644
--- a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java
@@ -28,6 +28,7 @@ public class ArchitectureTest {
"..test",
"..test.cust",
"..test.pac",
+ "..test.dom",
"..context",
"..generated..",
"..persistence..",
@@ -40,7 +41,7 @@ public class ArchitectureTest {
"..hs.office.migration",
"..hs.office.partner",
"..hs.office.person",
- "..hs.office.relationship",
+ "..hs.office.relation",
"..hs.office.sepamandate",
"..errors",
"..mapper",
@@ -49,6 +50,8 @@ public class ArchitectureTest {
"..rbac.rbacuser",
"..rbac.rbacgrant",
"..rbac.rbacrole",
+ "..rbac.rbacobject",
+ "..rbac.rbacdef",
"..stringify"
// ATTENTION: Don't simply add packages here, also add arch rules for the new package!
);
@@ -116,14 +119,18 @@ public class ArchitectureTest {
public static final ArchRule hsAdminPackagesRule = classes()
.that().resideInAPackage("..hs.office.(*)..")
.should().onlyBeAccessed().byClassesThat()
- .resideInAnyPackage("..hs.office.(*)..");
+ .resideInAnyPackage(
+ "..hs.office.(*)..",
+ "..rbac.rbacgrant" // TODO.test: just because of RbacGrantsDiagramServiceIntegrationTest
+ );
@ArchTest
@SuppressWarnings("unused")
public static final ArchRule hsOfficeBankAccountPackageRule = classes()
.that().resideInAPackage("..hs.office.bankaccount..")
.should().onlyBeAccessed().byClassesThat()
- .resideInAnyPackage("..hs.office.bankaccount..",
+ .resideInAnyPackage(
+ "..hs.office.bankaccount..",
"..hs.office.sepamandate..",
"..hs.office.debitor..",
"..hs.office.migration..");
@@ -133,7 +140,8 @@ public class ArchitectureTest {
public static final ArchRule hsOfficeSepaMandatePackageRule = classes()
.that().resideInAPackage("..hs.office.sepamandate..")
.should().onlyBeAccessed().byClassesThat()
- .resideInAnyPackage("..hs.office.sepamandate..",
+ .resideInAnyPackage(
+ "..hs.office.sepamandate..",
"..hs.office.debitor..",
"..hs.office.migration..");
@@ -142,7 +150,9 @@ public class ArchitectureTest {
public static final ArchRule hsOfficeContactPackageRule = classes()
.that().resideInAPackage("..hs.office.contact..")
.should().onlyBeAccessed().byClassesThat()
- .resideInAnyPackage("..hs.office.contact..", "..hs.office.relationship..",
+ .resideInAnyPackage(
+ "..hs.office.contact..",
+ "..hs.office.relation..",
"..hs.office.partner..",
"..hs.office.debitor..",
"..hs.office.membership..",
@@ -153,37 +163,46 @@ public class ArchitectureTest {
public static final ArchRule hsOfficePersonPackageRule = classes()
.that().resideInAPackage("..hs.office.person..")
.should().onlyBeAccessed().byClassesThat()
- .resideInAnyPackage("..hs.office.person..", "..hs.office.relationship..",
+ .resideInAnyPackage(
+ "..hs.office.person..",
+ "..hs.office.relation..",
"..hs.office.partner..",
"..hs.office.debitor..",
"..hs.office.membership..",
- "..hs.office.migration..");
+ "..hs.office.migration..")
+ .orShould().haveNameNotMatching(".*Test$");
+
@ArchTest
@SuppressWarnings("unused")
- public static final ArchRule hsOfficeRelationshipPackageRule = classes()
- .that().resideInAPackage("..hs.office.relationship..")
+ public static final ArchRule hsOfficeRelationPackageRule = classes()
+ .that().resideInAPackage("..hs.office.relation..")
.should().onlyBeAccessed().byClassesThat()
- .resideInAnyPackage("..hs.office.relationship..",
+ .resideInAnyPackage(
+ "..hs.office.relation..",
"..hs.office.partner..",
- "..hs.office.migration..");
+ "..hs.office.migration..")
+ .orShould().haveNameNotMatching(".*Test$");
@ArchTest
@SuppressWarnings("unused")
public static final ArchRule hsOfficePartnerPackageRule = classes()
.that().resideInAPackage("..hs.office.partner..")
.should().onlyBeAccessed().byClassesThat()
- .resideInAnyPackage("..hs.office.partner..",
+ .resideInAnyPackage(
+ "..hs.office.partner..",
"..hs.office.debitor..",
"..hs.office.membership..",
- "..hs.office.migration..");
+ "..hs.office.migration..")
+ .orShould().haveNameNotMatching(".*Test$");
@ArchTest
@SuppressWarnings("unused")
public static final ArchRule hsOfficeMembershipPackageRule = classes()
.that().resideInAPackage("..hs.office.membership..")
.should().onlyBeAccessed().byClassesThat()
- .resideInAnyPackage("..hs.office.membership..",
+ .resideInAnyPackage(
+ "..hs.office.membership..",
"..hs.office.coopassets..",
"..hs.office.coopshares..",
"..hs.office.migration..");
diff --git a/src/test/java/net/hostsharing/hsadminng/context/ContextBasedTest.java b/src/test/java/net/hostsharing/hsadminng/context/ContextBasedTest.java
index 1069fa5f..7f08f044 100644
--- a/src/test/java/net/hostsharing/hsadminng/context/ContextBasedTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/context/ContextBasedTest.java
@@ -1,14 +1,37 @@
package net.hostsharing.hsadminng.context;
+import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestInfo;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Import;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+
+@Import(RbacGrantsDiagramService.class)
public abstract class ContextBasedTest {
@Autowired
protected Context context;
+ @PersistenceContext
+ protected EntityManager em; // just to be used in subclasses
+
+ /**
+ * To generate a flowchart diagram from the database use something like this in a defined context:
+
+