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): - *


-     *      return (UUID[]) em.createNativeQuery("select currentSubjectsUuids() as uuids", UUID[].class).getSingleResult();
-     * 
- *

- * - *

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 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 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 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 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 entityClass, + final Column dependsOnColum) { + importEntityAliasImpl(aliasName, entityClass, directlyFetchedByDependsOnColumn(), dependsOnColum, false, null); + return this; + } + + private EntityAlias importEntityAliasImpl( + final String aliasName, final Class 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 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 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 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 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: + +
+     RbacGrantsDiagramService.writeToFile(
+         "title",
+         diagramService.allGrantsToCurrentUser(of(RbacGrantsDiagramService.Include.USERS, RbacGrantsDiagramService.Include.TEST_ENTITIES, RbacGrantsDiagramService.Include.NOT_ASSUMED, RbacGrantsDiagramService.Include.DETAILS, RbacGrantsDiagramService.Include.PERMISSIONS)),
+         "filename.md
+     );
+    
+ */ + @Autowired + protected RbacGrantsDiagramService diagramService; // just to be used in subclasses + TestInfo test; @BeforeEach diff --git a/src/test/java/net/hostsharing/hsadminng/context/ContextIntegrationTests.java b/src/test/java/net/hostsharing/hsadminng/context/ContextIntegrationTests.java index c02cb944..0daa0a15 100644 --- a/src/test/java/net/hostsharing/hsadminng/context/ContextIntegrationTests.java +++ b/src/test/java/net/hostsharing/hsadminng/context/ContextIntegrationTests.java @@ -59,13 +59,13 @@ class ContextIntegrationTests { void defineWithoutCurrentUserButWithAssumedRoles() { // when final var result = jpaAttempt.transacted(() -> - context.define(null, "test_package#yyy00.admin") + context.define(null, "test_package#yyy00:ADMIN") ); // then result.assertExceptionWithRootCauseMessage( jakarta.persistence.PersistenceException.class, - "ERROR: [403] undefined has no permission to assume role test_package#yyy00.admin"); + "ERROR: [403] undefined has no permission to assume role test_package#yyy00:ADMIN"); } @Test @@ -85,7 +85,7 @@ class ContextIntegrationTests { @Transactional void defineWithCurrentUserAndAssumedRoles() { // given - context.define("superuser-alex@hostsharing.net", "test_customer#xxx.owner;test_customer#yyy.owner"); + context.define("superuser-alex@hostsharing.net", "test_customer#xxx:OWNER;test_customer#yyy:OWNER"); // when final var currentUser = context.getCurrentUser(); @@ -93,7 +93,7 @@ class ContextIntegrationTests { // then assertThat(context.getAssumedRoles()) - .isEqualTo(Array.of("test_customer#xxx.owner", "test_customer#yyy.owner")); + .isEqualTo(Array.of("test_customer#xxx:OWNER", "test_customer#yyy:OWNER")); assertThat(context.currentSubjectsUuids()).hasSize(2); } @@ -101,12 +101,12 @@ class ContextIntegrationTests { public void defineContextWithCurrentUserAndAssumeInaccessibleRole() { // when final var result = jpaAttempt.transacted(() -> - context.define("customer-admin@xxx.example.com", "test_package#yyy00.admin") + context.define("customer-admin@xxx.example.com", "test_package#yyy00:ADMIN") ); // then result.assertExceptionWithRootCauseMessage( jakarta.persistence.PersistenceException.class, - "ERROR: [403] user customer-admin@xxx.example.com has no permission to assume role test_package#yyy00.admin"); + "ERROR: [403] user customer-admin@xxx.example.com has no permission to assume role test_package#yyy00:ADMIN"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/context/ContextUnitTest.java b/src/test/java/net/hostsharing/hsadminng/context/ContextUnitTest.java index af78c76a..2104f297 100644 --- a/src/test/java/net/hostsharing/hsadminng/context/ContextUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/context/ContextUnitTest.java @@ -27,12 +27,12 @@ import static org.mockito.Mockito.verify; class ContextUnitTest { private static final String DEFINE_CONTEXT_QUERY_STRING = """ - call defineContext( - cast(:currentTask as varchar), - cast(:currentRequest as varchar), - cast(:currentUser as varchar), - cast(:assumedRoles as varchar)); - """; + call defineContext( + cast(:currentTask as varchar(127)), + cast(:currentRequest as text), + cast(:currentUser as varchar(63)), + cast(:assumedRoles as varchar(1023))); + """; @Nested class WithoutHttpRequest { @@ -71,7 +71,7 @@ class ContextUnitTest { context.define("current-user"); verify(em).createNativeQuery(DEFINE_CONTEXT_QUERY_STRING); - verify(nativeQuery).setParameter("currentRequest", ""); + verify(nativeQuery).setParameter("currentRequest", null); } } @@ -142,8 +142,8 @@ class ContextUnitTest { } @Test - void shortensCurrentTaskTo96Chars() throws IOException { - givenRequest("GET", "http://localhost:9999/api/endpoint/" + "0123456789".repeat(10), + void shortensCurrentTaskToMaxLength() throws IOException { + givenRequest("GET", "http://localhost:9999/api/endpoint/" + "0123456789".repeat(13), Map.ofEntries( Map.entry("current-user", "given-user"), Map.entry("content-type", "application/json"), @@ -153,26 +153,7 @@ class ContextUnitTest { context.define("current-user"); verify(em).createNativeQuery(DEFINE_CONTEXT_QUERY_STRING); - verify(nativeQuery).setParameter(eq("currentTask"), argThat((String t) -> t.length() == 96)); - } - - @Test - void shortensCurrentRequestTo512Chars() throws IOException { - givenRequest("GET", "http://localhost:9999/api/endpoint", - Map.ofEntries( - Map.entry("current-user", "given-user"), - Map.entry("content-type", "application/json"), - Map.entry("user-agent", "given-user-agent")), - """ - { - "dummy": "%s" - } - """.formatted("0123456789".repeat(60))); - - context.define("current-user"); - - verify(em).createNativeQuery(DEFINE_CONTEXT_QUERY_STRING); - verify(nativeQuery).setParameter(eq("currentRequest"), argThat((String t) -> t.length() == 512)); + verify(nativeQuery).setParameter(eq("currentTask"), argThat((String t) -> t.length() == 127)); } private void givenRequest(final String method, final String url, final Map headers, final String body) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountEntityUnitTest.java index 9fea3b5e..acd6c8f3 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountEntityUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountEntityUnitTest.java @@ -19,7 +19,7 @@ class HsOfficeBankAccountEntityUnitTest { .iban("DE02370502990000684712") .bic("COKSDE33") .build(); - assertThat("" + givenBankAccount).isEqualTo("bankAccount(holder='given holder', iban='DE02370502990000684712', bic='COKSDE33')"); + assertThat(givenBankAccount.toString()).isEqualTo("bankAccount(DE02370502990000684712: holder='given holder', bic='COKSDE33')"); } @Test diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java index f2847290..f0541813 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java @@ -102,23 +102,21 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTestWithC final var roles = rawRoleRepo.findAll(); assertThat(distinctRoleNamesOf(roles)).containsExactlyInAnyOrder(Array.from( initialRoleNames, - "hs_office_bankaccount#sometempaccC.owner", - "hs_office_bankaccount#sometempaccC.admin", - "hs_office_bankaccount#sometempaccC.tenant", - "hs_office_bankaccount#sometempaccC.guest" + "hs_office_bankaccount#DE25500105176934832579:OWNER", + "hs_office_bankaccount#DE25500105176934832579:ADMIN", + "hs_office_bankaccount#DE25500105176934832579:REFERRER" )); assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(Array.fromFormatted( initialGrantNames, - "{ grant perm delete on hs_office_bankaccount#sometempaccC to role hs_office_bankaccount#sometempaccC.owner by system and assume }", - "{ grant role hs_office_bankaccount#sometempaccC.owner to role global#global.admin by system and assume }", - "{ grant role hs_office_bankaccount#sometempaccC.owner to user selfregistered-user-drew@hostsharing.org by global#global.admin and assume }", + "{ grant perm:hs_office_bankaccount#DE25500105176934832579:DELETE to role:hs_office_bankaccount#DE25500105176934832579:OWNER by system and assume }", + "{ grant role:hs_office_bankaccount#DE25500105176934832579:OWNER to role:global#global:ADMIN by system and assume }", + "{ grant role:hs_office_bankaccount#DE25500105176934832579:OWNER to user:selfregistered-user-drew@hostsharing.org by hs_office_bankaccount#DE25500105176934832579:OWNER and assume }", - "{ grant role hs_office_bankaccount#sometempaccC.admin to role hs_office_bankaccount#sometempaccC.owner by system and assume }", + "{ grant role:hs_office_bankaccount#DE25500105176934832579:ADMIN to role:hs_office_bankaccount#DE25500105176934832579:OWNER by system and assume }", + "{ grant perm:hs_office_bankaccount#DE25500105176934832579:UPDATE to role:hs_office_bankaccount#DE25500105176934832579:ADMIN by system and assume }", - "{ grant role hs_office_bankaccount#sometempaccC.tenant to role hs_office_bankaccount#sometempaccC.admin by system and assume }", - - "{ grant perm view on hs_office_bankaccount#sometempaccC to role hs_office_bankaccount#sometempaccC.guest by system and assume }", - "{ grant role hs_office_bankaccount#sometempaccC.guest to role hs_office_bankaccount#sometempaccC.tenant by system and assume }", + "{ grant perm:hs_office_bankaccount#DE25500105176934832579:SELECT to role:hs_office_bankaccount#DE25500105176934832579:REFERRER by system and assume }", + "{ grant role:hs_office_bankaccount#DE25500105176934832579:REFERRER to role:hs_office_bankaccount#DE25500105176934832579:ADMIN by system and assume }", null )); } @@ -241,10 +239,6 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTestWithC final var initialRoleNames = distinctRoleNamesOf(rawRoleRepo.findAll()); final var initialGrantNames = distinctGrantDisplaysOf(rawGrantRepo.findAll()); final var givenBankAccount = givenSomeTemporaryBankAccount("selfregistered-user-drew@hostsharing.org"); - assertThat(distinctRoleNamesOf(rawRoleRepo.findAll()).size()).as("unexpected number of roles created") - .isEqualTo(initialRoleNames.size() + 4); - assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll()).size()).as("unexpected number of grants created") - .isEqualTo(initialGrantNames.size() + 7); // when final var result = jpaAttempt.transacted(() -> { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRepositoryIntegrationTest.java index a78b761e..3187a4f4 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRepositoryIntegrationTest.java @@ -103,21 +103,20 @@ class HsOfficeContactRepositoryIntegrationTest extends ContextBasedTestWithClean final var roles = rawRoleRepo.findAll(); assertThat(distinctRoleNamesOf(roles)).containsExactlyInAnyOrder(Array.from( initialRoleNames, - "hs_office_contact#anothernewcontact.owner", - "hs_office_contact#anothernewcontact.admin", - "hs_office_contact#anothernewcontact.tenant", - "hs_office_contact#anothernewcontact.guest" + "hs_office_contact#anothernewcontact:OWNER", + "hs_office_contact#anothernewcontact:ADMIN", + "hs_office_contact#anothernewcontact:REFERRER" )); - assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(Array.from( + assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(Array.fromFormatted( initialGrantNames, - "{ grant role hs_office_contact#anothernewcontact.owner to role global#global.admin by system and assume }", - "{ grant perm edit on hs_office_contact#anothernewcontact to role hs_office_contact#anothernewcontact.admin by system and assume }", - "{ grant role hs_office_contact#anothernewcontact.tenant to role hs_office_contact#anothernewcontact.admin by system and assume }", - "{ grant perm * on hs_office_contact#anothernewcontact to role hs_office_contact#anothernewcontact.owner by system and assume }", - "{ grant role hs_office_contact#anothernewcontact.admin to role hs_office_contact#anothernewcontact.owner by system and assume }", - "{ grant perm view on hs_office_contact#anothernewcontact to role hs_office_contact#anothernewcontact.guest by system and assume }", - "{ grant role hs_office_contact#anothernewcontact.guest to role hs_office_contact#anothernewcontact.tenant by system and assume }", - "{ grant role hs_office_contact#anothernewcontact.owner to user selfregistered-user-drew@hostsharing.org by global#global.admin and assume }" + "{ grant role:hs_office_contact#anothernewcontact:OWNER to role:global#global:ADMIN by system and assume }", + "{ grant perm:hs_office_contact#anothernewcontact:UPDATE to role:hs_office_contact#anothernewcontact:ADMIN by system and assume }", + "{ grant role:hs_office_contact#anothernewcontact:OWNER to user:selfregistered-user-drew@hostsharing.org by hs_office_contact#anothernewcontact:OWNER and assume }", + "{ grant perm:hs_office_contact#anothernewcontact:DELETE to role:hs_office_contact#anothernewcontact:OWNER by system and assume }", + "{ grant role:hs_office_contact#anothernewcontact:ADMIN to role:hs_office_contact#anothernewcontact:OWNER by system and assume }", + + "{ grant perm:hs_office_contact#anothernewcontact:SELECT to role:hs_office_contact#anothernewcontact:REFERRER by system and assume }", + "{ grant role:hs_office_contact#anothernewcontact:REFERRER to role:hs_office_contact#anothernewcontact:ADMIN by system and assume }" )); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerAcceptanceTest.java index 04122059..2c9a811d 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionControllerAcceptanceTest.java @@ -276,7 +276,7 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased @Test @Accepts({ "CoopAssetTransaction:X(Access Control)" }) - void contactAdminUser_canGetRelatedCoopAssetTransaction() { + void partnerPersonUser_canGetRelatedCoopAssetTransaction() { context.define("superuser-alex@hostsharing.net"); final var givenCoopAssetTransactionUuid = coopAssetsTransactionRepo.findCoopAssetsTransactionByOptionalMembershipUuidAndDateRange( null, @@ -285,7 +285,7 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased RestAssured // @formatter:off .given() - .header("current-user", "contact-admin@firstcontact.example.com") + .header("current-user", "person-FirstGmbH@example.com") .port(port) .when() .get("http://localhost/api/hs/office/coopassetstransactions/" + givenCoopAssetTransactionUuid) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntityUnitTest.java index d93aa90f..82ba35e3 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntityUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionEntityUnitTest.java @@ -23,27 +23,27 @@ class HsOfficeCoopAssetsTransactionEntityUnitTest { void toStringContainsAlmostAllPropertiesAccount() { final var result = givenCoopAssetTransaction.toString(); - assertThat(result).isEqualTo("CoopAssetsTransaction(1000101, 2020-01-01, DEPOSIT, 128.00, some-ref)"); + assertThat(result).isEqualTo("CoopAssetsTransaction(M-1000101: 2020-01-01, DEPOSIT, 128.00, some-ref)"); } @Test void toShortStringContainsOnlyMemberNumberSuffixAndSharesCountOnly() { final var result = givenCoopAssetTransaction.toShortString(); - assertThat(result).isEqualTo("1000101+128.00"); + assertThat(result).isEqualTo("M-1000101:+128.00"); } @Test void toStringWithEmptyTransactionDoesNotThrowException() { final var result = givenEmptyCoopAssetsTransaction.toString(); - assertThat(result).isEqualTo("CoopAssetsTransaction()"); + assertThat(result).isEqualTo("CoopAssetsTransaction(M-?????: )"); } @Test void toShortStringEmptyTransactionDoesNotThrowException() { final var result = givenEmptyCoopAssetsTransaction.toShortString(); - assertThat(result).isEqualTo("nullnu"); + assertThat(result).isEqualTo("M-?????:+0.00"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionRepositoryIntegrationTest.java index f18447df..978e2081 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionRepositoryIntegrationTest.java @@ -89,7 +89,6 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase context("superuser-alex@hostsharing.net"); final var initialRoleNames = distinctRoleNamesOf(rawRoleRepo.findAll()); final var initialGrantNames = distinctGrantDisplaysOf(rawGrantRepo.findAll()).stream() - .map(s -> s.replace("FirstGmbH-firstcontact", "...")) .map(s -> s.replace("hs_office_", "")) .toList(); @@ -110,11 +109,11 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase final var all = rawRoleRepo.findAll(); assertThat(distinctRoleNamesOf(all)).containsExactlyInAnyOrder(Array.from(initialRoleNames)); // no new roles created assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())) - .map(s -> s.replace("FirstGmbH-firstcontact", "...")) .map(s -> s.replace("hs_office_", "")) .containsExactlyInAnyOrder(Array.fromFormatted( initialGrantNames, - "{ grant perm view on coopassetstransaction#temprefB to role membership#1000101:....tenant by system and assume }", + "{ grant perm:coopassetstransaction#temprefB:SELECT to role:membership#M-1000101:AGENT by system and assume }", + "{ grant perm:coopassetstransaction#temprefB:UPDATE to role:membership#M-1000101:ADMIN by system and assume }", null)); } @@ -141,17 +140,17 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase // then allTheseCoopAssetsTransactionsAreReturned( result, - "CoopAssetsTransaction(1000101, 2010-03-15, DEPOSIT, 320.00, ref 1000101-1, initial deposit)", - "CoopAssetsTransaction(1000101, 2021-09-01, DISBURSAL, -128.00, ref 1000101-2, partial disbursal)", - "CoopAssetsTransaction(1000101, 2022-10-20, ADJUSTMENT, 128.00, ref 1000101-3, some adjustment)", + "CoopAssetsTransaction(M-1000101: 2010-03-15, DEPOSIT, 320.00, ref 1000101-1, initial deposit)", + "CoopAssetsTransaction(M-1000101: 2021-09-01, DISBURSAL, -128.00, ref 1000101-2, partial disbursal)", + "CoopAssetsTransaction(M-1000101: 2022-10-20, ADJUSTMENT, 128.00, ref 1000101-3, some adjustment)", - "CoopAssetsTransaction(1000202, 2010-03-15, DEPOSIT, 320.00, ref 1000202-1, initial deposit)", - "CoopAssetsTransaction(1000202, 2021-09-01, DISBURSAL, -128.00, ref 1000202-2, partial disbursal)", - "CoopAssetsTransaction(1000202, 2022-10-20, ADJUSTMENT, 128.00, ref 1000202-3, some adjustment)", + "CoopAssetsTransaction(M-1000202: 2010-03-15, DEPOSIT, 320.00, ref 1000202-1, initial deposit)", + "CoopAssetsTransaction(M-1000202: 2021-09-01, DISBURSAL, -128.00, ref 1000202-2, partial disbursal)", + "CoopAssetsTransaction(M-1000202: 2022-10-20, ADJUSTMENT, 128.00, ref 1000202-3, some adjustment)", - "CoopAssetsTransaction(1000303, 2010-03-15, DEPOSIT, 320.00, ref 1000303-1, initial deposit)", - "CoopAssetsTransaction(1000303, 2021-09-01, DISBURSAL, -128.00, ref 1000303-2, partial disbursal)", - "CoopAssetsTransaction(1000303, 2022-10-20, ADJUSTMENT, 128.00, ref 1000303-3, some adjustment)"); + "CoopAssetsTransaction(M-1000303: 2010-03-15, DEPOSIT, 320.00, ref 1000303-1, initial deposit)", + "CoopAssetsTransaction(M-1000303: 2021-09-01, DISBURSAL, -128.00, ref 1000303-2, partial disbursal)", + "CoopAssetsTransaction(M-1000303: 2022-10-20, ADJUSTMENT, 128.00, ref 1000303-3, some adjustment)"); } @Test @@ -169,9 +168,9 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase // then allTheseCoopAssetsTransactionsAreReturned( result, - "CoopAssetsTransaction(1000202, 2010-03-15, DEPOSIT, 320.00, ref 1000202-1, initial deposit)", - "CoopAssetsTransaction(1000202, 2021-09-01, DISBURSAL, -128.00, ref 1000202-2, partial disbursal)", - "CoopAssetsTransaction(1000202, 2022-10-20, ADJUSTMENT, 128.00, ref 1000202-3, some adjustment)"); + "CoopAssetsTransaction(M-1000202: 2010-03-15, DEPOSIT, 320.00, ref 1000202-1, initial deposit)", + "CoopAssetsTransaction(M-1000202: 2021-09-01, DISBURSAL, -128.00, ref 1000202-2, partial disbursal)", + "CoopAssetsTransaction(M-1000202: 2022-10-20, ADJUSTMENT, 128.00, ref 1000202-3, some adjustment)"); } @Test @@ -189,13 +188,13 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase // then allTheseCoopAssetsTransactionsAreReturned( result, - "CoopAssetsTransaction(1000202, 2021-09-01, DISBURSAL, -128.00, ref 1000202-2, partial disbursal)"); + "CoopAssetsTransaction(M-1000202: 2021-09-01, DISBURSAL, -128.00, ref 1000202-2, partial disbursal)"); } @Test - public void normalUser_canViewOnlyRelatedCoopAssetsTransactions() { + public void partnerPersonAdmin_canViewRelatedCoopAssetsTransactions() { // given: - context("superuser-alex@hostsharing.net", "hs_office_partner#10001:FirstGmbH-firstcontact.admin"); + context("superuser-alex@hostsharing.net", "hs_office_person#FirstGmbH:ADMIN"); // when: final var result = coopAssetsTransactionRepo.findCoopAssetsTransactionByOptionalMembershipUuidAndDateRange( @@ -206,9 +205,9 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase // then: exactlyTheseCoopAssetsTransactionsAreReturned( result, - "CoopAssetsTransaction(1000101, 2010-03-15, DEPOSIT, 320.00, ref 1000101-1, initial deposit)", - "CoopAssetsTransaction(1000101, 2021-09-01, DISBURSAL, -128.00, ref 1000101-2, partial disbursal)", - "CoopAssetsTransaction(1000101, 2022-10-20, ADJUSTMENT, 128.00, ref 1000101-3, some adjustment)"); + "CoopAssetsTransaction(M-1000101: 2010-03-15, DEPOSIT, 320.00, ref 1000101-1, initial deposit)", + "CoopAssetsTransaction(M-1000101: 2021-09-01, DISBURSAL, -128.00, ref 1000101-2, partial disbursal)", + "CoopAssetsTransaction(M-1000101: 2022-10-20, ADJUSTMENT, 128.00, ref 1000101-3, some adjustment)"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerAcceptanceTest.java index 3d120cd1..d6291512 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerAcceptanceTest.java @@ -218,17 +218,27 @@ class HsOfficeCoopSharesTransactionControllerAcceptanceTest extends ContextBased @Test @Accepts({"CoopShareTransaction:X(Access Control)"}) - void contactAdminUser_canGetRelatedCoopShareTransaction() { + void partnerPersonUser_canGetRelatedCoopShareTransaction() { context.define("superuser-alex@hostsharing.net"); final var givenCoopShareTransactionUuid = coopSharesTransactionRepo.findCoopSharesTransactionByOptionalMembershipUuidAndDateRange(null, LocalDate.of(2010, 3, 15), LocalDate.of(2010, 3, 15)).get(0).getUuid(); RestAssured // @formatter:off - .given().header("current-user", "contact-admin@firstcontact.example.com").port(port).when().get("http://localhost/api/hs/office/coopsharestransactions/" + givenCoopShareTransactionUuid).then().log().body().assertThat().statusCode(200).contentType("application/json").body("", lenientlyEquals(""" - { - "transactionType": "SUBSCRIPTION", - "shareCount": 4 - } - """)); // @formatter:on + .given() + .header("current-user", "person-FirstGmbH@example.com") + .port(port) + .when() + .get("http://localhost/api/hs/office/coopsharestransactions/" + givenCoopShareTransactionUuid) + .then() + .log().body() + .assertThat() + .statusCode(200) + .contentType("application/json") + .body("", lenientlyEquals(""" + { + "transactionType": "SUBSCRIPTION", + "shareCount": 4 + } + """)); // @formatter:on } } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java index 20602661..eff83079 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java @@ -88,7 +88,6 @@ class HsOfficeCoopSharesTransactionRepositoryIntegrationTest extends ContextBase context("superuser-alex@hostsharing.net"); final var initialRoleNames = distinctRoleNamesOf(rawRoleRepo.findAll()); final var initialGrantNames = distinctGrantDisplaysOf(rawGrantRepo.findAll()).stream() - .map(s -> s.replace("FirstGmbH-firstcontact", "...")) .map(s -> s.replace("hs_office_", "")) .toList(); @@ -109,11 +108,11 @@ class HsOfficeCoopSharesTransactionRepositoryIntegrationTest extends ContextBase final var all = rawRoleRepo.findAll(); assertThat(distinctRoleNamesOf(all)).containsExactlyInAnyOrder(Array.from(initialRoleNames)); // no new roles created assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())) - .map(s -> s.replace("FirstGmbH-firstcontact", "...")) .map(s -> s.replace("hs_office_", "")) .containsExactlyInAnyOrder(Array.fromFormatted( initialGrantNames, - "{ grant perm view on coopsharestransaction#temprefB to role membership#1000101:....tenant by system and assume }", + "{ grant perm:coopsharestransaction#temprefB:SELECT to role:membership#M-1000101:AGENT by system and assume }", + "{ grant perm:coopsharestransaction#temprefB:UPDATE to role:membership#M-1000101:ADMIN by system and assume }", null)); } @@ -194,7 +193,7 @@ class HsOfficeCoopSharesTransactionRepositoryIntegrationTest extends ContextBase @Test public void normalUser_canViewOnlyRelatedCoopSharesTransactions() { // given: - context("superuser-alex@hostsharing.net", "hs_office_partner#10001:FirstGmbH-firstcontact.admin"); + context("superuser-alex@hostsharing.net", "hs_office_membership#M-1000101:ADMIN"); // when: final var result = coopSharesTransactionRepo.findCoopSharesTransactionByOptionalMembershipUuidAndDateRange( diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java index 839039a2..07ecb5f5 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java @@ -7,6 +7,9 @@ import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountRepository; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRepository; import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRepository; +import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository; +import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity; +import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRepository; import net.hostsharing.hsadminng.hs.office.test.ContextBasedTestWithCleanup; import net.hostsharing.test.Accepts; import net.hostsharing.test.JpaAttempt; @@ -24,6 +27,7 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import java.util.UUID; +import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR; import static net.hostsharing.test.IsValidUuidMatcher.isUuidValid; import static net.hostsharing.test.JsonMatcher.lenientlyEquals; import static org.assertj.core.api.Assertions.assertThat; @@ -57,6 +61,12 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu @Autowired HsOfficeBankAccountRepository bankAccountRepo; + @Autowired + HsOfficePersonRepository personRepo; + + @Autowired + HsOfficeRelationRepository relRepo; + @Autowired JpaAttempt jpaAttempt; @@ -81,37 +91,135 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu .contentType("application/json") .body("", lenientlyEquals(""" [ - { - "debitorNumber": 1000111, - "debitorNumberSuffix": 11, - "partner": { "person": { "personType": "LEGAL_PERSON" } }, - "billingContact": { "label": "first contact" }, - "vatId": null, - "vatCountryCode": null, - "vatBusiness": true, - "refundBankAccount": { "holder": "First GmbH" } - }, - { - "debitorNumber": 1000212, - "debitorNumberSuffix": 12, - "partner": { "person": { "tradeName": "Second e.K." } }, - "billingContact": { "label": "second contact" }, - "vatId": null, - "vatCountryCode": null, - "vatBusiness": true, - "refundBankAccount": { "holder": "Second e.K." } - }, - { - "debitorNumber": 1000313, - "debitorNumberSuffix": 13, - "partner": { "person": { "tradeName": "Third OHG" } }, - "billingContact": { "label": "third contact" }, - "vatId": null, - "vatCountryCode": null, - "vatBusiness": true, - "refundBankAccount": { "holder": "Third OHG" } - } - ] + { + "debitorRel": { + "anchor": { + "personType": "LEGAL_PERSON", + "tradeName": "First GmbH", + "givenName": null, + "familyName": null + }, + "holder": { + "personType": "LEGAL_PERSON", + "tradeName": "First GmbH", + "givenName": null, + "familyName": null + }, + "type": "DEBITOR", + "mark": null, + "contact": { + "label": "first contact", + "emailAddresses": "contact-admin@firstcontact.example.com", + "phoneNumbers": "+49 123 1234567" + } + }, + "debitorNumber": 1000111, + "debitorNumberSuffix": 11, + "partner": { + "partnerNumber": 10001, + "partnerRel": { + "anchor": { + "personType": "LEGAL_PERSON", + "tradeName": "Hostsharing eG", + "givenName": null, + "familyName": null + }, + "holder": { + "personType": "LEGAL_PERSON", + "tradeName": "First GmbH", + "givenName": null, + "familyName": null + }, + "type": "PARTNER", + "mark": null, + "contact": { + "label": "first contact", + "emailAddresses": "contact-admin@firstcontact.example.com", + "phoneNumbers": "+49 123 1234567" + } + }, + "details": { + "registrationOffice": "Hamburg", + "registrationNumber": "RegNo123456789", + "birthName": null, + "birthPlace": null, + "birthday": null, + "dateOfDeath": null + } + }, + "billable": true, + "vatId": null, + "vatCountryCode": null, + "vatBusiness": true, + "vatReverseCharge": false, + "refundBankAccount": { + "holder": "First GmbH", + "iban": "DE02120300000000202051", + "bic": "BYLADEM1001" + }, + "defaultPrefix": "fir" + }, + { + "debitorRel": { + "anchor": {"tradeName": "Second e.K."}, + "holder": {"tradeName": "Second e.K."}, + "type": "DEBITOR", + "contact": {"emailAddresses": "contact-admin@secondcontact.example.com"} + }, + "debitorNumber": 1000212, + "debitorNumberSuffix": 12, + "partner": { + "partnerNumber": 10002, + "partnerRel": { + "anchor": {"tradeName": "Hostsharing eG"}, + "holder": {"tradeName": "Second e.K."}, + "type": "PARTNER", + "contact": {"emailAddresses": "contact-admin@secondcontact.example.com"} + }, + "details": { + "registrationOffice": "Hamburg", + "registrationNumber": "RegNo123456789" + } + }, + "billable": true, + "vatId": null, + "vatCountryCode": null, + "vatBusiness": true, + "vatReverseCharge": false, + "refundBankAccount": {"iban": "DE02100500000054540402"}, + "defaultPrefix": "sec" + }, + { + "debitorRel": { + "anchor": {"tradeName": "Third OHG"}, + "holder": {"tradeName": "Third OHG"}, + "type": "DEBITOR", + "contact": {"emailAddresses": "contact-admin@thirdcontact.example.com"} + }, + "debitorNumber": 1000313, + "debitorNumberSuffix": 13, + "partner": { + "partnerNumber": 10003, + "partnerRel": { + "anchor": {"tradeName": "Hostsharing eG"}, + "holder": {"tradeName": "Third OHG"}, + "type": "PARTNER", + "contact": {"emailAddresses": "contact-admin@thirdcontact.example.com"} + }, + "details": { + "registrationOffice": "Hamburg", + "registrationNumber": "RegNo123456789" + } + }, + "billable": true, + "vatId": null, + "vatCountryCode": null, + "vatBusiness": true, + "vatReverseCharge": false, + "refundBankAccount": {"iban": "DE02300209000106531065"}, + "defaultPrefix": "thi" + } + ] """)); // @formatter:on } @@ -132,8 +240,10 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu [ { "debitorNumber": 1000212, - "partner": { "person": { "tradeName": "Second e.K." } }, - "billingContact": { "label": "second contact" }, + "partner": { "partnerNumber": 10002 }, + "debitorRel": { + "contact": { "label": "second contact" } + }, "vatId": null, "vatCountryCode": null, "vatBusiness": true @@ -145,8 +255,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu } @Nested - @Accepts({ "Debitor:C(Create)" }) - class CreateDebitor { + class AddDebitor { @Test void globalAdmin_withoutAssumedRole_canAddDebitorWithBankAccount() { @@ -155,6 +264,17 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("Third").get(0); final var givenContact = contactRepo.findContactByOptionalLabelLike("fourth").get(0); final var givenBankAccount = bankAccountRepo.findByOptionalHolderLike("Fourth").get(0); + final var givenBillingPerson = personRepo.findPersonByOptionalNameLike("Fourth").get(0); + + final var givenDebitorRelUUid = jpaAttempt.transacted(() -> { + context.define("superuser-alex@hostsharing.net"); + return relRepo.save(HsOfficeRelationEntity.builder() + .type(DEBITOR) + .anchor(givenPartner.getPartnerRel().getHolder()) + .holder(givenBillingPerson) + .contact(givenContact) + .build()).getUuid(); + }).assertSuccessful().returnedValue(); final var location = RestAssured // @formatter:off .given() @@ -162,8 +282,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu .contentType(ContentType.JSON) .body(""" { - "partnerUuid": "%s", - "billingContactUuid": "%s", + "debitorRelUuid": "%s", "debitorNumberSuffix": "%s", "billable": "true", "vatId": "VAT123456", @@ -173,7 +292,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu "refundBankAccountUuid": "%s", "defaultPrefix": "for" } - """.formatted( givenPartner.getUuid(), givenContact.getUuid(), ++nextDebitorSuffix, givenBankAccount.getUuid())) + """.formatted( givenDebitorRelUUid, ++nextDebitorSuffix, givenBankAccount.getUuid())) .port(port) .when() .post("http://localhost/api/hs/office/debitors") @@ -183,8 +302,8 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu .body("uuid", isUuidValid()) .body("vatId", is("VAT123456")) .body("defaultPrefix", is("for")) - .body("billingContact.label", is(givenContact.getLabel())) - .body("partner.person.tradeName", is(givenPartner.getPerson().getTradeName())) + .body("debitorRel.contact.label", is(givenContact.getLabel())) + .body("debitorRel.holder.tradeName", is(givenBillingPerson.getTradeName())) .body("refundBankAccount.holder", is(givenBankAccount.getHolder())) .header("Location", startsWith("http://localhost")) .extract().header("Location"); // @formatter:on @@ -207,15 +326,23 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu .header("current-user", "superuser-alex@hostsharing.net") .contentType(ContentType.JSON) .body(""" - { - "partnerUuid": "%s", - "billingContactUuid": "%s", - "debitorNumberSuffix": "%s", - "defaultPrefix": "for", - "billable": "true", - "vatReverseCharge": "false" - } - """.formatted( givenPartner.getUuid(), givenContact.getUuid(), ++nextDebitorSuffix)) + { + "debitorRel": { + "type": "DEBITOR", + "anchorUuid": "%s", + "holderUuid": "%s", + "contactUuid": "%s" + }, + "debitorNumberSuffix": "%s", + "defaultPrefix": "for", + "billable": "true", + "vatReverseCharge": "false" + } + """.formatted( + givenPartner.getPartnerRel().getHolder().getUuid(), + givenPartner.getPartnerRel().getHolder().getUuid(), + givenContact.getUuid(), + ++nextDebitorSuffix)) .port(port) .when() .post("http://localhost/api/hs/office/debitors") @@ -223,8 +350,8 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu .statusCode(201) .contentType(ContentType.JSON) .body("uuid", isUuidValid()) - .body("billingContact.label", is(givenContact.getLabel())) - .body("partner.person.tradeName", is(givenPartner.getPerson().getTradeName())) + .body("debitorRel.contact.label", is(givenContact.getLabel())) + .body("partner.partnerRel.holder.tradeName", is(givenPartner.getPartnerRel().getHolder().getTradeName())) .body("vatId", equalTo(null)) .body("vatCountryCode", equalTo(null)) .body("vatBusiness", equalTo(false)) @@ -251,19 +378,22 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu .header("current-user", "superuser-alex@hostsharing.net") .contentType(ContentType.JSON) .body(""" - { - "partnerUuid": "%s", - "billingContactUuid": "%s", - "debitorNumberSuffix": "%s", - "billable": "true", - "vatId": "VAT123456", - "vatCountryCode": "DE", - "vatBusiness": true, - "vatReverseCharge": "false", - "defaultPrefix": "thi" - } - """ - .formatted( givenPartner.getUuid(), givenContactUuid, ++nextDebitorSuffix)) + { + "debitorRel": { + "type": "DEBITOR", + "anchorUuid": "%s", + "holderUuid": "%s", + "contactUuid": "%s" + }, + "debitorNumberSuffix": "%s", + "defaultPrefix": "for", + "billable": "true", + "vatReverseCharge": "false" + } + """.formatted( + givenPartner.getPartnerRel().getAnchor().getUuid(), + givenPartner.getPartnerRel().getAnchor().getUuid(), + givenContactUuid, ++nextDebitorSuffix)) .port(port) .when() .post("http://localhost/api/hs/office/debitors") @@ -274,10 +404,10 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu } @Test - void globalAdmin_canNotAddDebitor_ifPartnerDoesNotExist() { + void globalAdmin_canNotAddDebitor_ifDebitorRelDoesNotExist() { context.define("superuser-alex@hostsharing.net"); - final var givenPartnerUuid = UUID.fromString("00000000-0000-0000-0000-000000000000"); + final var givenDebitorRelUuid = UUID.fromString("00000000-0000-0000-0000-000000000000"); final var givenContact = contactRepo.findContactByOptionalLabelLike("fourth").get(0); final var location = RestAssured // @formatter:off @@ -285,24 +415,20 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu .header("current-user", "superuser-alex@hostsharing.net") .contentType(ContentType.JSON) .body(""" - { - "partnerUuid": "%s", - "billingContactUuid": "%s", - "debitorNumberSuffix": "%s", - "billable": "true", - "vatId": "VAT123456", - "vatCountryCode": "DE", - "vatBusiness": true, - "vatReverseCharge": "false", - "defaultPrefix": "for" - } - """.formatted( givenPartnerUuid, givenContact.getUuid(), ++nextDebitorSuffix)) + { + "debitorRelUuid": "%s", + "debitorNumberSuffix": "%s", + "defaultPrefix": "for", + "billable": "true", + "vatReverseCharge": "false" + } + """.formatted(givenDebitorRelUuid, ++nextDebitorSuffix)) .port(port) .when() .post("http://localhost/api/hs/office/debitors") .then().log().all().assertThat() .statusCode(400) - .body("message", is("Unable to find Partner with uuid 00000000-0000-0000-0000-000000000000")); + .body("message", is("Unable to find HsOfficeRelationEntity with uuid 00000000-0000-0000-0000-000000000000")); // @formatter:on } } @@ -322,14 +448,53 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu .port(port) .when() .get("http://localhost/api/hs/office/debitors/" + givenDebitorUuid) - .then().log().body().assertThat() + .then().log().all().assertThat() .statusCode(200) .contentType("application/json") .body("", lenientlyEquals(""" { - "partner": { person: { "tradeName": "First GmbH" } }, - "billingContact": { "label": "first contact" } - } + "debitorRel": { + "anchor": { "personType": "LEGAL_PERSON", "tradeName": "First GmbH"}, + "holder": { "personType": "LEGAL_PERSON", "tradeName": "First GmbH"}, + "type": "DEBITOR", + "contact": { + "label": "first contact", + "postalAddress": "\\nVorname Nachname\\nStraße Hnr\\nPLZ Stadt\\n", + "emailAddresses": "contact-admin@firstcontact.example.com", + "phoneNumbers": "+49 123 1234567" + } + }, + "debitorNumber": 1000111, + "debitorNumberSuffix": 11, + "partner": { + "partnerNumber": 10001, + "partnerRel": { + "anchor": { "personType": "LEGAL_PERSON", "tradeName": "Hostsharing eG"}, + "holder": { "personType": "LEGAL_PERSON", "tradeName": "First GmbH"}, + "type": "PARTNER", + "mark": null, + "contact": { + "label": "first contact", + "postalAddress": "\\nVorname Nachname\\nStraße Hnr\\nPLZ Stadt\\n", + "emailAddresses": "contact-admin@firstcontact.example.com", + "phoneNumbers": "+49 123 1234567" + } + }, + "details": { + "registrationOffice": "Hamburg", + "registrationNumber": "RegNo123456789" + } + }, + "billable": true, + "vatBusiness": true, + "vatReverseCharge": false, + "refundBankAccount": { + "holder": "First GmbH", + "iban": "DE02120300000000202051", + "bic": "BYLADEM1001" + }, + "defaultPrefix": "fir" + } """)); // @formatter:on } @@ -351,7 +516,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu @Test @Accepts({ "Debitor:X(Access Control)" }) - void contactAdminUser_canGetRelatedDebitor() { + void contactAdminUser_canGetRelatedDebitorExceptRefundBankAccount() { context.define("superuser-alex@hostsharing.net"); final var givenDebitorUuid = debitorRepo.findDebitorByOptionalNameLike("first contact").get(0).getUuid(); @@ -366,9 +531,10 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu .contentType("application/json") .body("", lenientlyEquals(""" { - "partner": { person: { "tradeName": "First GmbH" } }, - "billingContact": { "label": "first contact" }, - "refundBankAccount": { "holder": "First GmbH" } + "debitorNumber": 1000111, + "partner": { "partnerNumber": 10001 }, + "debitorRel": { "contact": { "label": "first contact" } }, + "refundBankAccount": null } """)); // @formatter:on } @@ -379,7 +545,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu class PatchDebitor { @Test - void globalAdmin_withoutAssumedRole_canPatchAllPropertiesOfArbitraryDebitor() { + void globalAdmin_withoutAssumedRole_canPatchArbitraryDebitor() { context.define("superuser-alex@hostsharing.net"); final var givenDebitor = givenSomeTemporaryDebitor(); @@ -401,77 +567,90 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu .port(port) .when() .patch("http://localhost/api/hs/office/debitors/" + givenDebitor.getUuid()) - .then().assertThat() + .then().log().all().assertThat() .statusCode(200) .contentType(ContentType.JSON) - .body("uuid", isUuidValid()) - .body("vatId", is("VAT222222")) - .body("vatCountryCode", is("AA")) - .body("vatBusiness", is(true)) - .body("defaultPrefix", is("for")) - .body("billingContact.label", is(givenContact.getLabel())) - .body("partner.person.tradeName", is(givenDebitor.getPartner().getPerson().getTradeName())); + .body("", lenientlyEquals(""" + { + "debitorRel": { + "anchor": { "tradeName": "Fourth eG" }, + "holder": { "tradeName": "Fourth eG" }, + "type": "DEBITOR", + "mark": null, + "contact": { "label": "fourth contact" } + }, + "debitorNumber": 10004${debitorNumberSuffix}, + "debitorNumberSuffix": ${debitorNumberSuffix}, + "partner": { + "partnerNumber": 10004, + "partnerRel": { + "anchor": { "tradeName": "Hostsharing eG" }, + "holder": { "tradeName": "Fourth eG" }, + "type": "PARTNER", + "mark": null, + "contact": { "label": "fourth contact" } + }, + "details": { + "registrationOffice": "Hamburg", + "registrationNumber": "RegNo123456789", + "birthName": null, + "birthPlace": null, + "birthday": null, + "dateOfDeath": null + } + }, + "billable": true, + "vatId": "VAT222222", + "vatCountryCode": "AA", + "vatBusiness": true, + "vatReverseCharge": false, + "defaultPrefix": "for" + } + """ + .replace("${debitorNumberSuffix}", givenDebitor.getDebitorNumberSuffix().toString())) + ); // @formatter:on // finally, the debitor is actually updated context.define("superuser-alex@hostsharing.net"); assertThat(debitorRepo.findByUuid(givenDebitor.getUuid())).isPresent().get() - .matches(partner -> { - assertThat(partner.getPartner().getPerson().getTradeName()).isEqualTo(givenDebitor.getPartner() - .getPerson() - .getTradeName()); - assertThat(partner.getBillingContact().getLabel()).isEqualTo("fourth contact"); - assertThat(partner.getVatId()).isEqualTo("VAT222222"); - assertThat(partner.getVatCountryCode()).isEqualTo("AA"); - assertThat(partner.isVatBusiness()).isEqualTo(true); + .matches(debitor -> { + assertThat(debitor.getDebitorRel().getHolder().getTradeName()) + .isEqualTo(givenDebitor.getDebitorRel().getHolder().getTradeName()); + assertThat(debitor.getDebitorRel().getContact().getLabel()).isEqualTo("fourth contact"); + assertThat(debitor.getVatId()).isEqualTo("VAT222222"); + assertThat(debitor.getVatCountryCode()).isEqualTo("AA"); + assertThat(debitor.isVatBusiness()).isEqualTo(true); return true; }); } @Test - void globalAdmin_withoutAssumedRole_canPatchPartialPropertiesOfArbitraryDebitor() { + void theContactOwner_canNotPatchARelatedDebitor() { context.define("superuser-alex@hostsharing.net"); final var givenDebitor = givenSomeTemporaryDebitor(); - final var newBillingContact = contactRepo.findContactByOptionalLabelLike("sixth").get(0); - final var location = RestAssured // @formatter:off - .given() + // @formatter:on + RestAssured // @formatter:off + .given() .header("current-user", "superuser-alex@hostsharing.net") + .header("assumed-roles", "hs_office_contact#fourthcontact:ADMIN") .contentType(ContentType.JSON) .body(""" - { - "billingContactUuid": "%s", - "vatId": "VAT999999" - } - """.formatted(newBillingContact.getUuid())) + { + "vatId": "VAT999999" + } + """) .port(port) - .when() + .when() .patch("http://localhost/api/hs/office/debitors/" + givenDebitor.getUuid()) - .then().assertThat() - .statusCode(200) - .contentType(ContentType.JSON) - .body("uuid", isUuidValid()) - .body("billingContact.label", is("sixth contact")) - .body("vatId", is("VAT999999")) - .body("vatCountryCode", is(givenDebitor.getVatCountryCode())) - .body("vatBusiness", is(givenDebitor.isVatBusiness())); - // @formatter:on + .then().log().all().assertThat() + .statusCode(403) + .body("message", containsString("ERROR: [403] Subject")) + .body("message", containsString("is not allowed to update hs_office_debitor uuid ")); - // finally, the debitor is actually updated - assertThat(debitorRepo.findByUuid(givenDebitor.getUuid())).isPresent().get() - .matches(partner -> { - assertThat(partner.getPartner().getPerson().getTradeName()).isEqualTo(givenDebitor.getPartner() - .getPerson() - .getTradeName()); - assertThat(partner.getBillingContact().getLabel()).isEqualTo("sixth contact"); - assertThat(partner.getVatId()).isEqualTo("VAT999999"); - assertThat(partner.getVatCountryCode()).isEqualTo(givenDebitor.getVatCountryCode()); - assertThat(partner.isVatBusiness()).isEqualTo(givenDebitor.isVatBusiness()); - return true; - }); } - } @Nested @@ -501,7 +680,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu void contactAdminUser_canNotDeleteRelatedDebitor() { context.define("superuser-alex@hostsharing.net"); final var givenDebitor = givenSomeTemporaryDebitor(); - assertThat(givenDebitor.getBillingContact().getLabel()).isEqualTo("fourth contact"); + assertThat(givenDebitor.getDebitorRel().getContact().getLabel()).isEqualTo("fourth contact"); RestAssured // @formatter:off .given() @@ -521,7 +700,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu void normalUser_canNotDeleteUnrelatedDebitor() { context.define("superuser-alex@hostsharing.net"); final var givenDebitor = givenSomeTemporaryDebitor(); - assertThat(givenDebitor.getBillingContact().getLabel()).isEqualTo("fourth contact"); + assertThat(givenDebitor.getDebitorRel().getContact().getLabel()).isEqualTo("fourth contact"); RestAssured // @formatter:off .given() @@ -543,10 +722,16 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("Fourth").get(0); final var givenContact = contactRepo.findContactByOptionalLabelLike("fourth contact").get(0); final var newDebitor = HsOfficeDebitorEntity.builder() - .debitorNumberSuffix(++nextDebitorSuffix) + .debitorNumberSuffix(nextDebitorSuffix()) .billable(true) - .partner(givenPartner) - .billingContact(givenContact) + .debitorRel( + HsOfficeRelationEntity.builder() + .type(DEBITOR) + .anchor(givenPartner.getPartnerRel().getHolder()) + .holder(givenPartner.getPartnerRel().getHolder()) + .contact(givenContact) + .build() + ) .defaultPrefix("abc") .vatReverseCharge(false) .build(); @@ -566,4 +751,8 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu System.out.printf("deleted %d entities%n", count); }); } + + private String nextDebitorSuffix() { + return String.format("%02d", nextDebitorSuffix++); + } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntityPatcherUnitTest.java index 01ea5777..4d826224 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntityPatcherUnitTest.java @@ -1,9 +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.partner.HsOfficePartnerEntity; +import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity; import net.hostsharing.test.PatchUnitTestBase; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestInstance; @@ -28,9 +27,8 @@ class HsOfficeDebitorEntityPatcherUnitTest extends PatchUnitTestBase< > { private static final UUID INITIAL_DEBITOR_UUID = UUID.randomUUID(); - private static final UUID INITIAL_PARTNER_UUID = UUID.randomUUID(); - private static final UUID INITIAL_CONTACT_UUID = UUID.randomUUID(); - private static final UUID PATCHED_CONTACT_UUID = UUID.randomUUID(); + private static final UUID INITIAL_DEBITOR_REL_UUID = UUID.randomUUID(); + private static final UUID PATCHED_DEBITOR_REL_UUID = UUID.randomUUID(); private static final String PATCHED_DEFAULT_PREFIX = "xyz"; private static final String PATCHED_VAT_COUNTRY_CODE = "ZZ"; @@ -46,12 +44,8 @@ class HsOfficeDebitorEntityPatcherUnitTest extends PatchUnitTestBase< private static final UUID INITIAL_REFUND_BANK_ACCOUNT_UUID = UUID.randomUUID(); private static final UUID PATCHED_REFUND_BANK_ACCOUNT_UUID = UUID.randomUUID(); - private final HsOfficePartnerEntity givenInitialPartner = HsOfficePartnerEntity.builder() - .uuid(INITIAL_PARTNER_UUID) - .build(); - - private final HsOfficeContactEntity givenInitialContact = HsOfficeContactEntity.builder() - .uuid(INITIAL_CONTACT_UUID) + private final HsOfficeRelationEntity givenInitialDebitorRel = HsOfficeRelationEntity.builder() + .uuid(INITIAL_DEBITOR_REL_UUID) .build(); private final HsOfficeBankAccountEntity givenInitialBankAccount = HsOfficeBankAccountEntity.builder() @@ -62,8 +56,8 @@ class HsOfficeDebitorEntityPatcherUnitTest extends PatchUnitTestBase< @BeforeEach void initMocks() { - lenient().when(em.getReference(eq(HsOfficeContactEntity.class), any())).thenAnswer(invocation -> - HsOfficeContactEntity.builder().uuid(invocation.getArgument(1)).build()); + lenient().when(em.getReference(eq(HsOfficeRelationEntity.class), any())).thenAnswer(invocation -> + HsOfficeRelationEntity.builder().uuid(invocation.getArgument(1)).build()); lenient().when(em.getReference(eq(HsOfficeBankAccountEntity.class), any())).thenAnswer(invocation -> HsOfficeBankAccountEntity.builder().uuid(invocation.getArgument(1)).build()); } @@ -72,8 +66,7 @@ class HsOfficeDebitorEntityPatcherUnitTest extends PatchUnitTestBase< protected HsOfficeDebitorEntity newInitialEntity() { final var entity = new HsOfficeDebitorEntity(); entity.setUuid(INITIAL_DEBITOR_UUID); - entity.setPartner(givenInitialPartner); - entity.setBillingContact(givenInitialContact); + entity.setDebitorRel(givenInitialDebitorRel); entity.setBillable(INITIAL_BILLABLE); entity.setVatId("initial VAT-ID"); entity.setVatCountryCode("AA"); @@ -98,11 +91,11 @@ class HsOfficeDebitorEntityPatcherUnitTest extends PatchUnitTestBase< protected Stream propertyTestDescriptors() { return Stream.of( new JsonNullableProperty<>( - "billingContact", - HsOfficeDebitorPatchResource::setBillingContactUuid, - PATCHED_CONTACT_UUID, - HsOfficeDebitorEntity::setBillingContact, - newBillingContact(PATCHED_CONTACT_UUID)) + "debitorRel", + HsOfficeDebitorPatchResource::setDebitorRelUuid, + PATCHED_DEBITOR_REL_UUID, + HsOfficeDebitorEntity::setDebitorRel, + newDebitorRel(PATCHED_DEBITOR_REL_UUID)) .notNullable(), new SimpleProperty<>( "billable", @@ -129,7 +122,7 @@ class HsOfficeDebitorEntityPatcherUnitTest extends PatchUnitTestBase< new SimpleProperty<>( "vatReverseCharge", HsOfficeDebitorPatchResource::setVatReverseCharge, - PATCHED_BILLABLE, + PATCHED_VAT_REVERSE_CHARGE, HsOfficeDebitorEntity::setVatReverseCharge) .notNullable(), new JsonNullableProperty<>( @@ -148,15 +141,15 @@ class HsOfficeDebitorEntityPatcherUnitTest extends PatchUnitTestBase< ); } - private HsOfficeContactEntity newBillingContact(final UUID uuid) { - final var newContact = new HsOfficeContactEntity(); - newContact.setUuid(uuid); - return newContact; + private HsOfficeRelationEntity newDebitorRel(final UUID uuid) { + return HsOfficeRelationEntity.builder() + .uuid(uuid) + .build(); } private HsOfficeBankAccountEntity newBankAccount(final UUID uuid) { - final var newBankAccount = new HsOfficeBankAccountEntity(); - newBankAccount.setUuid(uuid); - return newBankAccount; + return HsOfficeBankAccountEntity.builder() + .uuid(uuid) + .build(); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntityUnitTest.java index 96f1ba13..cb629b2b 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntityUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntityUnitTest.java @@ -1,61 +1,52 @@ package net.hostsharing.hsadminng.hs.office.debitor; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity; -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.person.HsOfficePersonType; +import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class HsOfficeDebitorEntityUnitTest { + private HsOfficeRelationEntity givenDebitorRel = HsOfficeRelationEntity.builder() + .anchor(HsOfficePersonEntity.builder() + .personType(HsOfficePersonType.LEGAL_PERSON) + .tradeName("some partner trade name") + .build()) + .holder(HsOfficePersonEntity.builder() + .personType(HsOfficePersonType.LEGAL_PERSON) + .tradeName("some billing trade name") + .build()) + .contact(HsOfficeContactEntity.builder().label("some label").build()) + .build(); + @Test void toStringContainsPartnerAndContact() { final var given = HsOfficeDebitorEntity.builder() - .debitorNumberSuffix((byte)67) - .partner(HsOfficePartnerEntity.builder() - .person(HsOfficePersonEntity.builder() - .personType(HsOfficePersonType.LEGAL_PERSON) - .tradeName("some trade name") - .build()) - .details(HsOfficePartnerDetailsEntity.builder().birthName("some birth name").build()) - .partnerNumber(12345) - .build()) - .billingContact(HsOfficeContactEntity.builder().label("some label").build()) + .debitorNumberSuffix("67") + .debitorRel(givenDebitorRel) .defaultPrefix("som") - .build(); - - final var result = given.toString(); - - assertThat(result).isEqualTo("debitor(D-1234567: LP some trade name: som)"); - } - - @Test - void toStringWithoutPersonContainsDebitorNumber() { - final var given = HsOfficeDebitorEntity.builder() - .debitorNumberSuffix((byte)67) .partner(HsOfficePartnerEntity.builder() - .person(null) - .details(HsOfficePartnerDetailsEntity.builder().birthName("some birth name").build()) .partnerNumber(12345) .build()) - .billingContact(HsOfficeContactEntity.builder().label("some label").build()) .build(); final var result = given.toString(); - assertThat(result).isEqualTo("debitor(D-1234567: )"); + assertThat(result).isEqualTo("debitor(D-1234567: rel(anchor='LP some partner trade name', holder='LP some billing trade name'), som)"); } @Test void toShortStringContainsDebitorNumber() { final var given = HsOfficeDebitorEntity.builder() + .debitorRel(givenDebitorRel) + .debitorNumberSuffix("67") .partner(HsOfficePartnerEntity.builder() .partnerNumber(12345) .build()) - .debitorNumberSuffix((byte)67) .build(); final var result = given.toShortString(); @@ -66,10 +57,11 @@ class HsOfficeDebitorEntityUnitTest { @Test void getDebitorNumberWithPartnerNumberAndDebitorNumberSuffix() { final var given = HsOfficeDebitorEntity.builder() + .debitorRel(givenDebitorRel) + .debitorNumberSuffix("67") .partner(HsOfficePartnerEntity.builder() .partnerNumber(12345) .build()) - .debitorNumberSuffix((byte)67) .build(); final var result = given.getDebitorNumber(); @@ -80,8 +72,9 @@ class HsOfficeDebitorEntityUnitTest { @Test void getDebitorNumberWithoutPartnerReturnsNull() { final var given = HsOfficeDebitorEntity.builder() + .debitorRel(givenDebitorRel) + .debitorNumberSuffix("67") .partner(null) - .debitorNumberSuffix((byte)67) .build(); final var result = given.getDebitorNumber(); @@ -92,10 +85,9 @@ class HsOfficeDebitorEntityUnitTest { @Test void getDebitorNumberWithoutPartnerNumberReturnsNull() { final var given = HsOfficeDebitorEntity.builder() - .partner(HsOfficePartnerEntity.builder() - .partnerNumber(null) - .build()) - .debitorNumberSuffix((byte)67) + .debitorRel(givenDebitorRel) + .debitorNumberSuffix("67") + .partner(HsOfficePartnerEntity.builder().build()) .build(); final var result = given.getDebitorNumber(); @@ -106,10 +98,11 @@ class HsOfficeDebitorEntityUnitTest { @Test void getDebitorNumberWithoutDebitorNumberSuffixReturnsNull() { final var given = HsOfficeDebitorEntity.builder() + .debitorRel(givenDebitorRel) + .debitorNumberSuffix(null) .partner(HsOfficePartnerEntity.builder() .partnerNumber(12345) .build()) - .debitorNumberSuffix(null) .build(); final var result = given.getDebitorNumber(); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java index c703c31a..32f441af 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java @@ -4,11 +4,16 @@ import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountRepository; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRepository; import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRepository; +import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository; +import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity; +import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType; import net.hostsharing.hsadminng.hs.office.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository; +import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService; import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository; import net.hostsharing.test.Array; import net.hostsharing.test.JpaAttempt; +import org.hibernate.Hibernate; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -18,6 +23,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; +import org.springframework.orm.jpa.JpaObjectRetrievalFailureException; import org.springframework.orm.jpa.JpaSystemException; import org.springframework.transaction.annotation.Transactional; @@ -27,13 +33,14 @@ import jakarta.servlet.http.HttpServletRequest; import java.util.Arrays; import java.util.List; +import static net.hostsharing.hsadminng.hs.office.test.EntityList.one; import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf; import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.distinctRoleNamesOf; import static net.hostsharing.test.JpaAttempt.attempt; import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest -@Import( { Context.class, JpaAttempt.class }) +@Import( { Context.class, JpaAttempt.class, RbacGrantsDiagramService.class }) class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithCleanup { @Autowired @@ -45,6 +52,9 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean @Autowired HsOfficeContactRepository contactRepo; + @Autowired + HsOfficePersonRepository personRepo; + @Autowired HsOfficeBankAccountRepository bankAccountRepo; @@ -60,9 +70,11 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean @Autowired JpaAttempt jpaAttempt; + @Autowired + RbacGrantsDiagramService mermaidService; + @MockBean HttpServletRequest request; - @Nested class CreateDebitor { @@ -71,15 +83,19 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean // given context("superuser-alex@hostsharing.net"); final var count = debitorRepo.count(); - final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("First GmbH").get(0); - final var givenContact = contactRepo.findContactByOptionalLabelLike("first contact").get(0); + final var givenPartnerPerson = one(personRepo.findPersonByOptionalNameLike("First GmbH")); + final var givenContact = one(contactRepo.findContactByOptionalLabelLike("first contact")); // when final var result = attempt(em, () -> { final var newDebitor = HsOfficeDebitorEntity.builder() - .debitorNumberSuffix((byte)21) - .partner(givenPartner) - .billingContact(givenContact) + .debitorNumberSuffix("21") + .debitorRel(HsOfficeRelationEntity.builder() + .type(HsOfficeRelationType.DEBITOR) + .anchor(givenPartnerPerson) + .holder(givenPartnerPerson) + .contact(givenContact) + .build()) .defaultPrefix("abc") .billable(false) .build(); @@ -99,16 +115,19 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean public void canNotCreateNewDebitorWithInvalidDefaultPrefix(final String givenPrefix) { // given context("superuser-alex@hostsharing.net"); - final var count = debitorRepo.count(); - final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("First GmbH").get(0); - final var givenContact = contactRepo.findContactByOptionalLabelLike("first contact").get(0); + final var givenPartnerPerson = one(personRepo.findPersonByOptionalNameLike("First GmbH")); + final var givenContact = one(contactRepo.findContactByOptionalLabelLike("first contact")); // when final var result = attempt(em, () -> { final var newDebitor = HsOfficeDebitorEntity.builder() - .debitorNumberSuffix((byte)21) - .partner(givenPartner) - .billingContact(givenContact) + .debitorNumberSuffix("21") + .debitorRel(HsOfficeRelationEntity.builder() + .type(HsOfficeRelationType.DEBITOR) + .anchor(givenPartnerPerson) + .holder(givenPartnerPerson) + .contact(givenContact) + .build()) .billable(true) .vatReverseCharge(false) .vatBusiness(false) @@ -118,8 +137,7 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean }); // then - System.out.println("ok"); -// result.assertExceptionWithRootCauseMessage(org.hibernate.exception.ConstraintViolationException.class); + result.assertExceptionWithRootCauseMessage(org.hibernate.exception.ConstraintViolationException.class); } @Test @@ -129,21 +147,22 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean final var initialRoleNames = distinctRoleNamesOf(rawRoleRepo.findAll()); final var initialGrantNames = distinctGrantDisplaysOf(rawGrantRepo.findAll()).stream() // some search+replace to make the output fit into the screen width - .map(s -> s.replace("superuser-alex@hostsharing.net", "superuser-alex")) - .map(s -> s.replace("22FourtheG-fourthcontact", "FeG")) - .map(s -> s.replace("FourtheG-fourthcontact", "FeG")) - .map(s -> s.replace("fourthcontact", "4th")) .map(s -> s.replace("hs_office_", "")) .toList(); // when attempt(em, () -> { - final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("Fourth").get(0); - final var givenContact = contactRepo.findContactByOptionalLabelLike("fourth contact").get(0); + final var givenPartnerPerson = one(personRepo.findPersonByOptionalNameLike("First GmbH")); + final var givenDebitorPerson = one(personRepo.findPersonByOptionalNameLike("Fourth eG")); + final var givenContact = one(contactRepo.findContactByOptionalLabelLike("fourth contact")); final var newDebitor = HsOfficeDebitorEntity.builder() - .debitorNumberSuffix((byte)22) - .partner(givenPartner) - .billingContact(givenContact) + .debitorNumberSuffix("22") + .debitorRel(HsOfficeRelationEntity.builder() + .type(HsOfficeRelationType.DEBITOR) + .anchor(givenPartnerPerson) + .holder(givenDebitorPerson) + .contact(givenContact) + .build()) .defaultPrefix("abc") .billable(false) .build(); @@ -153,49 +172,52 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean // then assertThat(distinctRoleNamesOf(rawRoleRepo.findAll())).containsExactlyInAnyOrder(Array.from( initialRoleNames, - "hs_office_debitor#1000422:FourtheG-fourthcontact.owner", - "hs_office_debitor#1000422:FourtheG-fourthcontact.admin", - "hs_office_debitor#1000422:FourtheG-fourthcontact.agent", - "hs_office_debitor#1000422:FourtheG-fourthcontact.tenant", - "hs_office_debitor#1000422:FourtheG-fourthcontact.guest")); + "hs_office_relation#FirstGmbH-with-DEBITOR-FourtheG:OWNER", + "hs_office_relation#FirstGmbH-with-DEBITOR-FourtheG:ADMIN", + "hs_office_relation#FirstGmbH-with-DEBITOR-FourtheG:AGENT", + "hs_office_relation#FirstGmbH-with-DEBITOR-FourtheG:TENANT")); assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())) - .map(s -> s.replace("superuser-alex@hostsharing.net", "superuser-alex")) - .map(s -> s.replace("22FourtheG-fourthcontact", "FeG")) - .map(s -> s.replace("FourtheG-fourthcontact", "FeG")) - .map(s -> s.replace("fourthcontact", "4th")) - .map(s -> s.replace("hs_office_", "")) - .containsExactlyInAnyOrder(Array.fromFormatted( - initialGrantNames, - // owner - "{ grant perm * on debitor#1000422:FeG to role debitor#1000422:FeG.owner by system and assume }", - "{ grant role debitor#1000422:FeG.owner to role global#global.admin by system and assume }", - "{ grant role debitor#1000422:FeG.owner to user superuser-alex by global#global.admin and assume }", + .map(s -> s.replace("hs_office_", "")) + .containsExactlyInAnyOrder(Array.fromFormatted( + initialGrantNames, + "{ grant perm:relation#FirstGmbH-with-DEBITOR-FourtheG:INSERT>sepamandate to role:relation#FirstGmbH-with-DEBITOR-FourtheG:ADMIN by system and assume }", - // admin - "{ grant perm edit on debitor#1000422:FeG to role debitor#1000422:FeG.admin by system and assume }", - "{ grant role debitor#1000422:FeG.admin to role debitor#1000422:FeG.owner by system and assume }", + // owner + "{ grant perm:debitor#D-1000122:DELETE to role:relation#FirstGmbH-with-DEBITOR-FourtheG:OWNER by system and assume }", + "{ grant perm:relation#FirstGmbH-with-DEBITOR-FourtheG:DELETE to role:relation#FirstGmbH-with-DEBITOR-FourtheG:OWNER by system and assume }", + "{ grant role:relation#FirstGmbH-with-DEBITOR-FourtheG:OWNER to role:global#global:ADMIN by system and assume }", + "{ grant role:relation#FirstGmbH-with-DEBITOR-FourtheG:OWNER to user:superuser-alex@hostsharing.net by relation#FirstGmbH-with-DEBITOR-FourtheG:OWNER and assume }", - // agent - "{ grant role debitor#1000422:FeG.agent to role debitor#1000422:FeG.admin by system and assume }", - "{ grant role debitor#1000422:FeG.agent to role contact#4th.admin by system and assume }", - "{ grant role debitor#1000422:FeG.agent to role partner#10004:FeG.admin by system and assume }", + // admin + "{ grant perm:debitor#D-1000122:UPDATE to role:relation#FirstGmbH-with-DEBITOR-FourtheG:ADMIN by system and assume }", + "{ grant perm:relation#FirstGmbH-with-DEBITOR-FourtheG:UPDATE to role:relation#FirstGmbH-with-DEBITOR-FourtheG:ADMIN by system and assume }", + "{ grant role:relation#FirstGmbH-with-DEBITOR-FourtheG:ADMIN to role:relation#FirstGmbH-with-DEBITOR-FourtheG:OWNER by system and assume }", + "{ grant role:relation#FirstGmbH-with-DEBITOR-FourtheG:ADMIN to role:person#FirstGmbH:ADMIN by system and assume }", + "{ grant role:relation#FirstGmbH-with-DEBITOR-FourtheG:ADMIN to role:relation#HostsharingeG-with-PARTNER-FirstGmbH:ADMIN by system and assume }", - // tenant - "{ grant role contact#4th.guest to role debitor#1000422:FeG.tenant by system and assume }", - "{ grant role debitor#1000422:FeG.tenant to role debitor#1000422:FeG.agent by system and assume }", - "{ grant role debitor#1000422:FeG.tenant to role partner#10004:FeG.agent by system and assume }", - "{ grant role partner#10004:FeG.tenant to role debitor#1000422:FeG.tenant by system and assume }", + // agent + "{ grant role:relation#FirstGmbH-with-DEBITOR-FourtheG:AGENT to role:person#FourtheG:ADMIN by system and assume }", + "{ grant role:relation#FirstGmbH-with-DEBITOR-FourtheG:AGENT to role:relation#FirstGmbH-with-DEBITOR-FourtheG:ADMIN by system and assume }", + "{ grant role:relation#FirstGmbH-with-DEBITOR-FourtheG:AGENT to role:relation#HostsharingeG-with-PARTNER-FirstGmbH:AGENT by system and assume }", - // guest - "{ grant perm view on debitor#1000422:FeG to role debitor#1000422:FeG.guest by system and assume }", - "{ grant role debitor#1000422:FeG.guest to role debitor#1000422:FeG.tenant by system and assume }", + // tenant + "{ grant perm:debitor#D-1000122:SELECT to role:relation#FirstGmbH-with-DEBITOR-FourtheG:TENANT by system and assume }", + "{ grant perm:relation#FirstGmbH-with-DEBITOR-FourtheG:SELECT to role:relation#FirstGmbH-with-DEBITOR-FourtheG:TENANT by system and assume }", + "{ grant role:relation#HostsharingeG-with-PARTNER-FirstGmbH:TENANT to role:relation#FirstGmbH-with-DEBITOR-FourtheG:AGENT by system and assume }", + "{ grant role:contact#fourthcontact:REFERRER to role:relation#FirstGmbH-with-DEBITOR-FourtheG:TENANT by system and assume }", + "{ grant role:person#FirstGmbH:REFERRER to role:relation#FirstGmbH-with-DEBITOR-FourtheG:TENANT by system and assume }", + "{ grant role:person#FourtheG:REFERRER to role:relation#FirstGmbH-with-DEBITOR-FourtheG:TENANT by system and assume }", + "{ grant role:relation#FirstGmbH-with-DEBITOR-FourtheG:TENANT to role:contact#fourthcontact:ADMIN by system and assume }", + "{ grant role:relation#FirstGmbH-with-DEBITOR-FourtheG:TENANT to role:person#FourtheG:ADMIN by system and assume }", + "{ grant role:relation#FirstGmbH-with-DEBITOR-FourtheG:TENANT to role:relation#FirstGmbH-with-DEBITOR-FourtheG:AGENT by system and assume }", - null)); + null)); } private void assertThatDebitorIsPersisted(final HsOfficeDebitorEntity saved) { + final var savedRefreshed = refresh(saved); final var found = debitorRepo.findByUuid(saved.getUuid()); - assertThat(found).isNotEmpty().get().usingRecursiveComparison().isEqualTo(saved); + assertThat(found).isNotEmpty().get().usingRecursiveComparison().isEqualTo(savedRefreshed); } } @@ -213,17 +235,17 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean // then allTheseDebitorsAreReturned( result, - "debitor(D-1000111: LP First GmbH: fir)", - "debitor(D-1000212: LP Second e.K.: sec)", - "debitor(D-1000313: IF Third OHG: thi)"); + "debitor(D-1000111: rel(anchor='LP First GmbH', type='DEBITOR', holder='LP First GmbH'), fir)", + "debitor(D-1000212: rel(anchor='LP Second e.K.', type='DEBITOR', holder='LP Second e.K.'), sec)", + "debitor(D-1000313: rel(anchor='IF Third OHG', type='DEBITOR', holder='IF Third OHG'), thi)"); } @ParameterizedTest @Disabled // TODO: reactivate once partner.person + partner.contact are removed @ValueSource(strings = { - "hs_office_partner#10001:FirstGmbH-firstcontact.admin", - "hs_office_person#FirstGmbH.admin", - "hs_office_contact#firstcontact.admin", + "hs_office_partner#10001:FirstGmbH-firstcontact:ADMIN", + "hs_office_person#FirstGmbH:ADMIN", + "hs_office_contact#firstcontact:ADMIN", }) public void relatedPersonAdmin_canViewRelatedDebitors(final String assumedRole) { // given: @@ -234,8 +256,8 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean // then: exactlyTheseDebitorsAreReturned(result, - "debitor(D-1000111: LP First GmbH: fir)", - "debitor(D-1000120: LP First GmbH: fif)"); + "debitor(D-1000111: P-10001, fir)", + "debitor(D-1000120: P-10001, fif)"); } @Test @@ -263,7 +285,8 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean final var result = debitorRepo.findDebitorByDebitorNumber(1000313); // then - exactlyTheseDebitorsAreReturned(result, "debitor(D-1000313: IF Third OHG: thi)"); + exactlyTheseDebitorsAreReturned(result, + "debitor(D-1000313: rel(anchor='IF Third OHG', type='DEBITOR', holder='IF Third OHG'), thi)"); } } @@ -279,7 +302,7 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean final var result = debitorRepo.findDebitorByOptionalNameLike("third contact"); // then - exactlyTheseDebitorsAreReturned(result, "debitor(D-1000313: IF Third OHG: thi)"); + exactlyTheseDebitorsAreReturned(result, "debitor(D-1000313: rel(anchor='IF Third OHG', type='DEBITOR', holder='IF Third OHG'), thi)"); } } @@ -291,13 +314,14 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean // given context("superuser-alex@hostsharing.net"); final var givenDebitor = givenSomeTemporaryDebitor("Fourth", "fifth contact", "Fourth", "fif"); + assertThatDebitorIsVisibleForUserWithRole( givenDebitor, - "hs_office_partner#10004:FourtheG-fourthcontact.admin"); - assertThatDebitorActuallyInDatabase(givenDebitor); - final var givenNewPartner = partnerRepo.findPartnerByOptionalNameLike("First").get(0); - final var givenNewContact = contactRepo.findContactByOptionalLabelLike("sixth contact").get(0); - final var givenNewBankAccount = bankAccountRepo.findByOptionalHolderLike("first").get(0); + "hs_office_relation#FourtheG-with-DEBITOR-FourtheG:ADMIN", true); + final var givenNewPartnerPerson = one(personRepo.findPersonByOptionalNameLike("First")); + final var givenNewBillingPerson = one(personRepo.findPersonByOptionalNameLike("Firby")); + final var givenNewContact = one(contactRepo.findContactByOptionalLabelLike("sixth contact")); + final var givenNewBankAccount = one(bankAccountRepo.findByOptionalHolderLike("first")); final String givenNewVatId = "NEW-VAT-ID"; final String givenNewVatCountryCode = "NC"; final boolean givenNewVatBusiness = !givenDebitor.isVatBusiness(); @@ -305,8 +329,12 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean // when final var result = jpaAttempt.transacted(() -> { context("superuser-alex@hostsharing.net"); - givenDebitor.setPartner(givenNewPartner); - givenDebitor.setBillingContact(givenNewContact); + givenDebitor.setDebitorRel(HsOfficeRelationEntity.builder() + .type(HsOfficeRelationType.DEBITOR) + .anchor(givenNewPartnerPerson) + .holder(givenNewBillingPerson) + .contact(givenNewContact) + .build()); givenDebitor.setRefundBankAccount(givenNewBankAccount); givenDebitor.setVatId(givenNewVatId); givenDebitor.setVatCountryCode(givenNewVatCountryCode); @@ -318,31 +346,31 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean result.assertSuccessful(); assertThatDebitorIsVisibleForUserWithRole( result.returnedValue(), - "global#global.admin"); + "global#global:ADMIN", true); // ... partner role was reassigned: assertThatDebitorIsNotVisibleForUserWithRole( result.returnedValue(), - "hs_office_partner#10004:FourtheG-fourthcontact.agent"); + "hs_office_relation#FourtheG-with-DEBITOR-FourtheG:ADMIN"); assertThatDebitorIsVisibleForUserWithRole( result.returnedValue(), - "hs_office_partner#10001:FirstGmbH-firstcontact.agent"); + "hs_office_relation#FirstGmbH-with-DEBITOR-FirbySusan:AGENT", true); // ... contact role was reassigned: assertThatDebitorIsNotVisibleForUserWithRole( result.returnedValue(), - "hs_office_contact#fifthcontact.admin"); + "hs_office_contact#fifthcontact:ADMIN"); assertThatDebitorIsVisibleForUserWithRole( result.returnedValue(), - "hs_office_contact#sixthcontact.admin"); + "hs_office_contact#sixthcontact:ADMIN", false); // ... bank-account role was reassigned: assertThatDebitorIsNotVisibleForUserWithRole( result.returnedValue(), - "hs_office_bankaccount#FourtheG.admin"); + "hs_office_bankaccount#DE02200505501015871393:ADMIN"); assertThatDebitorIsVisibleForUserWithRole( result.returnedValue(), - "hs_office_bankaccount#FirstGmbH.admin"); + "hs_office_bankaccount#DE02120300000000202051:ADMIN", true); } @Test @@ -352,9 +380,9 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean final var givenDebitor = givenSomeTemporaryDebitor("Fourth", "fifth contact", null, "fig"); assertThatDebitorIsVisibleForUserWithRole( givenDebitor, - "hs_office_partner#10004:FourtheG-fourthcontact.admin"); - assertThatDebitorActuallyInDatabase(givenDebitor); - final var givenNewBankAccount = bankAccountRepo.findByOptionalHolderLike("first").get(0); + "hs_office_relation#FourtheG-with-DEBITOR-FourtheG:ADMIN", true); + assertThatDebitorActuallyInDatabase(givenDebitor, true); + final var givenNewBankAccount = one(bankAccountRepo.findByOptionalHolderLike("first")); // when final var result = jpaAttempt.transacted(() -> { @@ -367,12 +395,12 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean result.assertSuccessful(); assertThatDebitorIsVisibleForUserWithRole( result.returnedValue(), - "global#global.admin"); + "global#global:ADMIN", true); // ... bank-account role was assigned: assertThatDebitorIsVisibleForUserWithRole( result.returnedValue(), - "hs_office_bankaccount#FirstGmbH.admin"); + "hs_office_bankaccount#DE02120300000000202051:ADMIN", true); } @Test @@ -382,8 +410,8 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean final var givenDebitor = givenSomeTemporaryDebitor("Fourth", "fifth contact", "Fourth", "fih"); assertThatDebitorIsVisibleForUserWithRole( givenDebitor, - "hs_office_partner#10004:FourtheG-fourthcontact.admin"); - assertThatDebitorActuallyInDatabase(givenDebitor); + "hs_office_relation#HostsharingeG-with-PARTNER-FourtheG:AGENT", true); + assertThatDebitorActuallyInDatabase(givenDebitor, true); // when final var result = jpaAttempt.transacted(() -> { @@ -396,34 +424,34 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean result.assertSuccessful(); assertThatDebitorIsVisibleForUserWithRole( result.returnedValue(), - "global#global.admin"); + "global#global:ADMIN", true); // ... bank-account role was removed from previous bank-account admin: assertThatDebitorIsNotVisibleForUserWithRole( result.returnedValue(), - "hs_office_bankaccount#FourtheG.admin"); + "hs_office_bankaccount#DE02200505501015871393:ADMIN"); } @Test - public void partnerAdmin_canNotUpdateRelatedDebitor() { + public void partnerAgent_canNotUpdateRelatedDebitor() { // given context("superuser-alex@hostsharing.net"); final var givenDebitor = givenSomeTemporaryDebitor("Fourth", "eighth", "Fourth", "eig"); assertThatDebitorIsVisibleForUserWithRole( givenDebitor, - "hs_office_partner#10004:FourtheG-fourthcontact.admin"); - assertThatDebitorActuallyInDatabase(givenDebitor); + "hs_office_relation#HostsharingeG-with-PARTNER-FourtheG:AGENT", true); + assertThatDebitorActuallyInDatabase(givenDebitor, true); // when final var result = jpaAttempt.transacted(() -> { - context("superuser-alex@hostsharing.net", "hs_office_partner#10004:FourtheG-fourthcontact.admin"); + context("superuser-alex@hostsharing.net", "hs_office_relation#HostsharingeG-with-PARTNER-FourtheG:AGENT"); givenDebitor.setVatId("NEW-VAT-ID"); return toCleanup(debitorRepo.save(givenDebitor)); }); // then - result.assertExceptionWithRootCauseMessage(JpaSystemException.class, - "[403] Subject ", " is not allowed to update hs_office_debitor uuid"); + result.assertExceptionWithRootCauseMessage(JpaSystemException.class, + "[403] Subject ", " is not allowed to update hs_office_debitor uuid"); } @Test @@ -431,35 +459,47 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean // given context("superuser-alex@hostsharing.net"); final var givenDebitor = givenSomeTemporaryDebitor("Fourth", "ninth", "Fourth", "nin"); + assertThatDebitorActuallyInDatabase(givenDebitor, true); assertThatDebitorIsVisibleForUserWithRole( givenDebitor, - "hs_office_contact#ninthcontact.admin"); - assertThatDebitorActuallyInDatabase(givenDebitor); + "hs_office_contact#ninthcontact:ADMIN", false); // when final var result = jpaAttempt.transacted(() -> { - context("superuser-alex@hostsharing.net", "hs_office_contact#ninthcontact.admin"); + context("superuser-alex@hostsharing.net", "hs_office_contact#ninthcontact:ADMIN"); givenDebitor.setVatId("NEW-VAT-ID"); return toCleanup(debitorRepo.save(givenDebitor)); }); // then - result.assertExceptionWithRootCauseMessage(JpaSystemException.class, - "[403] Subject ", " is not allowed to update hs_office_debitor uuid"); + result.assertExceptionWithRootCauseMessage( + JpaObjectRetrievalFailureException.class, + // this technical error message gets translated to a [403] error at the controller level + "Unable to find net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity with id "); } - private void assertThatDebitorActuallyInDatabase(final HsOfficeDebitorEntity saved) { + private void assertThatDebitorActuallyInDatabase(final HsOfficeDebitorEntity saved, final boolean withPartner) { final var found = debitorRepo.findByUuid(saved.getUuid()); - assertThat(found).isNotEmpty().get().isNotSameAs(saved) - .extracting(Object::toString).isEqualTo(saved.toString()); + assertThat(found).isNotEmpty(); + found.ifPresent(foundEntity -> { + em.refresh(foundEntity); + Hibernate.initialize(foundEntity); + assertThat(foundEntity).isNotSameAs(saved); + if (withPartner) { + assertThat(foundEntity.getPartner()).isNotNull(); + } + assertThat(foundEntity.getDebitorRel()).extracting(HsOfficeRelationEntity::toString) + .isEqualTo(saved.getDebitorRel().toString()); + }); } private void assertThatDebitorIsVisibleForUserWithRole( final HsOfficeDebitorEntity entity, - final String assumedRoles) { + final String assumedRoles, + final boolean withPartner) { jpaAttempt.transacted(() -> { context("superuser-alex@hostsharing.net", assumedRoles); - assertThatDebitorActuallyInDatabase(entity); + assertThatDebitorActuallyInDatabase(entity, withPartner); }).assertSuccessful(); } @@ -498,14 +538,14 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean } @Test - public void relatedPerson_canNotDeleteTheirRelatedDebitor() { + public void debitorAgent_canViewButNotDeleteTheirRelatedDebitor() { // given context("superuser-alex@hostsharing.net", null); final var givenDebitor = givenSomeTemporaryDebitor("Fourth", "eleventh", "Fourth", "ele"); // when final var result = jpaAttempt.transacted(() -> { - context("person-FourtheG@example.com"); + context("superuser-alex@hostsharing.net", "hs_office_relation#FourtheG-with-DEBITOR-FourtheG:ADMIN"); assertThat(debitorRepo.findByUuid(givenDebitor.getUuid())).isPresent(); debitorRepo.deleteByUuid(givenDebitor.getUuid()); @@ -562,20 +602,24 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean } private HsOfficeDebitorEntity givenSomeTemporaryDebitor( - final String partner, - final String contact, - final String bankAccount, + final String partnerName, + final String contactLabel, + final String bankAccountHolder, final String defaultPrefix) { return jpaAttempt.transacted(() -> { context("superuser-alex@hostsharing.net"); - final var givenPartner = partnerRepo.findPartnerByOptionalNameLike(partner).get(0); - final var givenContact = contactRepo.findContactByOptionalLabelLike(contact).get(0); + final var givenPartnerPerson = one(personRepo.findPersonByOptionalNameLike(partnerName)); + final var givenContact = one(contactRepo.findContactByOptionalLabelLike(contactLabel)); final var givenBankAccount = - bankAccount != null ? bankAccountRepo.findByOptionalHolderLike(bankAccount).get(0) : null; + bankAccountHolder != null ? one(bankAccountRepo.findByOptionalHolderLike(bankAccountHolder)) : null; final var newDebitor = HsOfficeDebitorEntity.builder() - .debitorNumberSuffix((byte)20) - .partner(givenPartner) - .billingContact(givenContact) + .debitorNumberSuffix("20") + .debitorRel(HsOfficeRelationEntity.builder() + .type(HsOfficeRelationType.DEBITOR) + .anchor(givenPartnerPerson) + .holder(givenPartnerPerson) + .contact(givenContact) + .build()) .refundBankAccount(givenBankAccount) .defaultPrefix(defaultPrefix) .billable(true) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/TestHsOfficeDebitor.java b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/TestHsOfficeDebitor.java index 36b3d534..4305b87a 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/TestHsOfficeDebitor.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/TestHsOfficeDebitor.java @@ -1,7 +1,8 @@ package net.hostsharing.hsadminng.hs.office.debitor; import lombok.experimental.UtilityClass; - +import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; +import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity; import static net.hostsharing.hsadminng.hs.office.contact.TestHsOfficeContact.TEST_CONTACT; import static net.hostsharing.hsadminng.hs.office.partner.TestHsOfficePartner.TEST_PARTNER; @@ -9,11 +10,15 @@ import static net.hostsharing.hsadminng.hs.office.partner.TestHsOfficePartner.TE @UtilityClass public class TestHsOfficeDebitor { - public byte DEFAULT_DEBITOR_SUFFIX = 0; + public String DEFAULT_DEBITOR_SUFFIX = "00"; public static final HsOfficeDebitorEntity TEST_DEBITOR = HsOfficeDebitorEntity.builder() .debitorNumberSuffix(DEFAULT_DEBITOR_SUFFIX) + .debitorRel(HsOfficeRelationEntity.builder() + .holder(HsOfficePersonEntity.builder().build()) + .anchor(HsOfficePersonEntity.builder().build()) + .contact(TEST_CONTACT) + .build()) .partner(TEST_PARTNER) - .billingContact(TEST_CONTACT) .build(); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipControllerAcceptanceTest.java index 293741b6..5ff5c032 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipControllerAcceptanceTest.java @@ -1,11 +1,10 @@ package net.hostsharing.hsadminng.hs.office.membership; -import com.vladmihalcea.hibernate.type.range.Range; +import io.hypersistence.utils.hibernate.type.range.Range; import io.restassured.RestAssured; import io.restassured.http.ContentType; import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.context.Context; -import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRepository; import net.hostsharing.hsadminng.hs.office.test.ContextBasedTestWithCleanup; import net.hostsharing.test.Accepts; @@ -24,6 +23,7 @@ import jakarta.persistence.PersistenceContext; import java.time.LocalDate; import java.util.UUID; +import static net.hostsharing.hsadminng.hs.office.membership.HsOfficeReasonForTermination.CANCELLATION; import static net.hostsharing.hsadminng.hs.office.membership.HsOfficeReasonForTermination.NONE; import static net.hostsharing.test.IsValidUuidMatcher.isUuidValid; import static net.hostsharing.test.JsonMatcher.lenientlyEquals; @@ -51,9 +51,6 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle @Autowired HsOfficeMembershipRepository membershipRepo; - @Autowired - HsOfficeDebitorRepository debitorRepo; - @Autowired HsOfficePartnerRepository partnerRepo; @@ -82,8 +79,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle .body("", lenientlyEquals(""" [ { - "partner": { "person": { "tradeName": "First GmbH" } }, - "mainDebitor": { "debitorNumber": 1000111 }, + "partner": { "partnerNumber": 10001 }, "memberNumber": 1000101, "memberNumberSuffix": "01", "validFrom": "2022-10-01", @@ -91,8 +87,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle "reasonForTermination": "NONE" }, { - "partner": { "person": { "tradeName": "Second e.K." } }, - "mainDebitor": { "debitorNumber": 1000212 }, + "partner": { "partnerNumber": 10002 }, "memberNumber": 1000202, "memberNumberSuffix": "02", "validFrom": "2022-10-01", @@ -100,8 +95,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle "reasonForTermination": "NONE" }, { - "partner": { "person": { "tradeName": "Third OHG" } }, - "mainDebitor": { "debitorNumber": 1000313 }, + "partner": { "partnerNumber": 10003 }, "memberNumber": 1000303, "memberNumberSuffix": "03", "validFrom": "2022-10-01", @@ -132,8 +126,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle .body("", lenientlyEquals(""" [ { - "partner": { "person": { "tradeName": "First GmbH" } }, - "mainDebitor": { "debitorNumber": 1000111 }, + "partner": { "partnerNumber": 10001 }, "memberNumber": 1000101, "memberNumberSuffix": "01", "validFrom": "2022-10-01", @@ -161,8 +154,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle .body("", lenientlyEquals(""" [ { - "partner": { "person": { "tradeName": "Second e.K." } }, - "mainDebitor": { "debitorNumber": 1000212 }, + "partner": { "partnerNumber": 10002 }, "memberNumber": 1000202, "memberNumberSuffix": "02", "validFrom": "2022-10-01", @@ -184,7 +176,6 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle context.define("superuser-alex@hostsharing.net"); final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("Third").get(0); - final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike("Third").get(0); final var givenMemberSuffix = TEMP_MEMBER_NUMBER_SUFFIX; final var expectedMemberNumber = Integer.parseInt(givenPartner.getPartnerNumber() + TEMP_MEMBER_NUMBER_SUFFIX); @@ -195,12 +186,11 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle .body(""" { "partnerUuid": "%s", - "mainDebitorUuid": "%s", "memberNumberSuffix": "%s", "validFrom": "2022-10-13", "membershipFeeBillable": "true" } - """.formatted(givenPartner.getUuid(), givenDebitor.getUuid(), givenMemberSuffix)) + """.formatted(givenPartner.getUuid(), givenMemberSuffix)) .port(port) .when() .post("http://localhost/api/hs/office/memberships") @@ -208,9 +198,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle .statusCode(201) .contentType(ContentType.JSON) .body("uuid", isUuidValid()) - .body("mainDebitor.debitorNumber", is(givenDebitor.getDebitorNumber())) - .body("mainDebitor.debitorNumberSuffix", is((int) givenDebitor.getDebitorNumberSuffix())) - .body("partner.person.tradeName", is("Third OHG")) + .body("partner.partnerNumber", is(10003)) .body("memberNumber", is(expectedMemberNumber)) .body("memberNumberSuffix", is(givenMemberSuffix)) .body("validFrom", is("2022-10-13")) @@ -246,8 +234,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle .contentType("application/json") .body("", lenientlyEquals(""" { - "partner": { "person": { "tradeName": "First GmbH" } }, - "mainDebitor": { "debitorNumber": 1000111 }, + "partner": { "partnerNumber": 10001 }, "memberNumber": 1000101, "memberNumberSuffix": "01", "validFrom": "2022-10-01", @@ -275,14 +262,14 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle @Test @Accepts({ "Membership:X(Access Control)" }) - void debitorAgentUser_canGetRelatedMembership() { + void parnerRelAgent_canGetRelatedMembership() { context.define("superuser-alex@hostsharing.net"); final var givenMembershipUuid = membershipRepo.findMembershipByMemberNumber(1000303).getUuid(); RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") - .header("assumed-roles", "hs_office_debitor#1000313:ThirdOHG-thirdcontact.agent") + .header("assumed-roles", "hs_office_relation#HostsharingeG-with-PARTNER-ThirdOHG:AGENT") .port(port) .when() .get("http://localhost/api/hs/office/memberships/" + givenMembershipUuid) @@ -291,11 +278,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle .contentType("application/json") .body("", lenientlyEquals(""" { - "partner": { "person": { "tradeName": "Third OHG" } }, - "mainDebitor": { - "debitorNumber": 1000313, - "billingContact": { "label": "third contact" } - }, + "partner": { "partnerNumber": 10003 }, "memberNumber": 1000303, "memberNumberSuffix": "03", "validFrom": "2022-10-01", @@ -314,7 +297,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle void globalAdmin_canPatchValidToOfArbitraryMembership() { context.define("superuser-alex@hostsharing.net"); - final var givenMembership = givenSomeTemporaryMembershipBessler(); + final var givenMembership = givenSomeTemporaryMembershipBessler("First"); final var location = RestAssured // @formatter:off .given() @@ -333,10 +316,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle .statusCode(200) .contentType(ContentType.JSON) .body("uuid", isUuidValid()) - .body("partner.person.tradeName", is(givenMembership.getPartner().getPerson().getTradeName())) - .body("mainDebitor.debitorNumber", is(givenMembership.getMainDebitor().getDebitorNumber())) - .body("mainDebitor.debitorNumberSuffix", is((int) givenMembership.getMainDebitor().getDebitorNumberSuffix())) - .body("mainDebitor.debitorNumberSuffix", is((int) givenMembership.getMainDebitor().getDebitorNumberSuffix())) + .body("partner.partnerNumber", is(givenMembership.getPartner().getPartnerNumber())) .body("memberNumberSuffix", is(givenMembership.getMemberNumberSuffix())) .body("validFrom", is("2022-11-01")) .body("validTo", is("2023-12-31")) @@ -346,72 +326,31 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle // finally, the Membership is actually updated assertThat(membershipRepo.findByUuid(givenMembership.getUuid())).isPresent().get() .matches(mandate -> { - assertThat(mandate.getPartner().toShortString()).isEqualTo("LP First GmbH"); - assertThat(mandate.getMainDebitor().toString()).isEqualTo(givenMembership.getMainDebitor().toString()); + assertThat(mandate.getPartner().toShortString()).isEqualTo("P-10001"); assertThat(mandate.getMemberNumberSuffix()).isEqualTo(givenMembership.getMemberNumberSuffix()); assertThat(mandate.getValidity().asString()).isEqualTo("[2022-11-01,2024-01-01)"); - assertThat(mandate.getReasonForTermination()).isEqualTo(HsOfficeReasonForTermination.CANCELLATION); + assertThat(mandate.getReasonForTermination()).isEqualTo(CANCELLATION); return true; }); } @Test - void globalAdmin_canPatchMainDebitorOfArbitraryMembership() { + void partnerRelAdmin_canPatchValidityOfRelatedMembership() { - context.define("superuser-alex@hostsharing.net"); - final var givenMembership = givenSomeTemporaryMembershipBessler(); - final var givenNewMainDebitor = debitorRepo.findDebitorByDebitorNumber(1000313).get(0); + // given + final var givenPartnerAdmin = "hs_office_relation#HostsharingeG-with-PARTNER-FirstGmbH:ADMIN"; + context.define("superuser-alex@hostsharing.net", givenPartnerAdmin); + final var givenMembership = givenSomeTemporaryMembershipBessler("First"); + // when RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") + .header("assumed-roles", givenPartnerAdmin) .contentType(ContentType.JSON) .body(""" { - "mainDebitorUuid": "%s" - } - """.formatted(givenNewMainDebitor.getUuid())) - .port(port) - .when() - .patch("http://localhost/api/hs/office/memberships/" + givenMembership.getUuid()) - .then().log().all().assertThat() - .statusCode(200) - .contentType(ContentType.JSON) - .body("uuid", isUuidValid()) - .body("partner.person.tradeName", is(givenMembership.getPartner().getPerson().getTradeName())) - .body("mainDebitor.debitorNumber", is(1000313)) - .body("memberNumberSuffix", is(givenMembership.getMemberNumberSuffix())) - .body("validFrom", is("2022-11-01")) - .body("validTo", nullValue()) - .body("reasonForTermination", is("NONE")); - // @formatter:on - - // finally, the Membership is actually updated - assertThat(membershipRepo.findByUuid(givenMembership.getUuid())).isPresent().get() - .matches(mandate -> { - assertThat(mandate.getPartner().toShortString()).isEqualTo("LP First GmbH"); - assertThat(mandate.getMainDebitor().toString()).isEqualTo(givenMembership.getMainDebitor().toString()); - assertThat(mandate.getMemberNumberSuffix()).isEqualTo(givenMembership.getMemberNumberSuffix()); - assertThat(mandate.getValidity().asString()).isEqualTo("[2022-11-01,)"); - assertThat(mandate.getReasonForTermination()).isEqualTo(NONE); - return true; - }); - } - - @Test - void partnerAgent_canViewButNotPatchValidityOfRelatedMembership() { - - context.define("superuser-alex@hostsharing.net", "hs_office_partner#10001:FirstGmbH-firstcontact.agent"); - final var givenMembership = givenSomeTemporaryMembershipBessler(); - - final var location = RestAssured // @formatter:off - .given() - .header("current-user", "superuser-alex@hostsharing.net") - .header("assumed-roles", "hs_office_partner#10001:FirstGmbH-firstcontact.agent") - .contentType(ContentType.JSON) - .body(""" - { - "validTo": "2023-12-31", + "validTo": "2024-01-01", "reasonForTermination": "CANCELLATION" } """) @@ -419,13 +358,13 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle .when() .patch("http://localhost/api/hs/office/memberships/" + givenMembership.getUuid()) .then().assertThat() - .statusCode(403); // @formatter:on + .statusCode(200); // @formatter:on // finally, the Membership is actually updated assertThat(membershipRepo.findByUuid(givenMembership.getUuid())).isPresent().get() .matches(mandate -> { - assertThat(mandate.getValidity().asString()).isEqualTo("[2022-11-01,)"); - assertThat(mandate.getReasonForTermination()).isEqualTo(NONE); + assertThat(mandate.getValidity().asString()).isEqualTo("[2022-11-01,2024-01-02)"); + assertThat(mandate.getReasonForTermination()).isEqualTo(CANCELLATION); return true; }); } @@ -438,7 +377,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle @Test void globalAdmin_canDeleteArbitraryMembership() { context.define("superuser-alex@hostsharing.net"); - final var givenMembership = givenSomeTemporaryMembershipBessler(); + final var givenMembership = givenSomeTemporaryMembershipBessler("First"); RestAssured // @formatter:off .given() @@ -457,12 +396,12 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle @Accepts({ "Membership:X(Access Control)" }) void partnerAgentUser_canNotDeleteRelatedMembership() { context.define("superuser-alex@hostsharing.net"); - final var givenMembership = givenSomeTemporaryMembershipBessler(); + final var givenMembership = givenSomeTemporaryMembershipBessler("First"); RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") - .header("assumed-roles", "hs_office_partner#FirstGmbH-firstcontact.agent") + .header("assumed-roles", "hs_office_relation#HostsharingeG-with-PARTNER-FirstGmbH:AGENT") .port(port) .when() .delete("http://localhost/api/hs/office/memberships/" + givenMembership.getUuid()) @@ -477,7 +416,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle @Accepts({ "Membership:X(Access Control)" }) void normalUser_canNotDeleteUnrelatedMembership() { context.define("superuser-alex@hostsharing.net"); - final var givenMembership = givenSomeTemporaryMembershipBessler(); + final var givenMembership = givenSomeTemporaryMembershipBessler("First"); RestAssured // @formatter:off .given() @@ -493,15 +432,13 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle } } - private HsOfficeMembershipEntity givenSomeTemporaryMembershipBessler() { + private HsOfficeMembershipEntity givenSomeTemporaryMembershipBessler(final String partnerName) { return jpaAttempt.transacted(() -> { context.define("superuser-alex@hostsharing.net"); - final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike("First").get(0); - final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("First").get(0); + final var givenPartner = partnerRepo.findPartnerByOptionalNameLike(partnerName).get(0); final var newMembership = HsOfficeMembershipEntity.builder() .uuid(UUID.randomUUID()) .partner(givenPartner) - .mainDebitor(givenDebitor) .memberNumberSuffix(TEMP_MEMBER_NUMBER_SUFFIX) .validity(Range.closedInfinite(LocalDate.parse("2022-11-01"))) .reasonForTermination(NONE) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipControllerRestTest.java index 63ea7306..bcd7e9ab 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipControllerRestTest.java @@ -2,7 +2,6 @@ package net.hostsharing.hsadminng.hs.office.membership; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionRepository; -import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity; import net.hostsharing.hsadminng.mapper.Mapper; import org.junit.jupiter.api.BeforeEach; @@ -28,7 +27,6 @@ import java.util.UUID; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -76,7 +74,6 @@ public class HsOfficeMembershipControllerRestTest { .content(""" { "partnerUuid": null, - "mainDebitorUuid": "%s", "memberNumberSuffix": "01", "validFrom": "2022-10-13", "membershipFeeBillable": "true" @@ -91,40 +88,12 @@ public class HsOfficeMembershipControllerRestTest { .andExpect(jsonPath("message", is("[partnerUuid must not be null but is \"null\"]"))); } - @Test - void respondBadRequest_ifDebitorUuidIsMissing() throws Exception { - - // when - mockMvc.perform(MockMvcRequestBuilders - .post("/api/hs/office/memberships") - .header("current-user", "superuser-alex@hostsharing.net") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - { - "partnerUuid": "%s", - "mainDebitorUuid": null, - "memberNumberSuffix": "01", - "validFrom": "2022-10-13", - "membershipFeeBillable": "true" - } - """.formatted(UUID.randomUUID())) - .accept(MediaType.APPLICATION_JSON)) - - // then - .andExpect(status().is4xxClientError()) - .andExpect(jsonPath("statusCode", is(400))) - .andExpect(jsonPath("statusPhrase", is("Bad Request"))) - .andExpect(jsonPath("message", is("[mainDebitorUuid must not be null but is \"null\"]"))); - } - @Test void respondBadRequest_ifAnyGivenPartnerUuidCannotBeFound() throws Exception { // given final var givenPartnerUuid = UUID.randomUUID(); - final var givenMainDebitorUuid = UUID.randomUUID(); when(em.find(HsOfficePartnerEntity.class, givenPartnerUuid)).thenReturn(null); - when(em.find(HsOfficeDebitorEntity.class, givenMainDebitorUuid)).thenReturn(mock(HsOfficeDebitorEntity.class)); // when mockMvc.perform(MockMvcRequestBuilders @@ -134,12 +103,11 @@ public class HsOfficeMembershipControllerRestTest { .content(""" { "partnerUuid": "%s", - "mainDebitorUuid": "%s", "memberNumberSuffix": "01", "validFrom": "2022-10-13", "membershipFeeBillable": "true" } - """.formatted(givenPartnerUuid, givenMainDebitorUuid)) + """.formatted(givenPartnerUuid)) .accept(MediaType.APPLICATION_JSON)) // then @@ -149,38 +117,6 @@ public class HsOfficeMembershipControllerRestTest { .andExpect(jsonPath("message", is("Unable to find Partner with uuid " + givenPartnerUuid))); } - @Test - void respondBadRequest_ifAnyGivenDebitorUuidCannotBeFound() throws Exception { - - // given - final var givenPartnerUuid = UUID.randomUUID(); - final var givenMainDebitorUuid = UUID.randomUUID(); - when(em.find(HsOfficePartnerEntity.class, givenPartnerUuid)).thenReturn(mock(HsOfficePartnerEntity.class)); - when(em.find(HsOfficeDebitorEntity.class, givenMainDebitorUuid)).thenReturn(null); - - // when - mockMvc.perform(MockMvcRequestBuilders - .post("/api/hs/office/memberships") - .header("current-user", "superuser-alex@hostsharing.net") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - { - "partnerUuid": "%s", - "mainDebitorUuid": "%s", - "memberNumberSuffix": "01", - "validFrom": "2022-10-13", - "membershipFeeBillable": "true" - } - """.formatted(givenPartnerUuid, givenMainDebitorUuid)) - .accept(MediaType.APPLICATION_JSON)) - - // then - .andExpect(status().is4xxClientError()) - .andExpect(jsonPath("statusCode", is(400))) - .andExpect(jsonPath("statusPhrase", is("Bad Request"))) - .andExpect(jsonPath("message", is("Unable to find Debitor with uuid " + givenMainDebitorUuid))); - } - @ParameterizedTest @EnumSource(InvalidMemberSuffixVariants.class) void respondBadRequest_ifMemberNumberSuffixIsInvalid(final InvalidMemberSuffixVariants testCase) throws Exception { @@ -193,12 +129,11 @@ public class HsOfficeMembershipControllerRestTest { .content(""" { "partnerUuid": "%s", - "mainDebitorUuid": "%s", %s "validFrom": "2022-10-13", "membershipFeeBillable": "true" } - """.formatted(UUID.randomUUID(), UUID.randomUUID(), testCase.memberNumberSuffixEntry)) + """.formatted(UUID.randomUUID(), testCase.memberNumberSuffixEntry)) .accept(MediaType.APPLICATION_JSON)) // then diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityPatcherUnitTest.java index ee4944c1..ddad360e 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityPatcherUnitTest.java @@ -1,6 +1,6 @@ package net.hostsharing.hsadminng.hs.office.membership; -import com.vladmihalcea.hibernate.type.range.Range; +import io.hypersistence.utils.hibernate.type.range.Range; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipPatchResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeReasonForTerminationResource; @@ -17,7 +17,6 @@ import java.time.LocalDate; import java.util.UUID; import java.util.stream.Stream; -import static net.hostsharing.hsadminng.hs.office.debitor.TestHsOfficeDebitor.TEST_DEBITOR; import static net.hostsharing.hsadminng.hs.office.partner.TestHsOfficePartner.TEST_PARTNER; import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; import static org.mockito.ArgumentMatchers.any; @@ -32,7 +31,6 @@ class HsOfficeMembershipEntityPatcherUnitTest extends PatchUnitTestBase< > { private static final UUID INITIAL_MEMBERSHIP_UUID = UUID.randomUUID(); - private static final UUID PATCHED_MAIN_DEBITOR_UUID = UUID.randomUUID(); private static final LocalDate GIVEN_VALID_FROM = LocalDate.parse("2020-04-15"); private static final LocalDate PATCHED_VALID_TO = LocalDate.parse("2022-12-31"); @@ -56,7 +54,6 @@ class HsOfficeMembershipEntityPatcherUnitTest extends PatchUnitTestBase< protected HsOfficeMembershipEntity newInitialEntity() { final var entity = new HsOfficeMembershipEntity(); entity.setUuid(INITIAL_MEMBERSHIP_UUID); - entity.setMainDebitor(TEST_DEBITOR); entity.setPartner(TEST_PARTNER); entity.setValidity(Range.closedInfinite(GIVEN_VALID_FROM)); entity.setMembershipFeeBillable(GIVEN_MEMBERSHIP_FEE_BILLABLE); @@ -70,19 +67,12 @@ class HsOfficeMembershipEntityPatcherUnitTest extends PatchUnitTestBase< @Override protected HsOfficeMembershipEntityPatcher createPatcher(final HsOfficeMembershipEntity membership) { - return new HsOfficeMembershipEntityPatcher(em, mapper, membership); + return new HsOfficeMembershipEntityPatcher(mapper, membership); } @Override protected Stream propertyTestDescriptors() { return Stream.of( - new JsonNullableProperty<>( - "debitor", - HsOfficeMembershipPatchResource::setMainDebitorUuid, - PATCHED_MAIN_DEBITOR_UUID, - HsOfficeMembershipEntity::setMainDebitor, - newDebitor(PATCHED_MAIN_DEBITOR_UUID)) - .notNullable(), new JsonNullableProperty<>( "valid", HsOfficeMembershipPatchResource::setValidTo, @@ -102,10 +92,4 @@ class HsOfficeMembershipEntityPatcherUnitTest extends PatchUnitTestBase< HsOfficeMembershipEntity::setMembershipFeeBillable) ); } - - private static HsOfficeDebitorEntity newDebitor(final UUID uuid) { - final var newDebitor = new HsOfficeDebitorEntity(); - newDebitor.setUuid(uuid); - return newDebitor; - } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityUnitTest.java index b1815755..ef47eaa0 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityUnitTest.java @@ -1,6 +1,6 @@ package net.hostsharing.hsadminng.hs.office.membership; -import com.vladmihalcea.hibernate.type.range.Range; +import io.hypersistence.utils.hibernate.type.range.Range; import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity; import org.junit.jupiter.api.Test; @@ -9,7 +9,6 @@ import java.lang.reflect.InvocationTargetException; import java.time.LocalDate; import java.util.Arrays; -import static net.hostsharing.hsadminng.hs.office.debitor.TestHsOfficeDebitor.TEST_DEBITOR; import static net.hostsharing.hsadminng.hs.office.partner.TestHsOfficePartner.TEST_PARTNER; import static org.assertj.core.api.Assertions.assertThat; @@ -20,14 +19,13 @@ class HsOfficeMembershipEntityUnitTest { final HsOfficeMembershipEntity givenMembership = HsOfficeMembershipEntity.builder() .memberNumberSuffix("01") .partner(TEST_PARTNER) - .mainDebitor(TEST_DEBITOR) .validity(Range.closedInfinite(GIVEN_VALID_FROM)) .build(); @Test void toStringContainsAllProps() { final var result = givenMembership.toString(); - assertThat(result).isEqualTo("Membership(M-1000101, LP Test Ltd., D-1000100, [2020-01-01,))"); + assertThat(result).isEqualTo("Membership(M-1000101, P-10001, [2020-01-01,))"); } @Test diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java index 6a0cd485..633278a0 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java @@ -1,6 +1,6 @@ package net.hostsharing.hsadminng.hs.office.membership; -import com.vladmihalcea.hibernate.type.range.Range; +import io.hypersistence.utils.hibernate.type.range.Range; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRepository; @@ -28,6 +28,7 @@ import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distin import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.distinctRoleNamesOf; import static net.hostsharing.test.JpaAttempt.attempt; import static org.assertj.core.api.Assertions.assertThat; + @DataJpaTest @Import( { Context.class, JpaAttempt.class }) class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCleanup { @@ -65,14 +66,12 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl context("superuser-alex@hostsharing.net"); final var count = membershipRepo.count(); final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("First").get(0); - final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike("First").get(0); // when final var result = attempt(em, () -> { final var newMembership = HsOfficeMembershipEntity.builder() .memberNumberSuffix("11") .partner(givenPartner) - .mainDebitor(givenDebitor) .validity(Range.closedInfinite(LocalDate.parse("2020-01-01"))) .membershipFeeBillable(true) .build(); @@ -92,18 +91,15 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl context("superuser-alex@hostsharing.net"); final var initialRoleNames = distinctRoleNamesOf(rawRoleRepo.findAll()); final var initialGrantNames = distinctGrantDisplaysOf(rawGrantRepo.findAll()).stream() - .map(s -> s.replace("GmbH-firstcontact", "")) .map(s -> s.replace("hs_office_", "")) .toList(); // when attempt(em, () -> { final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("First").get(0); - final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike("First").get(0); final var newMembership = HsOfficeMembershipEntity.builder() .memberNumberSuffix("17") .partner(givenPartner) - .mainDebitor(givenDebitor) .validity(Range.closedInfinite(LocalDate.parse("2020-01-01"))) .membershipFeeBillable(true) .build(); @@ -114,45 +110,32 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl final var all = rawRoleRepo.findAll(); assertThat(distinctRoleNamesOf(all)).containsExactlyInAnyOrder(Array.from( initialRoleNames, - "hs_office_membership#1000117:FirstGmbH-firstcontact.admin", - "hs_office_membership#1000117:FirstGmbH-firstcontact.agent", - "hs_office_membership#1000117:FirstGmbH-firstcontact.guest", - "hs_office_membership#1000117:FirstGmbH-firstcontact.owner", - "hs_office_membership#1000117:FirstGmbH-firstcontact.tenant")); + "hs_office_membership#M-1000117:OWNER", + "hs_office_membership#M-1000117:ADMIN", + "hs_office_membership#M-1000117:AGENT")); assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())) - .map(s -> s.replace("GmbH-firstcontact", "")) .map(s -> s.replace("hs_office_", "")) .containsExactlyInAnyOrder(Array.fromFormatted( initialGrantNames, + // insert + "{ grant perm:membership#M-1000117:INSERT>coopassetstransaction to role:membership#M-1000117:ADMIN by system and assume }", + "{ grant perm:membership#M-1000117:INSERT>coopsharestransaction to role:membership#M-1000117:ADMIN by system and assume }", // owner - "{ grant perm * on membership#1000117:First to role membership#1000117:First.owner by system and assume }", - "{ grant role membership#1000117:First.owner to role global#global.admin by system and assume }", + "{ grant perm:membership#M-1000117:DELETE to role:membership#M-1000117:ADMIN by system and assume }", + "{ grant role:membership#M-1000117:OWNER to user:superuser-alex@hostsharing.net by membership#M-1000117:OWNER and assume }", // admin - "{ grant perm edit on membership#1000117:First to role membership#1000117:First.admin by system and assume }", - "{ grant role membership#1000117:First.admin to role membership#1000117:First.owner by system and assume }", + "{ grant perm:membership#M-1000117:UPDATE to role:membership#M-1000117:ADMIN by system and assume }", + "{ grant role:membership#M-1000117:ADMIN to role:membership#M-1000117:OWNER by system and assume }", + "{ grant role:membership#M-1000117:ADMIN to role:relation#HostsharingeG-with-PARTNER-FirstGmbH:ADMIN by system and assume }", // agent - "{ grant role membership#1000117:First.agent to role membership#1000117:First.admin by system and assume }", - "{ grant role partner#10001:First.tenant to role membership#1000117:First.agent by system and assume }", - "{ grant role membership#1000117:First.agent to role debitor#1000111:First.admin by system and assume }", - "{ grant role membership#1000117:First.agent to role partner#10001:First.admin by system and assume }", - "{ grant role debitor#1000111:First.tenant to role membership#1000117:First.agent by system and assume }", + "{ grant perm:membership#M-1000117:SELECT to role:membership#M-1000117:AGENT by system and assume }", + "{ grant role:membership#M-1000117:AGENT to role:membership#M-1000117:ADMIN by system and assume }", - // tenant - "{ grant role membership#1000117:First.tenant to role membership#1000117:First.agent by system and assume }", - "{ grant role partner#10001:First.guest to role membership#1000117:First.tenant by system and assume }", - "{ grant role debitor#1000111:First.guest to role membership#1000117:First.tenant by system and assume }", - "{ grant role membership#1000117:First.tenant to role debitor#1000111:First.agent by system and assume }", - - "{ grant role membership#1000117:First.tenant to role partner#10001:First.agent by system and assume }", - - // guest - "{ grant perm view on membership#1000117:First to role membership#1000117:First.guest by system and assume }", - "{ grant role membership#1000117:First.guest to role membership#1000117:First.tenant by system and assume }", - "{ grant role membership#1000117:First.guest to role partner#10001:First.tenant by system and assume }", - "{ grant role membership#1000117:First.guest to role debitor#1000111:First.tenant by system and assume }", + "{ grant role:membership#M-1000117:AGENT to role:relation#HostsharingeG-with-PARTNER-FirstGmbH:AGENT by system and assume }", + "{ grant role:relation#HostsharingeG-with-PARTNER-FirstGmbH:TENANT to role:membership#M-1000117:AGENT by system and assume }", null)); } @@ -177,9 +160,9 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl // then exactlyTheseMembershipsAreReturned( result, - "Membership(M-1000101, LP First GmbH, D-1000111, [2022-10-01,), NONE)", - "Membership(M-1000202, LP Second e.K., D-1000212, [2022-10-01,), NONE)", - "Membership(M-1000303, IF Third OHG, D-1000313, [2022-10-01,), NONE)"); + "Membership(M-1000101, P-10001, [2022-10-01,), NONE)", + "Membership(M-1000202, P-10002, [2022-10-01,), NONE)", + "Membership(M-1000303, P-10003, [2022-10-01,), NONE)"); } @Test @@ -193,7 +176,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl // then exactlyTheseMembershipsAreReturned(result, - "Membership(M-1000101, LP First GmbH, D-1000111, [2022-10-01,), NONE)"); + "Membership(M-1000101, P-10001, [2022-10-01,), NONE)"); } @Test @@ -208,7 +191,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl assertThat(result) .isNotNull() .extracting(Object::toString) - .isEqualTo("Membership(M-1000202, LP Second e.K., D-1000212, [2022-10-01,), NONE)"); + .isEqualTo("Membership(M-1000202, P-10002, [2022-10-01,), NONE)"); } } @@ -219,10 +202,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl public void globalAdmin_canUpdateValidityOfArbitraryMembership() { // given context("superuser-alex@hostsharing.net"); - final var givenMembership = givenSomeTemporaryMembership("First", "First", "11"); - assertThatMembershipIsVisibleForUserWithRole( - givenMembership, - "hs_office_debitor#1000111:FirstGmbH-firstcontact.admin"); + final var givenMembership = givenSomeTemporaryMembership("First", "11"); assertThatMembershipExistsAndIsAccessibleToCurrentContext(givenMembership); final var newValidityEnd = LocalDate.now(); @@ -243,21 +223,22 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl } @Test - public void debitorAdmin_canViewButNotUpdateRelatedMembership() { + public void membershipAgent_canViewButNotUpdateRelatedMembership() { // given context("superuser-alex@hostsharing.net"); - final var givenMembership = givenSomeTemporaryMembership("First", "First", "13"); - assertThatMembershipIsVisibleForUserWithRole( - givenMembership, - "hs_office_debitor#1000111:FirstGmbH-firstcontact.admin"); + final var givenMembership = givenSomeTemporaryMembership("First", "13"); assertThatMembershipExistsAndIsAccessibleToCurrentContext(givenMembership); + assertThatMembershipIsVisibleForRole( + givenMembership, + "hs_office_membership#M-1000113:AGENT"); final var newValidityEnd = LocalDate.now(); // when final var result = jpaAttempt.transacted(() -> { - context("superuser-alex@hostsharing.net", "hs_office_debitor#1000111:FirstGmbH-firstcontact.admin"); - givenMembership.setValidity(Range.closedOpen( - givenMembership.getValidity().lower(), newValidityEnd)); + // TODO: we should test with debitor- and partner-admin as well + context("superuser-alex@hostsharing.net", "hs_office_membership#M-1000113:AGENT"); + givenMembership.setValidity( + Range.closedOpen(givenMembership.getValidity().lower(), newValidityEnd)); return membershipRepo.save(givenMembership); }); @@ -272,7 +253,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl .extracting(Object::toString).isEqualTo(saved.toString()); } - private void assertThatMembershipIsVisibleForUserWithRole( + private void assertThatMembershipIsVisibleForRole( final HsOfficeMembershipEntity entity, final String assumedRoles) { jpaAttempt.transacted(() -> { @@ -280,16 +261,6 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl assertThatMembershipExistsAndIsAccessibleToCurrentContext(entity); }).assertSuccessful(); } - - private void assertThatMembershipIsNotVisibleForUserWithRole( - final HsOfficeMembershipEntity entity, - final String assumedRoles) { - jpaAttempt.transacted(() -> { - context("superuser-alex@hostsharing.net", assumedRoles); - final var found = membershipRepo.findByUuid(entity.getUuid()); - assertThat(found).isEmpty(); - }).assertSuccessful(); - } } @Nested @@ -299,7 +270,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl public void globalAdmin_withoutAssumedRole_canDeleteAnyMembership() { // given context("superuser-alex@hostsharing.net", null); - final var givenMembership = givenSomeTemporaryMembership("First", "Second", "12"); + final var givenMembership = givenSomeTemporaryMembership("First", "12"); // when final var result = jpaAttempt.transacted(() -> { @@ -316,14 +287,14 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl } @Test - public void nonGlobalAdmin_canNotDeleteTheirRelatedMembership() { + public void partnerRelationAgent_canNotDeleteTheirRelatedMembership() { // given context("superuser-alex@hostsharing.net"); - final var givenMembership = givenSomeTemporaryMembership("First", "Third", "14"); + final var givenMembership = givenSomeTemporaryMembership("First", "14"); // when final var result = jpaAttempt.transacted(() -> { - context("superuser-alex@hostsharing.net", "hs_office_debitor#1000313:ThirdOHG-thirdcontact.admin"); + context("superuser-alex@hostsharing.net", "hs_office_relation#HostsharingeG-with-PARTNER-FirstGmbH:AGENT"); assertThat(membershipRepo.findByUuid(givenMembership.getUuid())).isPresent(); membershipRepo.deleteByUuid(givenMembership.getUuid()); @@ -345,11 +316,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl context("superuser-alex@hostsharing.net"); final var initialRoleNames = Array.from(distinctRoleNamesOf(rawRoleRepo.findAll())); final var initialGrantNames = Array.from(distinctGrantDisplaysOf(rawGrantRepo.findAll())); - final var givenMembership = givenSomeTemporaryMembership("First", "First", "15"); - assertThat(distinctRoleNamesOf(rawRoleRepo.findAll()).size()).as("precondition failed: unexpected number of roles created") - .isEqualTo(initialRoleNames.length + 5); - assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll()).size()).as("precondition failed: unexpected number of grants created") - .isEqualTo(initialGrantNames.length + 18); + final var givenMembership = givenSomeTemporaryMembership("First", "15"); // when final var result = jpaAttempt.transacted(() -> { @@ -379,19 +346,18 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl // then assertThat(customerLogEntries).map(Arrays::toString).contains( - "[creating Membership test-data FirstGmbH11, hs_office_membership, INSERT]", - "[creating Membership test-data Seconde.K.12, hs_office_membership, INSERT]"); + "[creating Membership test-data P-10001M-...01, hs_office_membership, INSERT]", + "[creating Membership test-data P-10002M-...02, hs_office_membership, INSERT]", + "[creating Membership test-data P-10003M-...03, hs_office_membership, INSERT]"); } - private HsOfficeMembershipEntity givenSomeTemporaryMembership(final String partnerTradeName, final String debitorName, final String memberNumberSuffix) { + private HsOfficeMembershipEntity givenSomeTemporaryMembership(final String partnerTradeName, final String memberNumberSuffix) { return jpaAttempt.transacted(() -> { context("superuser-alex@hostsharing.net"); final var givenPartner = partnerRepo.findPartnerByOptionalNameLike(partnerTradeName).get(0); - final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike(debitorName).get(0); final var newMembership = HsOfficeMembershipEntity.builder() .memberNumberSuffix(memberNumberSuffix) .partner(givenPartner) - .mainDebitor(givenDebitor) .validity(Range.closedInfinite(LocalDate.parse("2020-01-01"))) .membershipFeeBillable(true) .build(); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/TestHsMembership.java b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/TestHsMembership.java index ff50eb58..857e9369 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/TestHsMembership.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/TestHsMembership.java @@ -1,6 +1,6 @@ package net.hostsharing.hsadminng.hs.office.membership; -import com.vladmihalcea.hibernate.type.range.Range; +import io.hypersistence.utils.hibernate.type.range.Range; import java.time.LocalDate; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java b/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java index 325317b2..8da1f12f 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java @@ -18,10 +18,10 @@ 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.person.HsOfficePersonType; -import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity; -import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipType; +import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity; +import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType; import net.hostsharing.hsadminng.hs.office.sepamandate.HsOfficeSepaMandateEntity; -import net.hostsharing.hsadminng.persistence.HasUuid; +import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.test.JpaAttempt; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; @@ -106,7 +106,7 @@ import static org.assertj.core.api.Fail.fail; @Tag("import") @DataJpaTest(properties = { "spring.datasource.url=${HSADMINNG_POSTGRES_JDBC_URL:jdbc:tc:postgresql:15.5-bookworm:///spring_boot_testcontainers}", - "spring.datasource.username=${HSADMINNG_POSTGRES_ADMIN_USERNAME:admin}", + "spring.datasource.username=${HSADMINNG_POSTGRES_ADMIN_USERNAME:ADMIN}", "spring.datasource.password=${HSADMINNG_POSTGRES_ADMIN_PASSWORD:password}", "hsadminng.superuser=${HSADMINNG_SUPERUSER:superuser-alex@hostsharing.net}" }) @@ -127,7 +127,7 @@ public class ImportOfficeData extends ContextBasedTest { new String[]{"partner", "vip-contact", "ex-partner", "billing", "contractual", "operation"}, SUBSCRIBER_ROLES); - static int relationshipId = 2000000; + static int relationId = 2000000; @Value("${spring.datasource.url}") private String jdbcUrl; @@ -144,7 +144,7 @@ public class ImportOfficeData extends ContextBasedTest { private static Map debitors = new WriteOnceMap<>(); private static Map memberships = new WriteOnceMap<>(); - private static Map relationships = new WriteOnceMap<>(); + private static Map relations = new WriteOnceMap<>(); private static Map sepaMandates = new WriteOnceMap<>(); private static Map bankAccounts = new WriteOnceMap<>(); private static Map coopShares = new WriteOnceMap<>(); @@ -175,33 +175,33 @@ public class ImportOfficeData extends ContextBasedTest { } @Test - @Order(1011) + @Order(1019) void verifyBusinessPartners() { assumeThatWeAreImportingControlledTestData(); // no contacts yet => mostly null values assertThat(toFormattedString(partners)).isEqualToIgnoringWhitespace(""" { - 17=partner(null null, null), - 20=partner(null null, null), - 22=partner(null null, null), - 99=partner(null null, null) + 17=partner(P-10017: null null, null), + 20=partner(P-10020: null null, null), + 22=partner(P-11022: null null, null), + 99=partner(P-19999: null null, null) } """); assertThat(toFormattedString(contacts)).isEqualTo("{}"); assertThat(toFormattedString(debitors)).isEqualToIgnoringWhitespace(""" { - 17=debitor(D-1001700: null null, null: mih), - 20=debitor(D-1002000: null null, null: xyz), - 22=debitor(D-1102200: null null, null: xxx), - 99=debitor(D-1999900: null null, null: zzz) + 17=debitor(D-1001700: rel(anchor='null null, null', type='DEBITOR'), mih), + 20=debitor(D-1002000: rel(anchor='null null, null', type='DEBITOR'), xyz), + 22=debitor(D-1102200: rel(anchor='null null, null', type='DEBITOR'), xxx), + 99=debitor(D-1999900: rel(anchor='null null, null', type='DEBITOR'), zzz) } """); assertThat(toFormattedString(memberships)).isEqualToIgnoringWhitespace(""" { - 17=Membership(M-1001700, null null, null, D-1001700, [2000-12-06,), NONE), - 20=Membership(M-1002000, null null, null, D-1002000, [2000-12-06,2016-01-01), UNKNOWN), - 22=Membership(M-1102200, null null, null, D-1102200, [2021-04-01,), NONE) + 17=Membership(M-1001700, P-10017, [2000-12-06,), NONE), + 20=Membership(M-1002000, P-10020, [2000-12-06,2016-01-01), UNKNOWN), + 22=Membership(M-1102200, P-11022, [2021-04-01,), NONE) } """); } @@ -220,15 +220,32 @@ public class ImportOfficeData extends ContextBasedTest { @Test @Order(1021) + void buildDebitorRelations() { + debitors.forEach( (id, debitor) -> { + final var debitorRel = HsOfficeRelationEntity.builder() + .type(HsOfficeRelationType.DEBITOR) + .anchor(debitor.getPartner().getPartnerRel().getHolder()) + .holder(debitor.getPartner().getPartnerRel().getHolder()) // just 1 debitor/partner in legacy hsadmin + // FIXME .contact() + .build(); + if (debitorRel.getAnchor() != null && debitorRel.getHolder() != null && + debitorRel.getContact() != null ) { + relations.put(relationId++, debitorRel); + } + }); + } + + @Test + @Order(1029) void verifyContacts() { assumeThatWeAreImportingControlledTestData(); assertThat(toFormattedString(partners)).isEqualToIgnoringWhitespace(""" { - 17=partner(NP Mellies, Michael: Herr Michael Mellies ), - 20=partner(LP JM GmbH: Herr Philip Meyer-Contract , JM GmbH), - 22=partner(?? Test PS: Petra Schmidt , Test PS), - 99=partner(null null, null) + 17=partner(P-10017: NP Mellies, Michael, Herr Michael Mellies ), + 20=partner(P-10020: LP JM GmbH, Herr Philip Meyer-Contract , JM GmbH), + 22=partner(P-11022: ?? Test PS, Petra Schmidt , Test PS), + 99=partner(P-19999: null null, null) } """); assertThat(toFormattedString(contacts)).isEqualToIgnoringWhitespace(""" @@ -258,39 +275,47 @@ public class ImportOfficeData extends ContextBasedTest { """); assertThat(toFormattedString(debitors)).isEqualToIgnoringWhitespace(""" { - 17=debitor(D-1001700: NP Mellies, Michael: mih), - 20=debitor(D-1002000: LP JM GmbH: xyz), - 22=debitor(D-1102200: ?? Test PS: xxx), - 99=debitor(D-1999900: null null, null: zzz) + 17=debitor(D-1001700: rel(anchor='NP Mellies, Michael', type='DEBITOR', holder='NP Mellies, Michael'), mih), + 20=debitor(D-1002000: rel(anchor='LP JM GmbH', type='DEBITOR', holder='LP JM GmbH'), xyz), + 22=debitor(D-1102200: rel(anchor='?? Test PS', type='DEBITOR', holder='?? Test PS'), xxx), + 99=debitor(D-1999900: rel(anchor='null null, null', type='DEBITOR'), zzz) } """); assertThat(toFormattedString(memberships)).isEqualToIgnoringWhitespace(""" { - 17=Membership(M-1001700, NP Mellies, Michael, D-1001700, [2000-12-06,), NONE), - 20=Membership(M-1002000, LP JM GmbH, D-1002000, [2000-12-06,2016-01-01), UNKNOWN), - 22=Membership(M-1102200, ?? Test PS, D-1102200, [2021-04-01,), NONE) + 17=Membership(M-1001700, P-10017, [2000-12-06,), NONE), + 20=Membership(M-1002000, P-10020, [2000-12-06,2016-01-01), UNKNOWN), + 22=Membership(M-1102200, P-11022, [2021-04-01,), NONE) } """); - assertThat(toFormattedString(relationships)).isEqualToIgnoringWhitespace(""" + assertThat(toFormattedString(relations)).isEqualToIgnoringWhitespace(""" { - 2000000=rel(relAnchor='LP Hostsharing eG', relType='PARTNER', relHolder='NP Mellies, Michael', contact='Herr Michael Mellies '), - 2000001=rel(relAnchor='LP Hostsharing eG', relType='PARTNER', relHolder='LP JM GmbH', contact='Herr Philip Meyer-Contract , JM GmbH'), - 2000002=rel(relAnchor='LP Hostsharing eG', relType='PARTNER', relHolder='?? Test PS', contact='Petra Schmidt , Test PS'), - 2000003=rel(relAnchor='LP Hostsharing eG', relType='PARTNER', relHolder='null null, null'), - 2000004=rel(relAnchor='NP Mellies, Michael', relType='OPERATIONS', relHolder='NP Mellies, Michael', contact='Herr Michael Mellies '), - 2000005=rel(relAnchor='NP Mellies, Michael', relType='REPRESENTATIVE', relHolder='NP Mellies, Michael', contact='Herr Michael Mellies '), - 2000006=rel(relAnchor='LP JM GmbH', relType='EX_PARTNER', relHolder='LP JM e.K.', contact='JM e.K.'), - 2000007=rel(relAnchor='LP JM GmbH', relType='OPERATIONS', relHolder='LP JM GmbH', contact='Herr Andrew Meyer-Operation , JM GmbH'), - 2000008=rel(relAnchor='LP JM GmbH', relType='VIP_CONTACT', relHolder='LP JM GmbH', contact='Herr Andrew Meyer-Operation , JM GmbH'), - 2000009=rel(relAnchor='LP JM GmbH', relType='SUBSCRIBER', relMark='operations-announce', relHolder='LP JM GmbH', contact='Herr Andrew Meyer-Operation , JM GmbH'), - 2000010=rel(relAnchor='LP JM GmbH', relType='REPRESENTATIVE', relHolder='LP JM GmbH', contact='Herr Philip Meyer-Contract , JM GmbH'), - 2000011=rel(relAnchor='LP JM GmbH', relType='SUBSCRIBER', relMark='members-announce', relHolder='LP JM GmbH', contact='Herr Philip Meyer-Contract , JM GmbH'), - 2000012=rel(relAnchor='LP JM GmbH', relType='SUBSCRIBER', relMark='customers-announce', relHolder='LP JM GmbH', contact='Herr Philip Meyer-Contract , JM GmbH'), - 2000013=rel(relAnchor='LP JM GmbH', relType='VIP_CONTACT', relHolder='LP JM GmbH', contact='Frau Tammy Meyer-VIP , JM GmbH'), - 2000014=rel(relAnchor='?? Test PS', relType='OPERATIONS', relHolder='?? Test PS', contact='Petra Schmidt , Test PS'), - 2000015=rel(relAnchor='?? Test PS', relType='REPRESENTATIVE', relHolder='?? Test PS', contact='Petra Schmidt , Test PS'), - 2000016=rel(relAnchor='NP Mellies, Michael', relType='SUBSCRIBER', relMark='operations-announce', relHolder='NP Fanninga, Frauke', contact='Frau Frauke Fanninga ') - } + 2000000=rel(anchor='LP Hostsharing eG', type='PARTNER', holder='NP Mellies, Michael', contact='Herr Michael Mellies '), + 2000001=rel(anchor='NP Mellies, Michael', type='DEBITOR', holder='NP Mellies, Michael', contact='Herr Michael Mellies '), + 2000002=rel(anchor='NP Mellies, Michael', type='DEBITOR', holder='NP Mellies, Michael', contact='Herr Michael Mellies '), + 2000003=rel(anchor='LP Hostsharing eG', type='PARTNER', holder='LP JM GmbH', contact='Herr Philip Meyer-Contract , JM GmbH'), + 2000004=rel(anchor='LP JM GmbH', type='DEBITOR', holder='LP JM GmbH', contact='Frau Dr. Jenny Meyer-Billing , JM GmbH'), + 2000005=rel(anchor='LP JM GmbH', type='DEBITOR', holder='LP JM GmbH', contact='Frau Dr. Jenny Meyer-Billing , JM GmbH'), + 2000006=rel(anchor='LP Hostsharing eG', type='PARTNER', holder='?? Test PS', contact='Petra Schmidt , Test PS'), + 2000007=rel(anchor='?? Test PS', type='DEBITOR', holder='?? Test PS', contact='Petra Schmidt , Test PS'), + 2000008=rel(anchor='?? Test PS', type='DEBITOR', holder='?? Test PS', contact='Petra Schmidt , Test PS'), + 2000009=rel(anchor='LP Hostsharing eG', type='PARTNER', holder='null null, null'), + 2000010=rel(anchor='null null, null', type='DEBITOR'), + 2000011=rel(anchor='null null, null', type='DEBITOR'), + 2000012=rel(anchor='NP Mellies, Michael', type='OPERATIONS', holder='NP Mellies, Michael', contact='Herr Michael Mellies '), + 2000013=rel(anchor='NP Mellies, Michael', type='REPRESENTATIVE', holder='NP Mellies, Michael', contact='Herr Michael Mellies '), + 2000014=rel(anchor='LP JM GmbH', type='EX_PARTNER', holder='LP JM e.K.', contact='JM e.K.'), + 2000015=rel(anchor='LP JM GmbH', type='OPERATIONS', holder='LP JM GmbH', contact='Herr Andrew Meyer-Operation , JM GmbH'), + 2000016=rel(anchor='LP JM GmbH', type='VIP_CONTACT', holder='LP JM GmbH', contact='Herr Andrew Meyer-Operation , JM GmbH'), + 2000017=rel(anchor='LP JM GmbH', type='SUBSCRIBER', mark='operations-announce', holder='LP JM GmbH', contact='Herr Andrew Meyer-Operation , JM GmbH'), + 2000018=rel(anchor='LP JM GmbH', type='REPRESENTATIVE', holder='LP JM GmbH', contact='Herr Philip Meyer-Contract , JM GmbH'), + 2000019=rel(anchor='LP JM GmbH', type='SUBSCRIBER', mark='members-announce', holder='LP JM GmbH', contact='Herr Philip Meyer-Contract , JM GmbH'), + 2000020=rel(anchor='LP JM GmbH', type='SUBSCRIBER', mark='customers-announce', holder='LP JM GmbH', contact='Herr Philip Meyer-Contract , JM GmbH'), + 2000021=rel(anchor='LP JM GmbH', type='VIP_CONTACT', holder='LP JM GmbH', contact='Frau Tammy Meyer-VIP , JM GmbH'), + 2000022=rel(anchor='?? Test PS', type='OPERATIONS', holder='?? Test PS', contact='Petra Schmidt , Test PS'), + 2000023=rel(anchor='?? Test PS', type='REPRESENTATIVE', holder='?? Test PS', contact='Petra Schmidt , Test PS'), +2000024=rel(anchor='NP Mellies, Michael', type='SUBSCRIBER', mark='operations-announce', holder='NP Fanninga, Frauke', contact='Frau Frauke Fanninga ') + } """); } @@ -307,15 +332,15 @@ public class ImportOfficeData extends ContextBasedTest { } @Test - @Order(1031) + @Order(1039) void verifySepaMandates() { assumeThatWeAreImportingControlledTestData(); assertThat(toFormattedString(bankAccounts)).isEqualToIgnoringWhitespace(""" { - 234234=bankAccount(holder='Michael Mellies', iban='DE37500105177419788228', bic='INGDDEFFXXX'), - 235600=bankAccount(holder='JM e.K.', iban='DE02300209000106531065', bic='CMCIDEDD'), - 235662=bankAccount(holder='JM GmbH', iban='DE49500105174516484892', bic='INGDDEFFXXX') + 234234=bankAccount(DE37500105177419788228: holder='Michael Mellies', bic='INGDDEFFXXX'), + 235600=bankAccount(DE02300209000106531065: holder='JM e.K.', bic='CMCIDEDD'), + 235662=bankAccount(DE49500105174516484892: holder='JM GmbH', bic='INGDDEFFXXX') } """); assertThat(toFormattedString(sepaMandates)).isEqualToIgnoringWhitespace(""" @@ -366,20 +391,20 @@ public class ImportOfficeData extends ContextBasedTest { } @Test - @Order(1051) + @Order(1059) void verifyCoopAssets() { assumeThatWeAreImportingControlledTestData(); assertThat(toFormattedString(coopAssets)).isEqualToIgnoringWhitespace(""" { - 30000=CoopAssetsTransaction(1001700, 2000-12-06, DEPOSIT, 1280.00, for subscription A), - 31000=CoopAssetsTransaction(1002000, 2000-12-06, DEPOSIT, 128.00, for subscription B), - 32000=CoopAssetsTransaction(1001700, 2005-01-10, DEPOSIT, 2560.00, for subscription C), - 33001=CoopAssetsTransaction(1001700, 2005-01-10, TRANSFER, -512.00, for transfer to 10), - 33002=CoopAssetsTransaction(1002000, 2005-01-10, ADOPTION, 512.00, for transfer from 7), - 34001=CoopAssetsTransaction(1002000, 2016-12-31, CLEARING, -8.00, for cancellation D), - 34002=CoopAssetsTransaction(1002000, 2016-12-31, DISBURSAL, -100.00, for cancellation D), - 34003=CoopAssetsTransaction(1002000, 2016-12-31, LOSS, -20.00, for cancellation D) + 30000=CoopAssetsTransaction(M-1001700: 2000-12-06, DEPOSIT, 1280.00, for subscription A), + 31000=CoopAssetsTransaction(M-1002000: 2000-12-06, DEPOSIT, 128.00, for subscription B), + 32000=CoopAssetsTransaction(M-1001700: 2005-01-10, DEPOSIT, 2560.00, for subscription C), + 33001=CoopAssetsTransaction(M-1001700: 2005-01-10, TRANSFER, -512.00, for transfer to 10), + 33002=CoopAssetsTransaction(M-1002000: 2005-01-10, ADOPTION, 512.00, for transfer from 7), + 34001=CoopAssetsTransaction(M-1002000: 2016-12-31, CLEARING, -8.00, for cancellation D), + 34002=CoopAssetsTransaction(M-1002000: 2016-12-31, DISBURSAL, -100.00, for cancellation D), + 34003=CoopAssetsTransaction(M-1002000: 2016-12-31, LOSS, -20.00, for cancellation D) } """); } @@ -388,31 +413,37 @@ public class ImportOfficeData extends ContextBasedTest { @Order(2000) void verifyAllPartnersHavePersons() { partners.forEach((id, p) -> { + final var partnerRel = p.getPartnerRel(); + assertThat(partnerRel).describedAs("partner " + id + " without partnerRel").isNotNull(); if ( id != 99 ) { - assertThat(p.getContact()).describedAs("partner " + id + " without contact").isNotNull(); - assertThat(p.getContact().getLabel()).describedAs("partner " + id + " without valid contact").isNotNull(); - assertThat(p.getPerson()).describedAs("partner " + id + " without person").isNotNull(); - assertThat(p.getPerson().getPersonType()).describedAs("partner " + id + " without valid person").isNotNull(); + assertThat(partnerRel.getContact()).describedAs("partner " + id + " without partnerRel.contact").isNotNull(); + assertThat(partnerRel.getContact().getLabel()).describedAs("partner " + id + " without valid partnerRel.contact").isNotNull(); + assertThat(partnerRel.getHolder()).describedAs("partner " + id + " without partnerRel.relHolder").isNotNull(); + assertThat(partnerRel.getHolder().getPersonType()).describedAs("partner " + id + " without valid partnerRel.relHolder").isNotNull(); } }); } @Test - @Order(2001) - void removeEmptyRelationships() { + @Order(2009) + void removeEmptyRelations() { assumeThatWeAreImportingControlledTestData(); - // avoid a error when persisting the deliberetely invalid partner entry #99 + // avoid a error when persisting the deliberately invalid partner entry #99 final var idsToRemove = new HashSet(); - relationships.forEach( (id, r) -> { + relations.forEach( (id, r) -> { // such a record if (r.getContact() == null || r.getContact().getLabel() == null || - r.getRelHolder() == null | r.getRelHolder().getPersonType() == null ) { + r.getHolder() == null || r.getHolder().getPersonType() == null ) { idsToRemove.add(id); } }); - assertThat(idsToRemove.size()).isEqualTo(1); // only from partner #99 (partner+contractual roles) - idsToRemove.forEach(id -> relationships.remove(id)); + + // expected relations created from partner #99 + Hostsharing eG itself + idsToRemove.forEach(id -> { + System.out.println("removing unused relation: " + relations.get(id).toString()); + relations.remove(id); + }); } @Test @@ -423,14 +454,20 @@ public class ImportOfficeData extends ContextBasedTest { // avoid a error when persisting the deliberately invalid partner entry #99 final var idsToRemove = new HashSet(); partners.forEach( (id, r) -> { - // such a record - if (r.getContact() == null || r.getContact().getLabel() == null || - r.getPerson() == null | r.getPerson().getPersonType() == null ) { + final var partnerRole = r.getPartnerRel(); + + // such a record is in test data to test error messages + if (partnerRole.getContact() == null || partnerRole.getContact().getLabel() == null || + partnerRole.getHolder() == null | partnerRole.getHolder().getPersonType() == null ) { idsToRemove.add(id); } }); - assertThat(idsToRemove.size()).isEqualTo(1); // only from partner #99 - idsToRemove.forEach(id -> partners.remove(id)); + + // expected partners created from partner #99 + Hostsharing eG itself + idsToRemove.forEach(id -> { + System.out.println("removing unused partner: " + partners.get(id).toString()); + partners.remove(id); + }); } @Test @@ -440,10 +477,11 @@ public class ImportOfficeData extends ContextBasedTest { // avoid a error when persisting the deliberately invalid partner entry #99 final var idsToRemove = new HashSet(); - debitors.forEach( (id, r) -> { - // such a record - if (r.getBillingContact() == null || r.getBillingContact().getLabel() == null || - r.getPartner().getPerson() == null | r.getPartner().getPerson().getPersonType() == null ) { + debitors.forEach( (id, d) -> { + final var debitorRel = d.getDebitorRel(); + if (debitorRel.getContact() == null || debitorRel.getContact().getLabel() == null || + debitorRel.getAnchor() == null || debitorRel.getAnchor().getPersonType() == null || + debitorRel.getHolder() == null || debitorRel.getHolder().getPersonType() == null ) { idsToRemove.add(id); } }); @@ -475,18 +513,28 @@ public class ImportOfficeData extends ContextBasedTest { jpaAttempt.transacted(() -> { context(rbacSuperuser); - relationships.forEach(this::persist); + relations.forEach(this::persist); }).assertSuccessful(); jpaAttempt.transacted(() -> { context(rbacSuperuser); - partners.forEach(this::persist); + partners.forEach((id, partner) -> { + // TODO: this is ugly and I don't know why it's suddenly necessary + partner.getPartnerRel().setAnchor(em.merge(partner.getPartnerRel().getAnchor())); + partner.getPartnerRel().setHolder(em.merge(partner.getPartnerRel().getHolder())); + partner.getPartnerRel().setContact(em.merge(partner.getPartnerRel().getContact())); + partner.setPartnerRel(em.merge(partner.getPartnerRel())); + em.persist(partner); + }); updateLegacyIds(partners, "hs_office_partner_legacy_id", "bp_id"); }).assertSuccessful(); jpaAttempt.transacted(() -> { context(rbacSuperuser); - debitors.forEach(this::persist); + debitors.forEach((id, debitor) -> { + debitor.setDebitorRel(em.merge(debitor.getDebitorRel())); + em.persist(debitor); + }); }).assertSuccessful(); jpaAttempt.transacted(() -> { @@ -520,7 +568,7 @@ public class ImportOfficeData extends ContextBasedTest { } - private void persist(final Integer id, final HasUuid entity) { + private void persist(final Integer id, final RbacObject entity) { try { //System.out.println("persisting #" + entity.hashCode() + ": " + entity); em.persist(entity); @@ -552,7 +600,7 @@ public class ImportOfficeData extends ContextBasedTest { em.createNativeQuery("delete from hs_office_bankaccount where true").executeUpdate(); em.createNativeQuery("delete from hs_office_partner where true").executeUpdate(); em.createNativeQuery("delete from hs_office_partner_details where true").executeUpdate(); - em.createNativeQuery("delete from hs_office_relationship where true").executeUpdate(); + em.createNativeQuery("delete from hs_office_relation where true").executeUpdate(); em.createNativeQuery("delete from hs_office_contact where true").executeUpdate(); em.createNativeQuery("delete from hs_office_person where true").executeUpdate(); }).assertSuccessful(); @@ -591,7 +639,7 @@ public class ImportOfficeData extends ContextBasedTest { }).assertSuccessful(); } - private void updateLegacyIds( + private void updateLegacyIds( Map entities, final String legacyIdTable, final String legacyIdColumn) { @@ -656,28 +704,30 @@ public class ImportOfficeData extends ContextBasedTest { .forEach(rec -> { final var person = HsOfficePersonEntity.builder().build(); - final var partnerRelationship = HsOfficeRelationshipEntity.builder() - .relHolder(person) - .relType(HsOfficeRelationshipType.PARTNER) - .relAnchor(mandant) - .contact(null) // is set during contacts import depending on assigned roles - .build(); - relationships.put(relationshipId++, partnerRelationship); + final var partnerRel = addRelation( + HsOfficeRelationType.PARTNER, mandant, person, + null // is set during contacts import depending on assigned roles + ); final var partner = HsOfficePartnerEntity.builder() .partnerNumber(rec.getInteger("member_id")) .details(HsOfficePartnerDetailsEntity.builder().build()) - .partnerRole(partnerRelationship) - .contact(null) // is set during contacts import depending on assigned roles - .person(person) + .partnerRel(partnerRel) .build(); partners.put(rec.getInteger("bp_id"), partner); + final var debitorRel = addRelation( + HsOfficeRelationType.DEBITOR, partnerRel.getHolder(), // partner person + null, // will be set in contacts import + null // will beset in contacts import + ); + relations.put(relationId++, debitorRel); + final var debitor = HsOfficeDebitorEntity.builder() + .debitorNumberSuffix("00") .partner(partner) - .debitorNumberSuffix((byte) 0) + .debitorRel(debitorRel) .defaultPrefix(rec.getString("member_code").replace("hsh00-", "")) - .partner(partner) .billable(rec.isEmpty("free") || rec.getString("free").equals("f")) .vatReverseCharge(rec.getBoolean("exempt_vat")) .vatBusiness("GROSS".equals(rec.getString("indicator_vat"))) // TODO: remove @@ -698,7 +748,6 @@ public class ImportOfficeData extends ContextBasedTest { isBlank(rec.getString("member_until")) ? HsOfficeReasonForTermination.NONE : HsOfficeReasonForTermination.UNKNOWN) - .mainDebitor(debitor) .build(); memberships.put(rec.getInteger("bp_id"), membership); } @@ -824,91 +873,92 @@ public class ImportOfficeData extends ContextBasedTest { final var partner = partners.get(bpId); final var debitor = debitors.get(bpId); - final var partnerPerson = partner.getPerson(); - if (containsPartnerRole(rec)) { - initPerson(partner.getPerson(), rec); + final var partnerPerson = partner.getPartnerRel().getHolder(); + if (containsPartnerRel(rec)) { + addPerson(partnerPerson, rec); } HsOfficePersonEntity contactPerson = partnerPerson; if (!StringUtils.equals(rec.getString("firma"), partnerPerson.getTradeName()) || !StringUtils.equals(rec.getString("first_name"), partnerPerson.getGivenName()) || !StringUtils.equals(rec.getString("last_name"), partnerPerson.getFamilyName())) { - contactPerson = initPerson(HsOfficePersonEntity.builder().build(), rec); + contactPerson = addPerson(HsOfficePersonEntity.builder().build(), rec); } final var contact = HsOfficeContactEntity.builder().build(); initContact(contact, rec); - if (containsPartnerRole(rec)) { - assertThat(partner.getContact()).isNull(); - partner.setContact(contact); - partner.getPartnerRole().setContact(contact); + if (containsPartnerRel(rec)) { + assertThat(partner.getPartnerRel().getContact()).isNull(); + partner.getPartnerRel().setContact(contact); } if (containsRole(rec, "billing")) { - assertThat(debitor.getBillingContact()).isNull(); - debitor.setBillingContact(contact); + assertThat(debitor.getDebitorRel().getContact()).isNull(); + debitor.getDebitorRel().setHolder(contactPerson); + debitor.getDebitorRel().setContact(contact); } if (containsRole(rec, "operation")) { - addRelationship(partnerPerson, contactPerson, contact, HsOfficeRelationshipType.OPERATIONS); + addRelation(HsOfficeRelationType.OPERATIONS, partnerPerson, contactPerson, contact); } if (containsRole(rec, "contractual")) { - addRelationship(partnerPerson, contactPerson, contact, HsOfficeRelationshipType.REPRESENTATIVE); + addRelation(HsOfficeRelationType.REPRESENTATIVE, partnerPerson, contactPerson, contact); } if (containsRole(rec, "ex-partner")) { - addRelationship(partnerPerson, contactPerson, contact, HsOfficeRelationshipType.EX_PARTNER); + addRelation(HsOfficeRelationType.EX_PARTNER, partnerPerson, contactPerson, contact); } if (containsRole(rec, "vip-contact")) { - addRelationship(partnerPerson, contactPerson, contact, HsOfficeRelationshipType.VIP_CONTACT); + addRelation(HsOfficeRelationType.VIP_CONTACT, partnerPerson, contactPerson, contact); } for (String subscriberRole: SUBSCRIBER_ROLES) { if (containsRole(rec, subscriberRole)) { - addRelationship(partnerPerson, contactPerson, contact, HsOfficeRelationshipType.SUBSCRIBER) - .setRelMark(subscriberRole.split(":")[1]) + addRelation(HsOfficeRelationType.SUBSCRIBER, partnerPerson, contactPerson, contact) + .setMark(subscriberRole.split(":")[1]) ; } } verifyContainsOnlyKnownRoles(rec.getString("roles")); }); - optionallyAddMissingContractualRelationships(); + optionallyAddMissingContractualRelations(); } - private static void optionallyAddMissingContractualRelationships() { + private static void optionallyAddMissingContractualRelations() { final var contractualMissing = new HashSet(); partners.forEach( (id, partner) -> { - final var partnerPerson = partner.getPerson(); - if (relationships.values().stream() - .filter(rel -> rel.getRelAnchor() == partnerPerson && rel.getRelType() == HsOfficeRelationshipType.REPRESENTATIVE) + final var partnerPerson = partner.getPartnerRel().getHolder(); + if (relations.values().stream() + .filter(rel -> rel.getAnchor() == partnerPerson && rel.getType() == HsOfficeRelationType.REPRESENTATIVE) .findFirst().isEmpty()) { contractualMissing.add(partner.getPartnerNumber()); } }); + assertThat(contractualMissing).containsOnly(19999); // deliberately wrong partner entry } private static boolean containsRole(final Record rec, final String role) { final var roles = rec.getString("roles"); return ("," + roles + ",").contains("," + role + ","); } - private static boolean containsPartnerRole(final Record rec) { + private static boolean containsPartnerRel(final Record rec) { return containsRole(rec, "partner"); } - private static HsOfficeRelationshipEntity addRelationship( - final HsOfficePersonEntity partnerPerson, - final HsOfficePersonEntity contactPerson, - final HsOfficeContactEntity contact, - final HsOfficeRelationshipType representative) { - final var rel = HsOfficeRelationshipEntity.builder() - .relAnchor(partnerPerson) - .relHolder(contactPerson) + private static HsOfficeRelationEntity addRelation( + final HsOfficeRelationType type, + final HsOfficePersonEntity anchor, + final HsOfficePersonEntity holder, + final HsOfficeContactEntity contact) { + final var rel = HsOfficeRelationEntity.builder() + .anchor(anchor) + .holder(holder) .contact(contact) - .relType(representative) + .type(type) .build(); - relationships.put(relationshipId++, rel); + relations.put(relationId++, rel); return rel; } - private HsOfficePersonEntity initPerson(final HsOfficePersonEntity person, final Record contactRecord) { + private HsOfficePersonEntity addPerson(final HsOfficePersonEntity person, final Record contactRecord) { // TODO: title+salutation: add to person person.setGivenName(contactRecord.getString("first_name")); person.setFamilyName(contactRecord.getString("last_name")); @@ -1121,11 +1171,6 @@ class Record { return value == null || value.isBlank(); } - Byte getByte(final String columnName) { - final String value = getString(columnName); - return isNotBlank(value) ? Byte.valueOf(value.trim()) : 0; - } - boolean getBoolean(final String columnName) { final String value = getString(columnName); return isNotBlank(value) && diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerAcceptanceTest.java index 33a312c4..e8eac1c1 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerAcceptanceTest.java @@ -7,9 +7,9 @@ import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRepository; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository; -import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity; -import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipRepository; -import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipType; +import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity; +import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRepository; +import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType; import net.hostsharing.hsadminng.hs.office.test.ContextBasedTestWithCleanup; import net.hostsharing.test.Accepts; import net.hostsharing.test.JpaAttempt; @@ -41,7 +41,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu HsOfficePartnerRepository partnerRepo; @Autowired - HsOfficeRelationshipRepository relationshipRepository; + HsOfficeRelationRepository relationRepository; @Autowired HsOfficePersonRepository personRepo; @@ -91,9 +91,9 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu void globalAdmin_withoutAssumedRole_canAddPartner() { context.define("superuser-alex@hostsharing.net"); - final var givenMandantPerson = personRepo.findPersonByOptionalNameLike("Hostsharing eG").get(0); - final var givenPerson = personRepo.findPersonByOptionalNameLike("Third").get(0); - final var givenContact = contactRepo.findContactByOptionalLabelLike("fourth").get(0); + final var givenMandantPerson = personRepo.findPersonByOptionalNameLike("Hostsharing eG").stream().findFirst().orElseThrow(); + final var givenPerson = personRepo.findPersonByOptionalNameLike("Third").stream().findFirst().orElseThrow(); + final var givenContact = contactRepo.findContactByOptionalLabelLike("fourth").stream().findFirst().orElseThrow(); final var location = RestAssured // @formatter:off .given() @@ -102,13 +102,11 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu .body(""" { "partnerNumber": "20002", - "partnerRole": { - "relAnchorUuid": "%s", - "relHolderUuid": "%s", + "partnerRel": { + "anchorUuid": "%s", + "holderUuid": "%s", "contactUuid": "%s" }, - "personUuid": "%s", - "contactUuid": "%s", "details": { "registrationOffice": "Temp Registergericht Aurich", "registrationNumber": "111111" @@ -117,21 +115,29 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu """.formatted( givenMandantPerson.getUuid(), givenPerson.getUuid(), - givenContact.getUuid(), - givenPerson.getUuid(), givenContact.getUuid())) .port(port) .when() .post("http://localhost/api/hs/office/partners") - .then().assertThat() + .then().log().body().assertThat() .statusCode(201) .contentType(ContentType.JSON) - .body("uuid", isUuidValid()) - .body("partnerNumber", is(20002)) - .body("details.registrationOffice", is("Temp Registergericht Aurich")) - .body("details.registrationNumber", is("111111")) - .body("contact.label", is(givenContact.getLabel())) - .body("person.tradeName", is(givenPerson.getTradeName())) + .body("", lenientlyEquals(""" + { + "partnerNumber": 20002, + "partnerRel": { + "anchor": { "tradeName": "Hostsharing eG" }, + "holder": { "tradeName": "Third OHG" }, + "type": "PARTNER", + "mark": null, + "contact": { "label": "fourth contact" } + }, + "details": { + "registrationOffice": "Temp Registergericht Aurich", + "registrationNumber": "111111" + } + } + """)) .header("Location", startsWith("http://localhost")) .extract().header("Location"); // @formatter:on @@ -155,9 +161,9 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu .body(""" { "partnerNumber": "20003", - "partnerRole": { - "relAnchorUuid": "%s", - "relHolderUuid": "%s", + "partnerRel": { + "anchorUuid": "%s", + "holderUuid": "%s", "contactUuid": "%s" }, "personUuid": "%s", @@ -193,9 +199,9 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu .body(""" { "partnerNumber": "20004", - "partnerRole": { - "relAnchorUuid": "%s", - "relHolderUuid": "%s", + "partnerRel": { + "anchorUuid": "%s", + "holderUuid": "%s", "contactUuid": "%s" }, "personUuid": "%s", @@ -226,6 +232,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu @Test void globalAdmin_withoutAssumedRole_canGetArbitraryPartner() { context.define("superuser-alex@hostsharing.net"); + final var partners = partnerRepo.findAll(); final var givenPartnerUuid = partnerRepo.findPartnerByOptionalNameLike("First").get(0).getUuid(); RestAssured // @formatter:off @@ -239,8 +246,18 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu .contentType("application/json") .body("", lenientlyEquals(""" { - "person": { "tradeName": "First GmbH" }, - "contact": { "label": "first contact" } + "partnerNumber": 10001, + "partnerRel": { + "anchor": { "tradeName": "Hostsharing eG" }, + "holder": { "tradeName": "First GmbH" }, + "type": "PARTNER", + "contact": { "label": "first contact" } + }, + "details": { + "registrationOffice": "Hamburg", + "registrationNumber": "RegNo123456789" + } + } } """)); // @formatter:on } @@ -278,8 +295,10 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu .contentType("application/json") .body("", lenientlyEquals(""" { - "person": { "tradeName": "First GmbH" }, - "contact": { "label": "first contact" } + "partnerRel": { + "holder": { "tradeName": "First GmbH" }, + "contact": { "label": "first contact" } + } } """)); // @formatter:on } @@ -295,8 +314,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu context.define("superuser-alex@hostsharing.net"); final var givenPartner = givenSomeTemporaryPartnerBessler(20011); - final var givenPerson = personRepo.findPersonByOptionalNameLike("Third").get(0); - final var givenContact = contactRepo.findContactByOptionalLabelLike("fourth").get(0); + final var givenPartnerRel = givenSomeTemporaryPartnerRel("Third OHG", "third contact"); RestAssured // @formatter:off .given() @@ -305,8 +323,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu .body(""" { "partnerNumber": "20011", - "contactUuid": "%s", - "personUuid": "%s", + "partnerRelUuid": "%s", "details": { "registrationOffice": "Temp Registergericht Aurich", "registrationNumber": "222222", @@ -315,18 +332,32 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu "dateOfDeath": "2022-01-12" } } - """.formatted(givenContact.getUuid(), givenPerson.getUuid())) + """.formatted(givenPartnerRel.getUuid())) .port(port) .when() .patch("http://localhost/api/hs/office/partners/" + givenPartner.getUuid()) - .then().assertThat() + .then().log().body().assertThat() .statusCode(200) .contentType(ContentType.JSON) - .body("uuid", is(givenPartner.getUuid().toString())) // not patched! - .body("partnerNumber", is(givenPartner.getPartnerNumber())) // not patched! - .body("details.registrationNumber", is("222222")) - .body("contact.label", is(givenContact.getLabel())) - .body("person.tradeName", is(givenPerson.getTradeName())); + .body("", lenientlyEquals(""" + { + "partnerNumber": 20011, + "partnerRel": { + "anchor": { "tradeName": "Hostsharing eG" }, + "holder": { "tradeName": "Third OHG" }, + "type": "PARTNER", + "contact": { "label": "third contact" } + }, + "details": { + "registrationOffice": "Temp Registergericht Aurich", + "registrationNumber": "222222", + "birthName": "Maja Schmidt", + "birthPlace": null, + "birthday": "1938-04-08", + "dateOfDeath": "2022-01-12" + } + } + """)); // @formatter:on // finally, the partner is actually updated @@ -334,8 +365,8 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu assertThat(partnerRepo.findByUuid(givenPartner.getUuid())).isPresent().get() .matches(partner -> { assertThat(partner.getPartnerNumber()).isEqualTo(givenPartner.getPartnerNumber()); - assertThat(partner.getPerson().getTradeName()).isEqualTo("Third OHG"); - assertThat(partner.getContact().getLabel()).isEqualTo("fourth contact"); + assertThat(partner.getPartnerRel().getHolder().getTradeName()).isEqualTo("Third OHG"); + assertThat(partner.getPartnerRel().getContact().getLabel()).isEqualTo("third contact"); assertThat(partner.getDetails().getRegistrationOffice()).isEqualTo("Temp Registergericht Aurich"); assertThat(partner.getDetails().getRegistrationNumber()).isEqualTo("222222"); assertThat(partner.getDetails().getBirthName()).isEqualTo("Maja Schmidt"); @@ -371,16 +402,18 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu .statusCode(200) .contentType(ContentType.JSON) .body("uuid", isUuidValid()) - .body("details.birthName", is("Maja Schmidt")) - .body("contact.label", is(givenPartner.getContact().getLabel())) - .body("person.tradeName", is(givenPartner.getPerson().getTradeName())); + .body("details.birthName", is("Maja Schmidt")); + // TODO: assert partnerRel +// .body("contact.label", is(givenPartner.getContact().getLabel())) +// .body("person.tradeName", is(givenPartner.getPerson().getTradeName())); // @formatter:on // finally, the partner is actually updated assertThat(partnerRepo.findByUuid(givenPartner.getUuid())).isPresent().get() .matches(person -> { - assertThat(person.getPerson().getTradeName()).isEqualTo(givenPartner.getPerson().getTradeName()); - assertThat(person.getContact().getLabel()).isEqualTo(givenPartner.getContact().getLabel()); + // TODO: assert partnerRel +// assertThat(person.getPerson().getTradeName()).isEqualTo(givenPartner.getPerson().getTradeName()); +// assertThat(person.getContact().getLabel()).isEqualTo(givenPartner.getContact().getLabel()); assertThat(person.getDetails().getRegistrationOffice()).isEqualTo("Temp Registergericht Leer"); assertThat(person.getDetails().getRegistrationNumber()).isEqualTo("333333"); assertThat(person.getDetails().getBirthName()).isEqualTo("Maja Schmidt"); @@ -413,7 +446,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu // then the given partner is gone assertThat(partnerRepo.findByUuid(givenPartner.getUuid())).isEmpty(); - assertThat(relationshipRepository.findByUuid(givenPartner.getPartnerRole().getUuid())).isEmpty(); + assertThat(relationRepository.findByUuid(givenPartner.getPartnerRel().getUuid())).isEmpty(); } @Test @@ -421,7 +454,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu void contactAdminUser_canNotDeleteRelatedPartner() { context.define("superuser-alex@hostsharing.net"); final var givenPartner = givenSomeTemporaryPartnerBessler(20014); - assertThat(givenPartner.getContact().getLabel()).isEqualTo("fourth contact"); + assertThat(givenPartner.getPartnerRel().getContact().getLabel()).isEqualTo("fourth contact"); RestAssured // @formatter:off .given() @@ -441,7 +474,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu void normalUser_canNotDeleteUnrelatedPartner() { context.define("superuser-alex@hostsharing.net"); final var givenPartner = givenSomeTemporaryPartnerBessler(20015); - assertThat(givenPartner.getContact().getLabel()).isEqualTo("fourth contact"); + assertThat(givenPartner.getPartnerRel().getContact().getLabel()).isEqualTo("fourth contact"); RestAssured // @formatter:off .given() @@ -457,26 +490,32 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu } } + private HsOfficeRelationEntity givenSomeTemporaryPartnerRel( + final String partnerHolderName, + final String contactName) { + return jpaAttempt.transacted(() -> { + context.define("superuser-alex@hostsharing.net"); + final var givenMandantPerson = personRepo.findPersonByOptionalNameLike("Hostsharing eG").stream().findFirst().orElseThrow(); + final var givenPerson = personRepo.findPersonByOptionalNameLike(partnerHolderName).stream().findFirst().orElseThrow(); + final var givenContact = contactRepo.findContactByOptionalLabelLike(contactName).stream().findFirst().orElseThrow(); + + final var partnerRel = new HsOfficeRelationEntity(); + partnerRel.setType(HsOfficeRelationType.PARTNER); + partnerRel.setAnchor(givenMandantPerson); + partnerRel.setHolder(givenPerson); + partnerRel.setContact(givenContact); + em.persist(partnerRel); + return partnerRel; + }).assertSuccessful().returnedValue(); + } private HsOfficePartnerEntity givenSomeTemporaryPartnerBessler(final Integer partnerNumber) { return jpaAttempt.transacted(() -> { context.define("superuser-alex@hostsharing.net"); - final var givenMandantPerson = personRepo.findPersonByOptionalNameLike("Hostsharing eG").get(0); - - final var givenPerson = personRepo.findPersonByOptionalNameLike("Erben Bessler").get(0); - final var givenContact = contactRepo.findContactByOptionalLabelLike("fourth contact").get(0); - - final var partnerRole = new HsOfficeRelationshipEntity(); - partnerRole.setRelType(HsOfficeRelationshipType.PARTNER); - partnerRole.setRelAnchor(givenMandantPerson); - partnerRole.setRelHolder(givenPerson); - partnerRole.setContact(givenContact); - em.persist(partnerRole); + final var partnerRel = em.merge(givenSomeTemporaryPartnerRel("Erben Bessler", "fourth contact")); final var newPartner = HsOfficePartnerEntity.builder() - .partnerRole(partnerRole) + .partnerRel(partnerRel) .partnerNumber(partnerNumber) - .person(givenPerson) - .contact(givenContact) .details(HsOfficePartnerDetailsEntity.builder() .registrationOffice("Temp Registergericht Leer") .registrationNumber("333333") @@ -492,6 +531,6 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu cleanupAllNew(HsOfficePartnerEntity.class); // TODO: should not be necessary anymore, once it's deleted via after delete trigger - cleanupAllNew(HsOfficeRelationshipEntity.class); + cleanupAllNew(HsOfficeRelationEntity.class); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerRestTest.java index ed04d899..e6e7fb7e 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerControllerRestTest.java @@ -3,8 +3,8 @@ package net.hostsharing.hsadminng.hs.office.partner; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; -import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity; -import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipRepository; +import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity; +import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRepository; import net.hostsharing.hsadminng.mapper.Mapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -54,7 +54,7 @@ class HsOfficePartnerControllerRestTest { HsOfficePartnerRepository partnerRepo; @MockBean - HsOfficeRelationshipRepository relationshipRepo; + HsOfficeRelationRepository relationRepo; @MockBean EntityManager em; @@ -100,9 +100,9 @@ class HsOfficePartnerControllerRestTest { .content(""" { "partnerNumber": "20002", - "partnerRole": { - "relAnchorUuid": "%s", - "relHolderUuid": "%s", + "partnerRel": { + "anchorUuid": "%s", + "holderUuid": "%s", "contactUuid": "%s" }, "personUuid": "%s", @@ -137,9 +137,9 @@ class HsOfficePartnerControllerRestTest { .content(""" { "partnerNumber": "20002", - "partnerRole": { - "relAnchorUuid": "%s", - "relHolderUuid": "%s", + "partnerRel": { + "anchorUuid": "%s", + "holderUuid": "%s", "contactUuid": "%s" }, "personUuid": "%s", @@ -175,11 +175,11 @@ class HsOfficePartnerControllerRestTest { when(partnerRepo.findByUuid(givenPartnerUuid)).thenReturn(Optional.of(partnerMock)); when(partnerRepo.deleteByUuid(givenPartnerUuid)).thenReturn(0); - final UUID givenRelationshipUuid = UUID.randomUUID(); - when(partnerMock.getPartnerRole()).thenReturn(HsOfficeRelationshipEntity.builder() - .uuid(givenRelationshipUuid) + final UUID givenRelationUuid = UUID.randomUUID(); + when(partnerMock.getPartnerRel()).thenReturn(HsOfficeRelationEntity.builder() + .uuid(givenRelationUuid) .build()); - when(relationshipRepo.deleteByUuid(givenRelationshipUuid)).thenReturn(0); + when(relationRepo.deleteByUuid(givenRelationUuid)).thenReturn(0); // when mockMvc.perform(MockMvcRequestBuilders @@ -191,31 +191,5 @@ class HsOfficePartnerControllerRestTest { // then .andExpect(status().isForbidden()); } - - @Test - void respondBadRequest_ifRelationshipCannotBeDeleted() throws Exception { - // given - final UUID givenPartnerUuid = UUID.randomUUID(); - when(partnerRepo.findByUuid(givenPartnerUuid)).thenReturn(Optional.of(partnerMock)); - when(partnerRepo.deleteByUuid(givenPartnerUuid)).thenReturn(1); - when(relationshipRepo.deleteByUuid(any())).thenReturn(0); - - final UUID givenRelationshipUuid = UUID.randomUUID(); - when(partnerMock.getPartnerRole()).thenReturn(HsOfficeRelationshipEntity.builder() - .uuid(givenRelationshipUuid) - .build()); - when(relationshipRepo.deleteByUuid(givenRelationshipUuid)).thenReturn(0); - - // when - mockMvc.perform(MockMvcRequestBuilders - .delete("/api/hs/office/partners/" + givenPartnerUuid) - .header("current-user", "superuser-alex@hostsharing.net") - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON)) - - // then - .andExpect(status().isForbidden()); - } - } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcherUnitTest.java index 5fe483ae..7f350649 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityPatcherUnitTest.java @@ -3,6 +3,7 @@ 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.test.PatchUnitTestBase; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestInstance; @@ -30,8 +31,7 @@ class HsOfficePartnerEntityPatcherUnitTest extends PatchUnitTestBase< private static final UUID INITIAL_CONTACT_UUID = UUID.randomUUID(); private static final UUID INITIAL_PERSON_UUID = UUID.randomUUID(); private static final UUID INITIAL_DETAILS_UUID = UUID.randomUUID(); - private static final UUID PATCHED_CONTACT_UUID = UUID.randomUUID(); - private static final UUID PATCHED_PERSON_UUID = UUID.randomUUID(); + private static final UUID PATCHED_PARTNER_ROLE_UUID = UUID.randomUUID(); private final HsOfficePersonEntity givenInitialPerson = HsOfficePersonEntity.builder() .uuid(INITIAL_PERSON_UUID) @@ -48,19 +48,21 @@ class HsOfficePartnerEntityPatcherUnitTest extends PatchUnitTestBase< @BeforeEach void initMocks() { - lenient().when(em.getReference(eq(HsOfficeContactEntity.class), any())).thenAnswer(invocation -> - HsOfficeContactEntity.builder().uuid(invocation.getArgument(1)).build()); - lenient().when(em.getReference(eq(HsOfficePersonEntity.class), any())).thenAnswer(invocation -> - HsOfficePersonEntity.builder().uuid(invocation.getArgument(1)).build()); + lenient().when(em.getReference(eq(HsOfficeRelationEntity.class), any())).thenAnswer(invocation -> + HsOfficeRelationEntity.builder().uuid(invocation.getArgument(1)).build()); } @Override protected HsOfficePartnerEntity newInitialEntity() { - final var entity = new HsOfficePartnerEntity(); - entity.setUuid(INITIAL_PARTNER_UUID); - entity.setPerson(givenInitialPerson); - entity.setContact(givenInitialContact); - entity.setDetails(givenInitialDetails); + final var entity = HsOfficePartnerEntity.builder() + .uuid(INITIAL_PARTNER_UUID) + .partnerNumber(12345) + .partnerRel(HsOfficeRelationEntity.builder() + .holder(givenInitialPerson) + .contact(givenInitialContact) + .build()) + .details(givenInitialDetails) + .build(); return entity; } @@ -78,31 +80,19 @@ class HsOfficePartnerEntityPatcherUnitTest extends PatchUnitTestBase< protected Stream propertyTestDescriptors() { return Stream.of( new JsonNullableProperty<>( - "contact", - HsOfficePartnerPatchResource::setContactUuid, - PATCHED_CONTACT_UUID, - HsOfficePartnerEntity::setContact, - newContact(PATCHED_CONTACT_UUID)) - .notNullable(), - new JsonNullableProperty<>( - "person", - HsOfficePartnerPatchResource::setPersonUuid, - PATCHED_PERSON_UUID, - HsOfficePartnerEntity::setPerson, - newPerson(PATCHED_PERSON_UUID)) + "partnerRel", + HsOfficePartnerPatchResource::setPartnerRelUuid, + PATCHED_PARTNER_ROLE_UUID, + HsOfficePartnerEntity::setPartnerRel, + newPartnerRel(PATCHED_PARTNER_ROLE_UUID)) .notNullable() ); } - private static HsOfficeContactEntity newContact(final UUID uuid) { - final var newContact = new HsOfficeContactEntity(); - newContact.setUuid(uuid); - return newContact; - } - - private HsOfficePersonEntity newPerson(final UUID uuid) { - final var newPerson = new HsOfficePersonEntity(); - newPerson.setUuid(uuid); - return newPerson; + private static HsOfficeRelationEntity newPartnerRel(final UUID uuid) { + final var newPartnerRel = HsOfficeRelationEntity.builder() + .uuid(uuid) + .build(); + return newPartnerRel; } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityUnitTest.java index a6d2c60a..62d81416 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntityUnitTest.java @@ -3,39 +3,39 @@ package net.hostsharing.hsadminng.hs.office.partner; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType; +import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity; +import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class HsOfficePartnerEntityUnitTest { + private final HsOfficePartnerEntity givenPartner = HsOfficePartnerEntity.builder() + .partnerNumber(12345) + .partnerRel(HsOfficeRelationEntity.builder() + .anchor(HsOfficePersonEntity.builder() + .personType(HsOfficePersonType.LEGAL_PERSON) + .tradeName("Hostsharing eG") + .build()) + .type(HsOfficeRelationType.PARTNER) + .holder(HsOfficePersonEntity.builder() + .personType(HsOfficePersonType.LEGAL_PERSON) + .tradeName("some trade name") + .build()) + .contact(HsOfficeContactEntity.builder().label("some label").build()) + .build()) + .build(); + @Test - void toStringContainsPersonAndContact() { - final var given = HsOfficePartnerEntity.builder() - .person(HsOfficePersonEntity.builder() - .personType(HsOfficePersonType.LEGAL_PERSON) - .tradeName("some trade name") - .build()) - .contact(HsOfficeContactEntity.builder().label("some label").build()) - .build(); - - final var result = given.toString(); - - assertThat(result).isEqualTo("partner(LP some trade name: some label)"); + void toStringContainsPartnerNumberPersonAndContact() { + final var result = givenPartner.toString(); + assertThat(result).isEqualTo("partner(P-12345: LP some trade name, some label)"); } @Test - void toShortStringContainsPersonAndContact() { - final var given = HsOfficePartnerEntity.builder() - .person(HsOfficePersonEntity.builder() - .personType(HsOfficePersonType.LEGAL_PERSON) - .tradeName("some trade name") - .build()) - .contact(HsOfficeContactEntity.builder().label("some label").build()) - .build(); - - final var result = given.toShortString(); - - assertThat(result).isEqualTo("LP some trade name"); + void toShortStringContainsPartnerNumber() { + final var result = givenPartner.toShortString(); + assertThat(result).isEqualTo("P-12345"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java index 2512a07d..98bff812 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java @@ -3,16 +3,16 @@ package net.hostsharing.hsadminng.hs.office.partner; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRepository; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository; -import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity; -import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipRepository; -import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipType; +import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity; +import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRepository; +import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType; import net.hostsharing.hsadminng.hs.office.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository; +import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacObjectRepository; import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository; import net.hostsharing.test.Array; import net.hostsharing.test.JpaAttempt; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -27,9 +27,9 @@ import jakarta.servlet.http.HttpServletRequest; import java.util.Arrays; import java.util.HashSet; import java.util.List; -import java.util.Set; import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf; +import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacObjectEntity.objectDisplaysOf; import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.distinctRoleNamesOf; import static net.hostsharing.test.Array.fromFormatted; import static net.hostsharing.test.JpaAttempt.attempt; @@ -43,7 +43,7 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean HsOfficePartnerRepository partnerRepo; @Autowired - HsOfficeRelationshipRepository relationshipRepo; + HsOfficeRelationRepository relationRepo; @Autowired HsOfficePersonRepository personRepo; @@ -51,6 +51,9 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean @Autowired HsOfficeContactRepository contactRepo; + @Autowired + RawRbacObjectRepository rawObjectRepo; + @Autowired RawRbacRoleRepository rawRoleRepo; @@ -66,8 +69,6 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean @MockBean HttpServletRequest request; - Set tempPartners = new HashSet<>(); - @Nested class CreatePartner { @@ -76,27 +77,14 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean // given context("superuser-alex@hostsharing.net"); final var count = partnerRepo.count(); - final var givenMandantorPerson = personRepo.findPersonByOptionalNameLike("Hostsharing eG").get(0); - final var givenPartnerPerson = personRepo.findPersonByOptionalNameLike("First GmbH").get(0); - final var givenContact = contactRepo.findContactByOptionalLabelLike("first contact").get(0); - - final var partnerRole = HsOfficeRelationshipEntity.builder() - .relHolder(givenPartnerPerson) - .relType(HsOfficeRelationshipType.PARTNER) - .relAnchor(givenMandantorPerson) - .contact(givenContact) - .build(); - relationshipRepo.save(partnerRole); + final var partnerRel = givenSomeTemporaryHostsharingPartnerRel("First GmbH", "first contact"); // when final var result = attempt(em, () -> { final var newPartner = HsOfficePartnerEntity.builder() .partnerNumber(20031) - .partnerRole(partnerRole) - .person(givenPartnerPerson) - .contact(givenContact) - .details(HsOfficePartnerDetailsEntity.builder() - .build()) + .partnerRel(partnerRel) + .details(HsOfficePartnerDetailsEntity.builder().build()) .build(); return partnerRepo.save(newPartner); }); @@ -125,19 +113,17 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean final var givenContact = contactRepo.findContactByOptionalLabelLike("fourth contact").get(0); final var givenMandantPerson = personRepo.findPersonByOptionalNameLike("Hostsharing eG").get(0); - final var newRelationship = HsOfficeRelationshipEntity.builder() - .relHolder(givenPartnerPerson) - .relType(HsOfficeRelationshipType.PARTNER) - .relAnchor(givenMandantPerson) + final var newRelation = HsOfficeRelationEntity.builder() + .holder(givenPartnerPerson) + .type(HsOfficeRelationType.PARTNER) + .anchor(givenMandantPerson) .contact(givenContact) .build(); - relationshipRepo.save(newRelationship); + relationRepo.save(newRelation); final var newPartner = HsOfficePartnerEntity.builder() .partnerNumber(20032) - .partnerRole(newRelationship) - .person(givenPartnerPerson) - .contact(givenContact) + .partnerRel(newRelation) .details(HsOfficePartnerDetailsEntity.builder().build()) .build(); return partnerRepo.save(newPartner); @@ -146,67 +132,52 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean // then assertThat(distinctRoleNamesOf(rawRoleRepo.findAll())).containsExactlyInAnyOrder(Array.from( initialRoleNames, - "hs_office_relationship#HostsharingeG-with-PARTNER-ErbenBesslerMelBessler.admin", - "hs_office_relationship#HostsharingeG-with-PARTNER-ErbenBesslerMelBessler.owner", - "hs_office_relationship#HostsharingeG-with-PARTNER-ErbenBesslerMelBessler.tenant", - "hs_office_partner#20032:ErbenBesslerMelBessler-fourthcontact.admin", - "hs_office_partner#20032:ErbenBesslerMelBessler-fourthcontact.agent", - "hs_office_partner#20032:ErbenBesslerMelBessler-fourthcontact.owner", - "hs_office_partner#20032:ErbenBesslerMelBessler-fourthcontact.tenant", - "hs_office_partner#20032:ErbenBesslerMelBessler-fourthcontact.guest")); + "hs_office_relation#HostsharingeG-with-PARTNER-ErbenBesslerMelBessler:OWNER", + "hs_office_relation#HostsharingeG-with-PARTNER-ErbenBesslerMelBessler:ADMIN", + "hs_office_relation#HostsharingeG-with-PARTNER-ErbenBesslerMelBessler:AGENT", + "hs_office_relation#HostsharingeG-with-PARTNER-ErbenBesslerMelBessler:TENANT")); assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())) .map(s -> s.replace("ErbenBesslerMelBessler", "EBess")) .map(s -> s.replace("fourthcontact", "4th")) .map(s -> s.replace("hs_office_", "")) .containsExactlyInAnyOrder(distinct(fromFormatted( initialGrantNames, - // relationship - TODO: check and cleanup - "{ grant role person#HostsharingeG.tenant to role person#EBess.admin by system and assume }", - "{ grant role person#EBess.tenant to role person#HostsharingeG.admin by system and assume }", - "{ grant role relationship#HostsharingeG-with-PARTNER-EBess.tenant to role partner#20032:EBess-4th.admin by system and assume }", - "{ grant role relationship#HostsharingeG-with-PARTNER-EBess.tenant to role partner#20032:EBess-4th.tenant by system and assume }", - "{ grant role partner#20032:EBess-4th.agent to role relationship#HostsharingeG-with-PARTNER-EBess.admin by system and assume }", - "{ grant role relationship#HostsharingeG-with-PARTNER-EBess.owner to role global#global.admin by system and assume }", - "{ grant role relationship#HostsharingeG-with-PARTNER-EBess.tenant to role contact#4th.admin by system and assume }", - "{ grant role relationship#HostsharingeG-with-PARTNER-EBess.tenant to role person#EBess.admin by system and assume }", - "{ grant role relationship#HostsharingeG-with-PARTNER-EBess.owner to role person#HostsharingeG.admin by system and assume }", - "{ grant role relationship#HostsharingeG-with-PARTNER-EBess.tenant to role person#HostsharingeG.admin by system and assume }", - "{ grant perm edit on relationship#HostsharingeG-with-PARTNER-EBess to role relationship#HostsharingeG-with-PARTNER-EBess.admin by system and assume }", - "{ grant role relationship#HostsharingeG-with-PARTNER-EBess.tenant to role relationship#HostsharingeG-with-PARTNER-EBess.admin by system and assume }", - "{ grant perm * on relationship#HostsharingeG-with-PARTNER-EBess to role relationship#HostsharingeG-with-PARTNER-EBess.owner by system and assume }", - "{ grant role relationship#HostsharingeG-with-PARTNER-EBess.admin to role relationship#HostsharingeG-with-PARTNER-EBess.owner by system and assume }", - "{ grant perm view on relationship#HostsharingeG-with-PARTNER-EBess to role relationship#HostsharingeG-with-PARTNER-EBess.tenant by system and assume }", - "{ grant role contact#4th.tenant to role relationship#HostsharingeG-with-PARTNER-EBess.tenant by system and assume }", - "{ grant role person#EBess.tenant to role relationship#HostsharingeG-with-PARTNER-EBess.tenant by system and assume }", - "{ grant role person#HostsharingeG.tenant to role relationship#HostsharingeG-with-PARTNER-EBess.tenant by system and assume }", + "{ grant perm:relation#HostsharingeG-with-PARTNER-EBess:INSERT>sepamandate to role:relation#HostsharingeG-with-PARTNER-EBess:ADMIN by system and assume }", - // owner - "{ grant perm * on partner#20032:EBess-4th to role partner#20032:EBess-4th.owner by system and assume }", - "{ grant perm * on partner_details#20032:EBess-4th-details to role partner#20032:EBess-4th.owner by system and assume }", - "{ grant role partner#20032:EBess-4th.owner to role global#global.admin by system and assume }", + // permissions on partner + "{ grant perm:partner#P-20032:DELETE to role:relation#HostsharingeG-with-PARTNER-EBess:ADMIN by system and assume }", + "{ grant perm:partner#P-20032:UPDATE to role:relation#HostsharingeG-with-PARTNER-EBess:AGENT by system and assume }", + "{ grant perm:partner#P-20032:SELECT to role:relation#HostsharingeG-with-PARTNER-EBess:TENANT by system and assume }", - // admin - "{ grant perm edit on partner#20032:EBess-4th to role partner#20032:EBess-4th.admin by system and assume }", - "{ grant perm edit on partner_details#20032:EBess-4th-details to role partner#20032:EBess-4th.admin by system and assume }", - "{ grant role partner#20032:EBess-4th.admin to role partner#20032:EBess-4th.owner by system and assume }", - "{ grant role person#EBess.tenant to role partner#20032:EBess-4th.admin by system and assume }", - "{ grant role contact#4th.tenant to role partner#20032:EBess-4th.admin by system and assume }", + // permissions on partner-details + "{ grant perm:partner_details#P-20032:DELETE to role:relation#HostsharingeG-with-PARTNER-EBess:ADMIN by system and assume }", + "{ grant perm:partner_details#P-20032:UPDATE to role:relation#HostsharingeG-with-PARTNER-EBess:AGENT by system and assume }", + "{ grant perm:partner_details#P-20032:SELECT to role:relation#HostsharingeG-with-PARTNER-EBess:AGENT by system and assume }", - // agent - "{ grant perm view on partner_details#20032:EBess-4th-details to role partner#20032:EBess-4th.agent by system and assume }", - "{ grant role partner#20032:EBess-4th.agent to role partner#20032:EBess-4th.admin by system and assume }", - "{ grant role partner#20032:EBess-4th.agent to role person#EBess.admin by system and assume }", - "{ grant role partner#20032:EBess-4th.agent to role contact#4th.admin by system and assume }", + // permissions on partner-relation + "{ grant perm:relation#HostsharingeG-with-PARTNER-EBess:DELETE to role:relation#HostsharingeG-with-PARTNER-EBess:OWNER by system and assume }", + "{ grant perm:relation#HostsharingeG-with-PARTNER-EBess:UPDATE to role:relation#HostsharingeG-with-PARTNER-EBess:ADMIN by system and assume }", + "{ grant perm:relation#HostsharingeG-with-PARTNER-EBess:SELECT to role:relation#HostsharingeG-with-PARTNER-EBess:TENANT by system and assume }", - // tenant - "{ grant role partner#20032:EBess-4th.tenant to role partner#20032:EBess-4th.agent by system and assume }", - "{ grant role person#EBess.guest to role partner#20032:EBess-4th.tenant by system and assume }", - "{ grant role contact#4th.guest to role partner#20032:EBess-4th.tenant by system and assume }", + // relation owner + "{ grant role:relation#HostsharingeG-with-PARTNER-EBess:OWNER to role:global#global:ADMIN by system and assume }", + "{ grant role:relation#HostsharingeG-with-PARTNER-EBess:OWNER to user:superuser-alex@hostsharing.net by relation#HostsharingeG-with-PARTNER-EBess:OWNER and assume }", - // guest - "{ grant perm view on partner#20032:EBess-4th to role partner#20032:EBess-4th.guest by system and assume }", - "{ grant role partner#20032:EBess-4th.guest to role partner#20032:EBess-4th.tenant by system and assume }", + // relation admin + "{ grant role:relation#HostsharingeG-with-PARTNER-EBess:ADMIN to role:relation#HostsharingeG-with-PARTNER-EBess:OWNER by system and assume }", + "{ grant role:relation#HostsharingeG-with-PARTNER-EBess:ADMIN to role:person#HostsharingeG:ADMIN by system and assume }", + // relation agent + "{ grant role:relation#HostsharingeG-with-PARTNER-EBess:AGENT to role:person#EBess:ADMIN by system and assume }", + "{ grant role:relation#HostsharingeG-with-PARTNER-EBess:AGENT to role:relation#HostsharingeG-with-PARTNER-EBess:ADMIN by system and assume }", + + // relation tenant + "{ grant role:contact#4th:REFERRER to role:relation#HostsharingeG-with-PARTNER-EBess:TENANT by system and assume }", + "{ grant role:person#EBess:REFERRER to role:relation#HostsharingeG-with-PARTNER-EBess:TENANT by system and assume }", + "{ grant role:person#HostsharingeG:REFERRER to role:relation#HostsharingeG-with-PARTNER-EBess:TENANT by system and assume }", + "{ grant role:relation#HostsharingeG-with-PARTNER-EBess:TENANT to role:contact#4th:ADMIN by system and assume }", + "{ grant role:relation#HostsharingeG-with-PARTNER-EBess:TENANT to role:person#EBess:ADMIN by system and assume }", + "{ grant role:relation#HostsharingeG-with-PARTNER-EBess:TENANT to role:relation#HostsharingeG-with-PARTNER-EBess:AGENT by system and assume }", null))); } @@ -230,9 +201,11 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean // then allThesePartnersAreReturned( result, - "partner(IF Third OHG: third contact)", - "partner(LP Second e.K.: second contact)", - "partner(LP First GmbH: first contact)"); + "partner(P-10001: LP First GmbH, first contact)", + "partner(P-10002: LP Second e.K., second contact)", + "partner(P-10003: IF Third OHG, third contact)", + "partner(P-10004: LP Fourth eG, fourth contact)", + "partner(P-10010: NP Smith, Peter, sixth contact)"); } @Test @@ -244,7 +217,7 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean final var result = partnerRepo.findPartnerByOptionalNameLike(null); // then: - exactlyThesePartnersAreReturned(result, "partner(LP First GmbH: first contact)"); + exactlyThesePartnersAreReturned(result, "partner(P-10001: LP First GmbH, first contact)"); } } @@ -260,7 +233,7 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean final var result = partnerRepo.findPartnerByOptionalNameLike("third contact"); // then - exactlyThesePartnersAreReturned(result, "partner(IF Third OHG: third contact)"); + exactlyThesePartnersAreReturned(result, "partner(P-10003: IF Third OHG, third contact)"); } } @@ -279,7 +252,7 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean assertThat(result) .isNotNull() .extracting(Object::toString) - .isEqualTo("partner(LP First GmbH: first contact)"); + .isEqualTo("partner(P-10001: LP First GmbH, first contact)"); } } @@ -290,62 +263,81 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean public void hostsharingAdmin_withoutAssumedRole_canUpdateArbitraryPartner() { // given context("superuser-alex@hostsharing.net"); - final var givenPartner = givenSomeTemporaryPartnerBessler(20036, "Erben Bessler", "fifth contact"); + final var givenPartner = givenSomeTemporaryHostsharingPartner(20036, "Erben Bessler", "fifth contact"); assertThatPartnerIsVisibleForUserWithRole( givenPartner, - "hs_office_partner#20036:ErbenBesslerMelBessler-fifthcontact.admin"); + "hs_office_person#ErbenBesslerMelBessler:ADMIN"); assertThatPartnerActuallyInDatabase(givenPartner); - final var givenNewPerson = personRepo.findPersonByOptionalNameLike("Third OHG").get(0); - final var givenNewContact = contactRepo.findContactByOptionalLabelLike("sixth contact").get(0); // when final var result = jpaAttempt.transacted(() -> { context("superuser-alex@hostsharing.net"); - givenPartner.setContact(givenNewContact); - givenPartner.setPerson(givenNewPerson); + givenPartner.setPartnerRel(givenSomeTemporaryHostsharingPartnerRel("Third OHG", "sixth contact")); return partnerRepo.save(givenPartner); }); // then result.assertSuccessful(); + assertThatPartnerIsVisibleForUserWithRole( - result.returnedValue(), - "global#global.admin"); + givenPartner, + "global#global:ADMIN"); assertThatPartnerIsVisibleForUserWithRole( - result.returnedValue(), - "hs_office_person#ThirdOHG.admin"); + givenPartner, + "hs_office_person#ThirdOHG:ADMIN"); assertThatPartnerIsNotVisibleForUserWithRole( - result.returnedValue(), - "hs_office_person#ErbenBesslerMelBessler.admin"); + givenPartner, + "hs_office_person#ErbenBesslerMelBessler:ADMIN"); } @Test - @Disabled // TODO: enable once partner.person and partner.contact are removed - public void partnerAgent_canNotUpdateRelatedPartner() { + public void partnerRelationAgent_canUpdateRelatedPartner() { // given context("superuser-alex@hostsharing.net"); - final var givenPartner = givenSomeTemporaryPartnerBessler(20037, "Erben Bessler", "ninth"); + final var givenPartner = givenSomeTemporaryHostsharingPartner(20037, "Erben Bessler", "ninth"); assertThatPartnerIsVisibleForUserWithRole( givenPartner, - "hs_office_partner#20033:ErbenBesslerMelBessler-ninthcontact.agent"); + "hs_office_person#ErbenBesslerMelBessler:ADMIN"); assertThatPartnerActuallyInDatabase(givenPartner); // when final var result = jpaAttempt.transacted(() -> { context("superuser-alex@hostsharing.net", - "hs_office_partner#20033:ErbenBesslerMelBessler-ninthcontact.agent"); + "hs_office_person#ErbenBesslerMelBessler:ADMIN"); + givenPartner.getDetails().setBirthName("new birthname"); + return partnerRepo.save(givenPartner); + }); + + // then + result.assertSuccessful(); + } + + @Test + public void partnerRelationTenant_canNotUpdateRelatedPartner() { + // given + context("superuser-alex@hostsharing.net"); + final var givenPartner = givenSomeTemporaryHostsharingPartner(20037, "Erben Bessler", "ninth"); + assertThatPartnerIsVisibleForUserWithRole( + givenPartner, + "hs_office_person#ErbenBesslerMelBessler:ADMIN"); + assertThatPartnerActuallyInDatabase(givenPartner); + + // when + final var result = jpaAttempt.transacted(() -> { + context("superuser-alex@hostsharing.net", + "hs_office_relation#HostsharingeG-with-PARTNER-ErbenBesslerMelBessler:TENANT"); givenPartner.getDetails().setBirthName("new birthname"); return partnerRepo.save(givenPartner); }); // then result.assertExceptionWithRootCauseMessage(JpaSystemException.class, - "[403] Subject ", " is not allowed to update hs_office_partner_details uuid"); + "[403] insert into hs_office_partner_details not allowed for current subjects {hs_office_relation#HostsharingeG-with-PARTNER-ErbenBesslerMelBessler:TENANT}"); } private void assertThatPartnerActuallyInDatabase(final HsOfficePartnerEntity saved) { final var found = partnerRepo.findByUuid(saved.getUuid()); - assertThat(found).isNotEmpty().get().isNotSameAs(saved).usingRecursiveComparison().isEqualTo(saved); + assertThat(found).isNotEmpty().get().isNotSameAs(saved).extracting(HsOfficePartnerEntity::toString).isEqualTo(saved.toString()); } private void assertThatPartnerIsVisibleForUserWithRole( @@ -375,7 +367,7 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean public void globalAdmin_withoutAssumedRole_canDeleteAnyPartner() { // given context("superuser-alex@hostsharing.net", null); - final var givenPartner = givenSomeTemporaryPartnerBessler(20032, "Erben Bessler", "tenth"); + final var givenPartner = givenSomeTemporaryHostsharingPartner(20032, "Erben Bessler", "tenth"); // when final var result = jpaAttempt.transacted(() -> { @@ -395,7 +387,7 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean public void nonGlobalAdmin_canNotDeleteTheirRelatedPartner() { // given context("superuser-alex@hostsharing.net", null); - final var givenPartner = givenSomeTemporaryPartnerBessler(20032, "Erben Bessler", "eleventh"); + final var givenPartner = givenSomeTemporaryHostsharingPartner(20033, "Erben Bessler", "eleventh"); // when final var result = jpaAttempt.transacted(() -> { @@ -419,22 +411,21 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean public void deletingAPartnerAlsoDeletesRelatedRolesAndGrants() { // given context("superuser-alex@hostsharing.net"); + final var initialObjects = Array.from(objectDisplaysOf(rawObjectRepo.findAll())); final var initialRoleNames = Array.from(distinctRoleNamesOf(rawRoleRepo.findAll())); final var initialGrantNames = Array.from(distinctGrantDisplaysOf(rawGrantRepo.findAll())); - final var givenPartner = givenSomeTemporaryPartnerBessler(20034, "Erben Bessler", "twelfth"); + final var givenPartner = givenSomeTemporaryHostsharingPartner(20034, "Erben Bessler", "twelfth"); // when final var result = jpaAttempt.transacted(() -> { context("superuser-alex@hostsharing.net"); - // TODO: should deleting a partner automatically delete the PARTNER relationship? (same for debitor) - // TODO: why did the test cleanup check does not notice this, if missing? - return partnerRepo.deleteByUuid(givenPartner.getUuid()) + - relationshipRepo.deleteByUuid(givenPartner.getPartnerRole().getUuid()); + return partnerRepo.deleteByUuid(givenPartner.getUuid()); }); // then result.assertSuccessful(); - assertThat(result.returnedValue()).isEqualTo(2); // partner+relationship + assertThat(result.returnedValue()).isEqualTo(1); + assertThat(objectDisplaysOf(rawObjectRepo.findAll())).containsExactlyInAnyOrder(initialObjects); assertThat(distinctRoleNamesOf(rawRoleRepo.findAll())).containsExactlyInAnyOrder(initialRoleNames); assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(initialGrantNames); } @@ -458,28 +449,15 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean "[creating partner test-data Seconde.K.-secondcontact, hs_office_partner, INSERT]"); } - private HsOfficePartnerEntity givenSomeTemporaryPartnerBessler( + private HsOfficePartnerEntity givenSomeTemporaryHostsharingPartner( final Integer partnerNumber, final String person, final String contact) { return jpaAttempt.transacted(() -> { context("superuser-alex@hostsharing.net"); - final var givenMandantorPerson = personRepo.findPersonByOptionalNameLike("Hostsharing eG").get(0); - final var givenPartnerPerson = personRepo.findPersonByOptionalNameLike(person).get(0); - final var givenContact = contactRepo.findContactByOptionalLabelLike(contact).get(0); - - final var partnerRole = HsOfficeRelationshipEntity.builder() - .relHolder(givenPartnerPerson) - .relType(HsOfficeRelationshipType.PARTNER) - .relAnchor(givenMandantorPerson) - .contact(givenContact) - .build(); - relationshipRepo.save(partnerRole); - em.flush(); // TODO: why is that necessary? + final var partnerRel = givenSomeTemporaryHostsharingPartnerRel(person, contact); final var newPartner = HsOfficePartnerEntity.builder() .partnerNumber(partnerNumber) - .partnerRole(partnerRole) - .person(givenPartnerPerson) - .contact(givenContact) + .partnerRel(partnerRel) .details(HsOfficePartnerDetailsEntity.builder().build()) .build(); @@ -487,6 +465,21 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean }).assertSuccessful().returnedValue(); } + private HsOfficeRelationEntity givenSomeTemporaryHostsharingPartnerRel(final String person, final String contact) { + final var givenMandantorPerson = personRepo.findPersonByOptionalNameLike("Hostsharing eG").get(0); + final var givenPartnerPerson = personRepo.findPersonByOptionalNameLike(person).get(0); + final var givenContact = contactRepo.findContactByOptionalLabelLike(contact).get(0); + + final var partnerRel = HsOfficeRelationEntity.builder() + .holder(givenPartnerPerson) + .type(HsOfficeRelationType.PARTNER) + .anchor(givenMandantorPerson) + .contact(givenContact) + .build(); + relationRepo.save(partnerRel); + return partnerRel; + } + void exactlyThesePartnersAreReturned(final List actualResult, final String... partnerNames) { assertThat(actualResult) .extracting(partnerEntity -> partnerEntity.toString()) @@ -501,9 +494,7 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean @AfterEach void cleanup() { - cleanupAllNew(HsOfficePartnerDetailsEntity.class); // TODO: should not be necessary cleanupAllNew(HsOfficePartnerEntity.class); - cleanupAllNew(HsOfficeRelationshipEntity.class); } private String[] distinct(final String[] strings) { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/TestHsOfficePartner.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/TestHsOfficePartner.java index abbb8e09..ce1986b2 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/TestHsOfficePartner.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/TestHsOfficePartner.java @@ -2,7 +2,8 @@ package net.hostsharing.hsadminng.hs.office.partner; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; - +import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity; +import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType; import static net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType.LEGAL_PERSON; @@ -13,13 +14,22 @@ public class TestHsOfficePartner { static public HsOfficePartnerEntity hsOfficePartnerWithLegalPerson(final String tradeName) { return HsOfficePartnerEntity.builder() .partnerNumber(10001) - .person(HsOfficePersonEntity.builder() - .personType(LEGAL_PERSON) - .tradeName(tradeName) - .build()) - .contact(HsOfficeContactEntity.builder() - .label(tradeName) - .build()) + .partnerRel( + HsOfficeRelationEntity.builder() + .holder(HsOfficePersonEntity.builder() + .personType(LEGAL_PERSON) + .tradeName("Hostsharing eG") + .build()) + .type(HsOfficeRelationType.PARTNER) + .holder(HsOfficePersonEntity.builder() + .personType(LEGAL_PERSON) + .tradeName(tradeName) + .build()) + .contact(HsOfficeContactEntity.builder() + .label(tradeName) + .build()) + .build() + ) .build(); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonControllerAcceptanceTest.java index 78b9c290..072df1a7 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonControllerAcceptanceTest.java @@ -65,7 +65,7 @@ class HsOfficePersonControllerAcceptanceTest extends ContextBasedTestWithCleanup .then().log().all().assertThat() .statusCode(200) .contentType("application/json") - .body("", hasSize(12)); + .body("", hasSize(13)); // @formatter:on } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonRepositoryIntegrationTest.java index dd3e08c9..ca4d82d4 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonRepositoryIntegrationTest.java @@ -59,7 +59,6 @@ class HsOfficePersonRepositoryIntegrationTest extends ContextBasedTestWithCleanu final var count = personRepo.count(); // when - final var result = attempt(em, () -> toCleanup(personRepo.save( hsOfficePerson("a new person")))); @@ -91,35 +90,35 @@ class HsOfficePersonRepositoryIntegrationTest extends ContextBasedTestWithCleanu public void createsAndGrantsRoles() { // given context("selfregistered-user-drew@hostsharing.org"); - final var count = personRepo.count(); final var initialRoleNames = distinctRoleNamesOf(rawRoleRepo.findAll()); final var initialGrantNames = distinctGrantDisplaysOf(rawGrantRepo.findAll()); // when - attempt(em, () -> toCleanup(personRepo.save( - hsOfficePerson("another new person"))) - ).assumeSuccessful(); + attempt(em, () -> toCleanup( + personRepo.save(hsOfficePerson("another new person")) + )).assumeSuccessful(); // then assertThat(distinctRoleNamesOf(rawRoleRepo.findAll())).containsExactlyInAnyOrder( Array.from( initialRoleNames, - "hs_office_person#anothernewperson.owner", - "hs_office_person#anothernewperson.admin", - "hs_office_person#anothernewperson.tenant", - "hs_office_person#anothernewperson.guest" + "hs_office_person#anothernewperson:OWNER", + "hs_office_person#anothernewperson:ADMIN", + "hs_office_person#anothernewperson:REFERRER" )); assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder( - Array.from( + Array.fromFormatted( initialGrantNames, - "{ grant role hs_office_person#anothernewperson.owner to role global#global.admin by system and assume }", - "{ grant perm edit on hs_office_person#anothernewperson to role hs_office_person#anothernewperson.admin by system and assume }", - "{ grant role hs_office_person#anothernewperson.tenant to role hs_office_person#anothernewperson.admin by system and assume }", - "{ grant perm * on hs_office_person#anothernewperson to role hs_office_person#anothernewperson.owner by system and assume }", - "{ grant role hs_office_person#anothernewperson.admin to role hs_office_person#anothernewperson.owner by system and assume }", - "{ grant perm view on hs_office_person#anothernewperson to role hs_office_person#anothernewperson.guest by system and assume }", - "{ grant role hs_office_person#anothernewperson.guest to role hs_office_person#anothernewperson.tenant by system and assume }", - "{ grant role hs_office_person#anothernewperson.owner to user selfregistered-user-drew@hostsharing.org by global#global.admin and assume }" + "{ grant perm:hs_office_person#anothernewperson:INSERT>hs_office_relation to role:hs_office_person#anothernewperson:ADMIN by system and assume }", + + "{ grant role:hs_office_person#anothernewperson:OWNER to user:selfregistered-user-drew@hostsharing.org by hs_office_person#anothernewperson:OWNER and assume }", + "{ grant role:hs_office_person#anothernewperson:OWNER to role:global#global:ADMIN by system and assume }", + "{ grant perm:hs_office_person#anothernewperson:UPDATE to role:hs_office_person#anothernewperson:ADMIN by system and assume }", + "{ grant perm:hs_office_person#anothernewperson:DELETE to role:hs_office_person#anothernewperson:OWNER by system and assume }", + "{ grant role:hs_office_person#anothernewperson:ADMIN to role:hs_office_person#anothernewperson:OWNER by system and assume }", + + "{ grant perm:hs_office_person#anothernewperson:SELECT to role:hs_office_person#anothernewperson:REFERRER by system and assume }", + "{ grant role:hs_office_person#anothernewperson:REFERRER to role:hs_office_person#anothernewperson:ADMIN by system and assume }" )); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationControllerAcceptanceTest.java similarity index 63% rename from src/test/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipControllerAcceptanceTest.java rename to src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationControllerAcceptanceTest.java index 8f9e9147..78d64e6a 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationControllerAcceptanceTest.java @@ -1,4 +1,4 @@ -package net.hostsharing.hsadminng.hs.office.relationship; +package net.hostsharing.hsadminng.hs.office.relation; import io.restassured.RestAssured; import io.restassured.http.ContentType; @@ -7,7 +7,7 @@ import net.hostsharing.test.Accepts; import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRepository; -import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeRelationshipTypeResource; +import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeRelationTypeResource; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository; import net.hostsharing.test.JpaAttempt; import org.json.JSONException; @@ -31,7 +31,7 @@ import static org.hamcrest.Matchers.startsWith; classes = { HsadminNgApplication.class, JpaAttempt.class } ) @Transactional -class HsOfficeRelationshipControllerAcceptanceTest extends ContextBasedTestWithCleanup { +class HsOfficeRelationControllerAcceptanceTest extends ContextBasedTestWithCleanup { public static final UUID GIVEN_NON_EXISTING_HOLDER_PERSON_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000"); @LocalServerPort @@ -44,7 +44,7 @@ class HsOfficeRelationshipControllerAcceptanceTest extends ContextBasedTestWithC Context contextMock; @Autowired - HsOfficeRelationshipRepository relationshipRepo; + HsOfficeRelationRepository relationRepo; @Autowired HsOfficePersonRepository personRepo; @@ -56,11 +56,11 @@ class HsOfficeRelationshipControllerAcceptanceTest extends ContextBasedTestWithC JpaAttempt jpaAttempt; @Nested - @Accepts({ "Relationship:F(Find)" }) - class ListRelationships { + @Accepts({ "Relation:F(Find)" }) + class ListRelations { @Test - void globalAdmin_withoutAssumedRoles_canViewAllRelationshipsOfGivenPersonAndType_ifNoCriteriaGiven() throws JSONException { + void globalAdmin_withoutAssumedRoles_canViewAllRelationsOfGivenPersonAndType_ifNoCriteriaGiven() throws JSONException { // given context.define("superuser-alex@hostsharing.net"); @@ -71,45 +71,45 @@ class HsOfficeRelationshipControllerAcceptanceTest extends ContextBasedTestWithC .header("current-user", "superuser-alex@hostsharing.net") .port(port) .when() - .get("http://localhost/api/hs/office/relationships?personUuid=%s&relationshipType=%s" - .formatted(givenPerson.getUuid(), HsOfficeRelationshipTypeResource.PARTNER)) + .get("http://localhost/api/hs/office/relations?personUuid=%s&relationType=%s" + .formatted(givenPerson.getUuid(), HsOfficeRelationTypeResource.PARTNER)) .then().log().all().assertThat() .statusCode(200) .contentType("application/json") .body("", lenientlyEquals(""" [ { - "relAnchor": { "personType": "LEGAL_PERSON", "tradeName": "Hostsharing eG" }, - "relHolder": { "personType": "LEGAL_PERSON", "tradeName": "First GmbH" }, - "relType": "PARTNER", - "relMark": null, + "anchor": { "personType": "LEGAL_PERSON", "tradeName": "Hostsharing eG" }, + "holder": { "personType": "LEGAL_PERSON", "tradeName": "First GmbH" }, + "type": "PARTNER", + "mark": null, "contact": { "label": "first contact" } }, { - "relAnchor": { "personType": "LEGAL_PERSON", "tradeName": "Hostsharing eG" }, - "relHolder": { "personType": "INCORPORATED_FIRM", "tradeName": "Fourth eG" }, - "relType": "PARTNER", + "anchor": { "personType": "LEGAL_PERSON", "tradeName": "Hostsharing eG" }, + "holder": { "personType": "LEGAL_PERSON", "tradeName": "Fourth eG" }, + "type": "PARTNER", "contact": { "label": "fourth contact" } }, { - "relAnchor": { "personType": "LEGAL_PERSON", "tradeName": "Hostsharing eG" }, - "relHolder": { "personType": "LEGAL_PERSON", "tradeName": "Second e.K.", "givenName": "Peter", "familyName": "Smith" }, - "relType": "PARTNER", - "relMark": null, + "anchor": { "personType": "LEGAL_PERSON", "tradeName": "Hostsharing eG" }, + "holder": { "personType": "LEGAL_PERSON", "tradeName": "Second e.K.", "givenName": "Peter", "familyName": "Smith" }, + "type": "PARTNER", + "mark": null, "contact": { "label": "second contact" } }, { - "relAnchor": { "personType": "LEGAL_PERSON", "tradeName": "Hostsharing eG" }, - "relHolder": { "personType": "NATURAL_PERSON", "givenName": "Peter", "familyName": "Smith" }, - "relType": "PARTNER", - "relMark": null, + "anchor": { "personType": "LEGAL_PERSON", "tradeName": "Hostsharing eG" }, + "holder": { "personType": "NATURAL_PERSON", "givenName": "Peter", "familyName": "Smith" }, + "type": "PARTNER", + "mark": null, "contact": { "label": "sixth contact" } }, { - "relAnchor": { "personType": "LEGAL_PERSON", "tradeName": "Hostsharing eG" }, - "relHolder": { "personType": "INCORPORATED_FIRM", "tradeName": "Third OHG" }, - "relType": "PARTNER", - "relMark": null, + "anchor": { "personType": "LEGAL_PERSON", "tradeName": "Hostsharing eG" }, + "holder": { "personType": "INCORPORATED_FIRM", "tradeName": "Third OHG" }, + "type": "PARTNER", + "mark": null, "contact": { "label": "third contact" } } ] @@ -119,11 +119,11 @@ class HsOfficeRelationshipControllerAcceptanceTest extends ContextBasedTestWithC } @Nested - @Accepts({ "Relationship:C(Create)" }) - class AddRelationship { + @Accepts({ "Relation:C(Create)" }) + class AddRelation { @Test - void globalAdmin_withoutAssumedRole_canAddRelationship() { + void globalAdmin_withoutAssumedRole_canAddRelation() { context.define("superuser-alex@hostsharing.net"); final var givenAnchorPerson = personRepo.findPersonByOptionalNameLike("Third").get(0); @@ -136,38 +136,41 @@ class HsOfficeRelationshipControllerAcceptanceTest extends ContextBasedTestWithC .contentType(ContentType.JSON) .body(""" { - "relType": "%s", - "relAnchorUuid": "%s", - "relHolderUuid": "%s", + "type": "%s", + "mark": "%s", + "anchorUuid": "%s", + "holderUuid": "%s", "contactUuid": "%s" } """.formatted( - HsOfficeRelationshipTypeResource.ACCOUNTING, + HsOfficeRelationTypeResource.SUBSCRIBER, + "operations-discuss", givenAnchorPerson.getUuid(), givenHolderPerson.getUuid(), givenContact.getUuid())) .port(port) .when() - .post("http://localhost/api/hs/office/relationships") + .post("http://localhost/api/hs/office/relations") .then().log().all().assertThat() .statusCode(201) .contentType(ContentType.JSON) .body("uuid", isUuidValid()) - .body("relType", is("ACCOUNTING")) - .body("relAnchor.tradeName", is("Third OHG")) - .body("relHolder.givenName", is("Paul")) + .body("type", is("SUBSCRIBER")) + .body("mark", is("operations-discuss")) + .body("anchor.tradeName", is("Third OHG")) + .body("holder.givenName", is("Paul")) .body("contact.label", is("second contact")) .header("Location", startsWith("http://localhost")) .extract().header("Location"); // @formatter:on - // finally, the new relationship can be accessed under the generated UUID - final var newUserUuid = toCleanup(HsOfficeRelationshipEntity.class, UUID.fromString( + // finally, the new relation can be accessed under the generated UUID + final var newUserUuid = toCleanup(HsOfficeRelationEntity.class, UUID.fromString( location.substring(location.lastIndexOf('/') + 1))); assertThat(newUserUuid).isNotNull(); } @Test - void globalAdmin_canNotAddRelationship_ifAnchorPersonDoesNotExist() { + void globalAdmin_canNotAddRelation_ifAnchorPersonDoesNotExist() { context.define("superuser-alex@hostsharing.net"); final var givenAnchorPersonUuid = GIVEN_NON_EXISTING_HOLDER_PERSON_UUID; @@ -180,27 +183,27 @@ class HsOfficeRelationshipControllerAcceptanceTest extends ContextBasedTestWithC .contentType(ContentType.JSON) .body(""" { - "relType": "%s", - "relAnchorUuid": "%s", - "relHolderUuid": "%s", + "type": "%s", + "anchorUuid": "%s", + "holderUuid": "%s", "contactUuid": "%s" } """.formatted( - HsOfficeRelationshipTypeResource.ACCOUNTING, + HsOfficeRelationTypeResource.DEBITOR, givenAnchorPersonUuid, givenHolderPerson.getUuid(), givenContact.getUuid())) .port(port) .when() - .post("http://localhost/api/hs/office/relationships") + .post("http://localhost/api/hs/office/relations") .then().log().all().assertThat() .statusCode(404) - .body("message", is("cannot find relAnchorUuid " + GIVEN_NON_EXISTING_HOLDER_PERSON_UUID)); + .body("message", is("cannot find anchorUuid " + GIVEN_NON_EXISTING_HOLDER_PERSON_UUID)); // @formatter:on } @Test - void globalAdmin_canNotAddRelationship_ifHolderPersonDoesNotExist() { + void globalAdmin_canNotAddRelation_ifHolderPersonDoesNotExist() { context.define("superuser-alex@hostsharing.net"); final var givenAnchorPerson = personRepo.findPersonByOptionalNameLike("Third").get(0); @@ -212,27 +215,27 @@ class HsOfficeRelationshipControllerAcceptanceTest extends ContextBasedTestWithC .contentType(ContentType.JSON) .body(""" { - "relType": "%s", - "relAnchorUuid": "%s", - "relHolderUuid": "%s", + "type": "%s", + "anchorUuid": "%s", + "holderUuid": "%s", "contactUuid": "%s" } """.formatted( - HsOfficeRelationshipTypeResource.ACCOUNTING, + HsOfficeRelationTypeResource.DEBITOR, givenAnchorPerson.getUuid(), GIVEN_NON_EXISTING_HOLDER_PERSON_UUID, givenContact.getUuid())) .port(port) .when() - .post("http://localhost/api/hs/office/relationships") + .post("http://localhost/api/hs/office/relations") .then().log().all().assertThat() .statusCode(404) - .body("message", is("cannot find relHolderUuid " + GIVEN_NON_EXISTING_HOLDER_PERSON_UUID)); + .body("message", is("cannot find holderUuid " + GIVEN_NON_EXISTING_HOLDER_PERSON_UUID)); // @formatter:on } @Test - void globalAdmin_canNotAddRelationship_ifContactDoesNotExist() { + void globalAdmin_canNotAddRelation_ifContactDoesNotExist() { context.define("superuser-alex@hostsharing.net"); final var givenAnchorPerson = personRepo.findPersonByOptionalNameLike("Third").get(0); @@ -245,19 +248,19 @@ class HsOfficeRelationshipControllerAcceptanceTest extends ContextBasedTestWithC .contentType(ContentType.JSON) .body(""" { - "relType": "%s", - "relAnchorUuid": "%s", - "relHolderUuid": "%s", + "type": "%s", + "anchorUuid": "%s", + "holderUuid": "%s", "contactUuid": "%s" } """.formatted( - HsOfficeRelationshipTypeResource.ACCOUNTING, + HsOfficeRelationTypeResource.DEBITOR, givenAnchorPerson.getUuid(), givenHolderPerson.getUuid(), givenContactUuid)) .port(port) .when() - .post("http://localhost/api/hs/office/relationships") + .post("http://localhost/api/hs/office/relations") .then().log().all().assertThat() .statusCode(404) .body("message", is("cannot find contactUuid 00000000-0000-0000-0000-000000000000")); @@ -266,97 +269,97 @@ class HsOfficeRelationshipControllerAcceptanceTest extends ContextBasedTestWithC } @Nested - @Accepts({ "Relationship:R(Read)" }) - class GetRelationship { + @Accepts({ "Relation:R(Read)" }) + class GetRelation { @Test - void globalAdmin_withoutAssumedRole_canGetArbitraryRelationship() { + void globalAdmin_withoutAssumedRole_canGetArbitraryRelation() { context.define("superuser-alex@hostsharing.net"); - final UUID givenRelationshipUuid = findRelationship("First", "Firby").getUuid(); + final UUID givenRelationUuid = findRelation("First", "Firby").getUuid(); RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") .port(port) .when() - .get("http://localhost/api/hs/office/relationships/" + givenRelationshipUuid) + .get("http://localhost/api/hs/office/relations/" + givenRelationUuid) .then().log().body().assertThat() .statusCode(200) .contentType("application/json") .body("", lenientlyEquals(""" { - "relAnchor": { "tradeName": "First GmbH" }, - "relHolder": { "familyName": "Firby" }, + "anchor": { "tradeName": "First GmbH" }, + "holder": { "familyName": "Firby" }, "contact": { "label": "first contact" } } """)); // @formatter:on } @Test - @Accepts({ "Relationship:X(Access Control)" }) - void normalUser_canNotGetUnrelatedRelationship() { + @Accepts({ "Relation:X(Access Control)" }) + void normalUser_canNotGetUnrelatedRelation() { context.define("superuser-alex@hostsharing.net"); - final UUID givenRelationshipUuid = findRelationship("First", "Firby").getUuid(); + final UUID givenRelationUuid = findRelation("First", "Firby").getUuid(); RestAssured // @formatter:off .given() .header("current-user", "selfregistered-user-drew@hostsharing.org") .port(port) .when() - .get("http://localhost/api/hs/office/relationships/" + givenRelationshipUuid) + .get("http://localhost/api/hs/office/relations/" + givenRelationUuid) .then().log().body().assertThat() .statusCode(404); // @formatter:on } @Test - @Accepts({ "Relationship:X(Access Control)" }) - void contactAdminUser_canGetRelatedRelationship() { + @Accepts({ "Relation:X(Access Control)" }) + void contactAdminUser_canGetRelatedRelation() { context.define("superuser-alex@hostsharing.net"); - final var givenRelationship = findRelationship("First", "Firby"); - assertThat(givenRelationship.getContact().getLabel()).isEqualTo("first contact"); + final var givenRelation = findRelation("First", "Firby"); + assertThat(givenRelation.getContact().getLabel()).isEqualTo("first contact"); RestAssured // @formatter:off .given() .header("current-user", "contact-admin@firstcontact.example.com") .port(port) .when() - .get("http://localhost/api/hs/office/relationships/" + givenRelationship.getUuid()) + .get("http://localhost/api/hs/office/relations/" + givenRelation.getUuid()) .then().log().body().assertThat() .statusCode(200) .contentType("application/json") .body("", lenientlyEquals(""" { - "relAnchor": { "tradeName": "First GmbH" }, - "relHolder": { "familyName": "Firby" }, + "anchor": { "tradeName": "First GmbH" }, + "holder": { "familyName": "Firby" }, "contact": { "label": "first contact" } } """)); // @formatter:on } } - private HsOfficeRelationshipEntity findRelationship( + private HsOfficeRelationEntity findRelation( final String anchorPersonName, final String holderPersoneName) { final var anchorPersonUuid = personRepo.findPersonByOptionalNameLike(anchorPersonName).get(0).getUuid(); final var holderPersonUuid = personRepo.findPersonByOptionalNameLike(holderPersoneName).get(0).getUuid(); - final var givenRelationship = relationshipRepo - .findRelationshipRelatedToPersonUuid(anchorPersonUuid) + final var givenRelation = relationRepo + .findRelationRelatedToPersonUuid(anchorPersonUuid) .stream() - .filter(r -> r.getRelHolder().getUuid().equals(holderPersonUuid)) + .filter(r -> r.getHolder().getUuid().equals(holderPersonUuid)) .findFirst().orElseThrow(); - return givenRelationship; + return givenRelation; } @Nested - @Accepts({ "Relationship:U(Update)" }) - class PatchRelationship { + @Accepts({ "Relation:U(Update)" }) + class PatchRelation { @Test - void globalAdmin_withoutAssumedRole_canPatchContactOfArbitraryRelationship() { + void globalAdmin_withoutAssumedRole_canPatchContactOfArbitraryRelation() { context.define("superuser-alex@hostsharing.net"); - final var givenRelationship = givenSomeTemporaryRelationshipBessler(); - assertThat(givenRelationship.getContact().getLabel()).isEqualTo("seventh contact"); + final var givenRelation = givenSomeTemporaryRelationBessler(); + assertThat(givenRelation.getContact().getLabel()).isEqualTo("seventh contact"); final var givenContact = contactRepo.findContactByOptionalLabelLike("fourth").get(0); final var location = RestAssured // @formatter:off @@ -370,109 +373,109 @@ class HsOfficeRelationshipControllerAcceptanceTest extends ContextBasedTestWithC """.formatted(givenContact.getUuid())) .port(port) .when() - .patch("http://localhost/api/hs/office/relationships/" + givenRelationship.getUuid()) + .patch("http://localhost/api/hs/office/relations/" + givenRelation.getUuid()) .then().log().all().assertThat() .statusCode(200) .contentType(ContentType.JSON) .body("uuid", isUuidValid()) - .body("relType", is("REPRESENTATIVE")) - .body("relAnchor.tradeName", is("Erben Bessler")) - .body("relHolder.familyName", is("Winkler")) + .body("type", is("REPRESENTATIVE")) + .body("anchor.tradeName", is("Erben Bessler")) + .body("holder.familyName", is("Winkler")) .body("contact.label", is("fourth contact")); // @formatter:on - // finally, the relationship is actually updated + // finally, the relation is actually updated context.define("superuser-alex@hostsharing.net"); - assertThat(relationshipRepo.findByUuid(givenRelationship.getUuid())).isPresent().get() + assertThat(relationRepo.findByUuid(givenRelation.getUuid())).isPresent().get() .matches(rel -> { - assertThat(rel.getRelAnchor().getTradeName()).contains("Bessler"); - assertThat(rel.getRelHolder().getFamilyName()).contains("Winkler"); + assertThat(rel.getAnchor().getTradeName()).contains("Bessler"); + assertThat(rel.getHolder().getFamilyName()).contains("Winkler"); assertThat(rel.getContact().getLabel()).isEqualTo("fourth contact"); - assertThat(rel.getRelType()).isEqualTo(HsOfficeRelationshipType.REPRESENTATIVE); + assertThat(rel.getType()).isEqualTo(HsOfficeRelationType.REPRESENTATIVE); return true; }); } } @Nested - @Accepts({ "Relationship:D(Delete)" }) - class DeleteRelationship { + @Accepts({ "Relation:D(Delete)" }) + class DeleteRelation { @Test - void globalAdmin_withoutAssumedRole_canDeleteArbitraryRelationship() { + void globalAdmin_withoutAssumedRole_canDeleteArbitraryRelation() { context.define("superuser-alex@hostsharing.net"); - final var givenRelationship = givenSomeTemporaryRelationshipBessler(); + final var givenRelation = givenSomeTemporaryRelationBessler(); RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") .port(port) .when() - .delete("http://localhost/api/hs/office/relationships/" + givenRelationship.getUuid()) + .delete("http://localhost/api/hs/office/relations/" + givenRelation.getUuid()) .then().log().body().assertThat() .statusCode(204); // @formatter:on - // then the given relationship is gone - assertThat(relationshipRepo.findByUuid(givenRelationship.getUuid())).isEmpty(); + // then the given relation is gone + assertThat(relationRepo.findByUuid(givenRelation.getUuid())).isEmpty(); } @Test - @Accepts({ "Relationship:X(Access Control)" }) - void contactAdminUser_canNotDeleteRelatedRelationship() { + @Accepts({ "Relation:X(Access Control)" }) + void contactAdminUser_canNotDeleteRelatedRelation() { context.define("superuser-alex@hostsharing.net"); - final var givenRelationship = givenSomeTemporaryRelationshipBessler(); - assertThat(givenRelationship.getContact().getLabel()).isEqualTo("seventh contact"); + final var givenRelation = givenSomeTemporaryRelationBessler(); + assertThat(givenRelation.getContact().getLabel()).isEqualTo("seventh contact"); RestAssured // @formatter:off .given() .header("current-user", "contact-admin@seventhcontact.example.com") .port(port) .when() - .delete("http://localhost/api/hs/office/relationships/" + givenRelationship.getUuid()) + .delete("http://localhost/api/hs/office/relations/" + givenRelation.getUuid()) .then().log().body().assertThat() .statusCode(403); // @formatter:on - // then the given relationship is still there - assertThat(relationshipRepo.findByUuid(givenRelationship.getUuid())).isNotEmpty(); + // then the given relation is still there + assertThat(relationRepo.findByUuid(givenRelation.getUuid())).isNotEmpty(); } @Test - @Accepts({ "Relationship:X(Access Control)" }) - void normalUser_canNotDeleteUnrelatedRelationship() { + @Accepts({ "Relation:X(Access Control)" }) + void normalUser_canNotDeleteUnrelatedRelation() { context.define("superuser-alex@hostsharing.net"); - final var givenRelationship = givenSomeTemporaryRelationshipBessler(); - assertThat(givenRelationship.getContact().getLabel()).isEqualTo("seventh contact"); + final var givenRelation = givenSomeTemporaryRelationBessler(); + assertThat(givenRelation.getContact().getLabel()).isEqualTo("seventh contact"); RestAssured // @formatter:off .given() .header("current-user", "selfregistered-user-drew@hostsharing.org") .port(port) .when() - .delete("http://localhost/api/hs/office/relationships/" + givenRelationship.getUuid()) + .delete("http://localhost/api/hs/office/relations/" + givenRelation.getUuid()) .then().log().body().assertThat() .statusCode(404); // @formatter:on - // then the given relationship is still there - assertThat(relationshipRepo.findByUuid(givenRelationship.getUuid())).isNotEmpty(); + // then the given relation is still there + assertThat(relationRepo.findByUuid(givenRelation.getUuid())).isNotEmpty(); } } - private HsOfficeRelationshipEntity givenSomeTemporaryRelationshipBessler() { + private HsOfficeRelationEntity givenSomeTemporaryRelationBessler() { return jpaAttempt.transacted(() -> { context.define("superuser-alex@hostsharing.net"); final var givenAnchorPerson = personRepo.findPersonByOptionalNameLike("Erben Bessler").get(0); final var givenHolderPerson = personRepo.findPersonByOptionalNameLike("Winkler").get(0); final var givenContact = contactRepo.findContactByOptionalLabelLike("seventh contact").get(0); - final var newRelationship = HsOfficeRelationshipEntity.builder() - .relType(HsOfficeRelationshipType.REPRESENTATIVE) - .relAnchor(givenAnchorPerson) - .relHolder(givenHolderPerson) + final var newRelation = HsOfficeRelationEntity.builder() + .type(HsOfficeRelationType.REPRESENTATIVE) + .anchor(givenAnchorPerson) + .holder(givenHolderPerson) .contact(givenContact) .build(); - assertThat(toCleanup(relationshipRepo.save(newRelationship))).isEqualTo(newRelationship); + assertThat(toCleanup(relationRepo.save(newRelation))).isEqualTo(newRelation); - return newRelationship; + return newRelation; }).assertSuccessful().returnedValue(); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityPatcherUnitTest.java similarity index 67% rename from src/test/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipEntityPatcherUnitTest.java rename to src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityPatcherUnitTest.java index 1c12a629..6aec1b25 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityPatcherUnitTest.java @@ -1,7 +1,7 @@ -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.hs.office.person.HsOfficePersonEntity; import net.hostsharing.test.PatchUnitTestBase; import org.junit.jupiter.api.BeforeEach; @@ -21,12 +21,12 @@ import static org.mockito.Mockito.lenient; @TestInstance(PER_CLASS) @ExtendWith(MockitoExtension.class) -class HsOfficeRelationshipEntityPatcherUnitTest extends PatchUnitTestBase< - HsOfficeRelationshipPatchResource, - HsOfficeRelationshipEntity +class HsOfficeRelationEntityPatcherUnitTest extends PatchUnitTestBase< + HsOfficeRelationPatchResource, + HsOfficeRelationEntity > { - static final UUID INITIAL_RELATIONSHIP_UUID = UUID.randomUUID(); + static final UUID INITIAL_RELATION_UUID = UUID.randomUUID(); static final UUID PATCHED_CONTACT_UUID = UUID.randomUUID(); @Mock @@ -49,24 +49,24 @@ class HsOfficeRelationshipEntityPatcherUnitTest extends PatchUnitTestBase< .build(); @Override - protected HsOfficeRelationshipEntity newInitialEntity() { - final var entity = new HsOfficeRelationshipEntity(); - entity.setUuid(INITIAL_RELATIONSHIP_UUID); - entity.setRelType(HsOfficeRelationshipType.REPRESENTATIVE); - entity.setRelAnchor(givenInitialAnchorPerson); - entity.setRelHolder(givenInitialHolderPerson); + protected HsOfficeRelationEntity newInitialEntity() { + final var entity = new HsOfficeRelationEntity(); + entity.setUuid(INITIAL_RELATION_UUID); + entity.setType(HsOfficeRelationType.REPRESENTATIVE); + entity.setAnchor(givenInitialAnchorPerson); + entity.setHolder(givenInitialHolderPerson); entity.setContact(givenInitialContact); return entity; } @Override - protected HsOfficeRelationshipPatchResource newPatchResource() { - return new HsOfficeRelationshipPatchResource(); + protected HsOfficeRelationPatchResource newPatchResource() { + return new HsOfficeRelationPatchResource(); } @Override - protected HsOfficeRelationshipEntityPatcher createPatcher(final HsOfficeRelationshipEntity relationship) { - return new HsOfficeRelationshipEntityPatcher(em, relationship); + protected HsOfficeRelationEntityPatcher createPatcher(final HsOfficeRelationEntity relation) { + return new HsOfficeRelationEntityPatcher(em, relation); } @Override @@ -74,9 +74,9 @@ class HsOfficeRelationshipEntityPatcherUnitTest extends PatchUnitTestBase< return Stream.of( new JsonNullableProperty<>( "contact", - HsOfficeRelationshipPatchResource::setContactUuid, + HsOfficeRelationPatchResource::setContactUuid, PATCHED_CONTACT_UUID, - HsOfficeRelationshipEntity::setContact, + HsOfficeRelationEntity::setContact, newContact(PATCHED_CONTACT_UUID)) .notNullable() ); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityUnitTest.java new file mode 100644 index 00000000..bf2a7ed3 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntityUnitTest.java @@ -0,0 +1,43 @@ +package net.hostsharing.hsadminng.hs.office.relation; + +import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; +import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class HsOfficeRelationEntityUnitTest { + + private HsOfficePersonEntity anchor = HsOfficePersonEntity.builder() + .personType(HsOfficePersonType.LEGAL_PERSON) + .tradeName("some trade name") + .build(); + private HsOfficePersonEntity holder = HsOfficePersonEntity.builder() + .personType(HsOfficePersonType.NATURAL_PERSON) + .familyName("Meier") + .givenName("Mellie") + .build(); + + @Test + void toStringReturnsAllProperties() { + final var given = HsOfficeRelationEntity.builder() + .type(HsOfficeRelationType.SUBSCRIBER) + .mark("members-announce") + .anchor(anchor) + .holder(holder) + .build(); + + assertThat(given.toString()).isEqualTo("rel(anchor='LP some trade name', type='SUBSCRIBER', mark='members-announce', holder='NP Meier, Mellie')"); + } + + @Test + void toShortString() { + final var given = HsOfficeRelationEntity.builder() + .type(HsOfficeRelationType.REPRESENTATIVE) + .anchor(anchor) + .holder(holder) + .build(); + + assertThat(given.toShortString()).isEqualTo("rel(anchor='LP some trade name', type='REPRESENTATIVE', holder='NP Meier, Mellie')"); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java new file mode 100644 index 00000000..f474de0c --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java @@ -0,0 +1,443 @@ +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.person.HsOfficePersonRepository; +import net.hostsharing.hsadminng.hs.office.test.ContextBasedTestWithCleanup; +import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository; +import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository; +import net.hostsharing.test.Array; +import net.hostsharing.test.JpaAttempt; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.orm.jpa.JpaSystemException; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.servlet.http.HttpServletRequest; +import java.util.Arrays; +import java.util.List; + +import static net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType.NATURAL_PERSON; +import static net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType.UNINCORPORATED_FIRM; +import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf; +import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.distinctRoleNamesOf; +import static net.hostsharing.test.JpaAttempt.attempt; +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@Import( { Context.class, JpaAttempt.class }) +class HsOfficeRelationRepositoryIntegrationTest extends ContextBasedTestWithCleanup { + + @Autowired + HsOfficeRelationRepository relationRepo; + + @Autowired + HsOfficePersonRepository personRepo; + + @Autowired + HsOfficeContactRepository contactRepo; + + @Autowired + RawRbacRoleRepository rawRoleRepo; + + @Autowired + RawRbacGrantRepository rawGrantRepo; + + @PersistenceContext + EntityManager em; + + @Autowired + JpaAttempt jpaAttempt; + + @MockBean + HttpServletRequest request; + + @Nested + class CreateRelation { + + @Test + public void testHostsharingAdmin_withoutAssumedRole_canCreateNewRelation() { + // given + context("superuser-alex@hostsharing.net"); + final var count = relationRepo.count(); + final var givenAnchorPerson = personRepo.findPersonByOptionalNameLike("Bessler").stream() + .filter(p -> p.getPersonType() == UNINCORPORATED_FIRM) + .findFirst().orElseThrow(); + final var givenHolderPerson = personRepo.findPersonByOptionalNameLike("Paul").stream() + .filter(p -> p.getPersonType() == NATURAL_PERSON) + .findFirst().orElseThrow(); + final var givenContact = contactRepo.findContactByOptionalLabelLike("fourth contact").stream() + .findFirst().orElseThrow(); + + // when + final var result = attempt(em, () -> { + final var newRelation = HsOfficeRelationEntity.builder() + .anchor(givenAnchorPerson) + .holder(givenHolderPerson) + .type(HsOfficeRelationType.SUBSCRIBER) + .mark("operations-announce") + .contact(givenContact) + .build(); + return toCleanup(relationRepo.save(newRelation)); + }); + + // then + result.assertSuccessful(); + assertThat(result.returnedValue()).isNotNull().extracting(HsOfficeRelationEntity::getUuid).isNotNull(); + assertThatRelationIsPersisted(result.returnedValue()); + assertThat(relationRepo.count()).isEqualTo(count + 1); + final var stored = relationRepo.findByUuid(result.returnedValue().getUuid()); + assertThat(stored).isNotEmpty().map(HsOfficeRelationEntity::toString).get() + .isEqualTo("rel(anchor='UF Erben Bessler', type='SUBSCRIBER', mark='operations-announce', holder='NP Winkler, Paul', contact='fourth contact')"); + } + + @Test + public void createsAndGrantsRoles() { + // given + context("superuser-alex@hostsharing.net"); + final var initialRoleNames = distinctRoleNamesOf(rawRoleRepo.findAll()); + final var initialGrantNames = distinctGrantDisplaysOf(rawGrantRepo.findAll()); + + // when + attempt(em, () -> { + final var givenAnchorPerson = personRepo.findPersonByOptionalNameLike("Bessler").stream() + .filter(p -> p.getPersonType() == UNINCORPORATED_FIRM) + .findFirst().orElseThrow(); + final var givenHolderPerson = personRepo.findPersonByOptionalNameLike("Bert").stream() + .filter(p -> p.getPersonType() == NATURAL_PERSON) + .findFirst().orElseThrow(); + final var givenContact = contactRepo.findContactByOptionalLabelLike("fourth contact").stream() + .findFirst().orElseThrow(); + final var newRelation = HsOfficeRelationEntity.builder() + .anchor(givenAnchorPerson) + .holder(givenHolderPerson) + .type(HsOfficeRelationType.REPRESENTATIVE) + .contact(givenContact) + .build(); + return toCleanup(relationRepo.save(newRelation)); + }); + + // then + assertThat(distinctRoleNamesOf(rawRoleRepo.findAll())).containsExactlyInAnyOrder(Array.from( + initialRoleNames, + "hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:OWNER", + "hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:ADMIN", + "hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:AGENT", + "hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:TENANT")); + assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(Array.fromFormatted( + initialGrantNames, + // TODO: this grant should only be created for DEBITOR-Relationships, thus the RBAC DSL needs to support conditional grants + "{ grant perm:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:INSERT>hs_office_sepamandate to role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:ADMIN by system and assume }", + + "{ grant perm:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:DELETE to role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:OWNER by system and assume }", + "{ grant role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:OWNER to role:global#global:ADMIN by system and assume }", + "{ grant role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:OWNER to user:superuser-alex@hostsharing.net by hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:OWNER and assume }", + + "{ grant perm:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:UPDATE to role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:ADMIN by system and assume }", + "{ grant role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:ADMIN to role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:OWNER by system and assume }", + "{ grant role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:ADMIN to role:hs_office_person#ErbenBesslerMelBessler:ADMIN by system and assume }", + + "{ grant role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:AGENT to role:hs_office_person#BesslerBert:ADMIN by system and assume }", + "{ grant role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:AGENT to role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:ADMIN by system and assume }", + + "{ grant perm:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:SELECT to role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:TENANT by system and assume }", + "{ grant role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:TENANT to role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:AGENT by system and assume }", + "{ grant role:hs_office_person#BesslerBert:REFERRER to role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:TENANT by system and assume }", + "{ grant role:hs_office_person#ErbenBesslerMelBessler:REFERRER to role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:TENANT by system and assume }", + "{ grant role:hs_office_contact#fourthcontact:REFERRER to role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:TENANT by system and assume }", + + // REPRESENTATIVE holder person -> (represented) anchor person + "{ grant role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:TENANT to role:hs_office_contact#fourthcontact:ADMIN by system and assume }", + "{ grant role:hs_office_relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerBert:TENANT to role:hs_office_person#BesslerBert:ADMIN by system and assume }", + + null) + ); + } + + private void assertThatRelationIsPersisted(final HsOfficeRelationEntity saved) { + final var found = relationRepo.findByUuid(saved.getUuid()); + assertThat(found).isNotEmpty().get().usingRecursiveComparison().isEqualTo(saved); + } + } + + @Nested + class FindAllRelations { + + @Test + public void globalAdmin_withoutAssumedRole_canViewAllRelationsOfArbitraryPerson() { + // given + context("superuser-alex@hostsharing.net"); + final var person = personRepo.findPersonByOptionalNameLike("Smith").stream() + .filter(p -> p.getPersonType() == NATURAL_PERSON) + .findFirst().orElseThrow(); + + // when + final var result = relationRepo.findRelationRelatedToPersonUuid(person.getUuid()); + + // then + allTheseRelationsAreReturned( + result, + "rel(anchor='LP Hostsharing eG', type='PARTNER', holder='NP Smith, Peter', contact='sixth contact')", + "rel(anchor='LP Second e.K.', type='REPRESENTATIVE', holder='NP Smith, Peter', contact='second contact')", + "rel(anchor='IF Third OHG', type='SUBSCRIBER', mark='members-announce', holder='NP Smith, Peter', contact='third contact')"); + } + + @Test + public void normalUser_canViewRelationsOfOwnedPersons() { + // given: + context("person-SmithPeter@example.com"); + final var person = personRepo.findPersonByOptionalNameLike("Smith").stream() + .filter(p -> p.getPersonType() == NATURAL_PERSON) + .findFirst().orElseThrow(); + + // when: + final var result = relationRepo.findRelationRelatedToPersonUuid(person.getUuid()); + + // then: + exactlyTheseRelationsAreReturned( + result, + "rel(anchor='LP Second e.K.', type='REPRESENTATIVE', holder='NP Smith, Peter', contact='second contact')", + "rel(anchor='IF Third OHG', type='SUBSCRIBER', mark='members-announce', holder='NP Smith, Peter', contact='third contact')", + "rel(anchor='LP Hostsharing eG', type='PARTNER', holder='NP Smith, Peter', contact='sixth contact')", + "rel(anchor='NP Smith, Peter', type='DEBITOR', holder='NP Smith, Peter', contact='third contact')"); + } + } + + @Nested + class UpdateRelation { + + @Test + public void hostsharingAdmin_withoutAssumedRole_canUpdateContactOfArbitraryRelation() { + // given + context("superuser-alex@hostsharing.net"); + final var givenRelation = givenSomeTemporaryRelationBessler( + "Bert", "fifth contact"); + assertThatRelationIsVisibleForUserWithRole( + givenRelation, + "hs_office_person#ErbenBesslerMelBessler:ADMIN"); + assertThatRelationActuallyInDatabase(givenRelation); + context("superuser-alex@hostsharing.net"); + final var givenContact = contactRepo.findContactByOptionalLabelLike("sixth contact").stream().findFirst().orElseThrow(); + + // when + final var result = jpaAttempt.transacted(() -> { + context("superuser-alex@hostsharing.net"); + givenRelation.setContact(givenContact); + return toCleanup(relationRepo.save(givenRelation)); + }); + + // then + result.assertSuccessful(); + assertThat(result.returnedValue().getContact().getLabel()).isEqualTo("sixth contact"); + assertThatRelationIsVisibleForUserWithRole( + result.returnedValue(), + "global#global:ADMIN"); + assertThatRelationIsVisibleForUserWithRole( + result.returnedValue(), + "hs_office_contact#sixthcontact:ADMIN"); + + assertThatRelationIsNotVisibleForUserWithRole( + result.returnedValue(), + "hs_office_contact#fifthcontact:ADMIN"); + + relationRepo.deleteByUuid(givenRelation.getUuid()); + } + + @Test + public void holderAdmin_canNotUpdateRelatedRelation() { + // given + context("superuser-alex@hostsharing.net"); + final var givenRelation = givenSomeTemporaryRelationBessler( + "Anita", "eighth"); + assertThatRelationIsVisibleForUserWithRole( + givenRelation, + "hs_office_person#BesslerAnita:ADMIN"); + assertThatRelationActuallyInDatabase(givenRelation); + + // when + final var result = jpaAttempt.transacted(() -> { + context("superuser-alex@hostsharing.net", "hs_office_person#BesslerAnita:ADMIN"); + givenRelation.setContact(null); + return relationRepo.save(givenRelation); + }); + + // then + result.assertExceptionWithRootCauseMessage(JpaSystemException.class, + "[403] Subject ", " is not allowed to update hs_office_relation uuid"); + } + + @Test + public void contactAdmin_canNotUpdateRelatedRelation() { + // given + context("superuser-alex@hostsharing.net"); + final var givenRelation = givenSomeTemporaryRelationBessler( + "Anita", "ninth"); + assertThatRelationIsVisibleForUserWithRole( + givenRelation, + "hs_office_contact#ninthcontact:ADMIN"); + assertThatRelationActuallyInDatabase(givenRelation); + + // when + final var result = jpaAttempt.transacted(() -> { + context("superuser-alex@hostsharing.net", "hs_office_contact#ninthcontact:ADMIN"); + givenRelation.setContact(null); // TODO + return relationRepo.save(givenRelation); + }); + + // then + result.assertExceptionWithRootCauseMessage(JpaSystemException.class, + "[403] Subject ", " is not allowed to update hs_office_relation uuid"); + } + + private void assertThatRelationActuallyInDatabase(final HsOfficeRelationEntity saved) { + final var found = relationRepo.findByUuid(saved.getUuid()); + assertThat(found).isNotEmpty().get().isNotSameAs(saved).usingRecursiveComparison().isEqualTo(saved); + } + + private void assertThatRelationIsVisibleForUserWithRole( + final HsOfficeRelationEntity entity, + final String assumedRoles) { + jpaAttempt.transacted(() -> { + context("superuser-alex@hostsharing.net", assumedRoles); + assertThatRelationActuallyInDatabase(entity); + }).assertSuccessful(); + } + + private void assertThatRelationIsNotVisibleForUserWithRole( + final HsOfficeRelationEntity entity, + final String assumedRoles) { + jpaAttempt.transacted(() -> { + context("superuser-alex@hostsharing.net", assumedRoles); + final var found = relationRepo.findByUuid(entity.getUuid()); + assertThat(found).isEmpty(); + }).assertSuccessful(); + } + } + + @Nested + class DeleteByUuid { + + @Test + public void globalAdmin_withoutAssumedRole_canDeleteAnyRelation() { + // given + context("superuser-alex@hostsharing.net", null); + final var givenRelation = givenSomeTemporaryRelationBessler( + "Anita", "tenth"); + + // when + final var result = jpaAttempt.transacted(() -> { + context("superuser-alex@hostsharing.net"); + relationRepo.deleteByUuid(givenRelation.getUuid()); + }); + + // then + result.assertSuccessful(); + assertThat(jpaAttempt.transacted(() -> { + context("superuser-fran@hostsharing.net", null); + return relationRepo.findByUuid(givenRelation.getUuid()); + }).assertSuccessful().returnedValue()).isEmpty(); + } + + @Test + public void contactUser_canViewButNotDeleteTheirRelatedRelation() { + // given + context("superuser-alex@hostsharing.net", null); + final var givenRelation = givenSomeTemporaryRelationBessler( + "Anita", "eleventh"); + + // when + final var result = jpaAttempt.transacted(() -> { + context("contact-admin@eleventhcontact.example.com"); + assertThat(relationRepo.findByUuid(givenRelation.getUuid())).isPresent(); + relationRepo.deleteByUuid(givenRelation.getUuid()); + }); + + // then + result.assertExceptionWithRootCauseMessage( + JpaSystemException.class, + "[403] Subject ", " not allowed to delete hs_office_relation"); + assertThat(jpaAttempt.transacted(() -> { + context("superuser-alex@hostsharing.net"); + return relationRepo.findByUuid(givenRelation.getUuid()); + }).assertSuccessful().returnedValue()).isPresent(); // still there + } + + @Test + public void deletingARelationAlsoDeletesRelatedRolesAndGrants() { + // given + context("superuser-alex@hostsharing.net"); + final var initialRoleNames = Array.from(distinctRoleNamesOf(rawRoleRepo.findAll())); + final var initialGrantNames = Array.from(distinctGrantDisplaysOf(rawGrantRepo.findAll())); + final var givenRelation = givenSomeTemporaryRelationBessler( + "Anita", "twelfth"); + + // when + final var result = jpaAttempt.transacted(() -> { + context("superuser-alex@hostsharing.net"); + return relationRepo.deleteByUuid(givenRelation.getUuid()); + }); + + // then + result.assertSuccessful(); + assertThat(result.returnedValue()).isEqualTo(1); + assertThat(distinctRoleNamesOf(rawRoleRepo.findAll())).containsExactlyInAnyOrder(initialRoleNames); + assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(initialGrantNames); + } + } + + @Test + public void auditJournalLogIsAvailable() { + // given + final var query = em.createNativeQuery(""" + select currentTask, targetTable, targetOp + from tx_journal_v + where targettable = 'hs_office_relation'; + """); + + // when + @SuppressWarnings("unchecked") final List customerLogEntries = query.getResultList(); + + // then + assertThat(customerLogEntries).map(Arrays::toString).contains( + "[creating relation test-data HostsharingeG-FirstGmbH, hs_office_relation, INSERT]", + "[creating relation test-data FirstGmbH-Firby, hs_office_relation, INSERT]"); + } + + private HsOfficeRelationEntity givenSomeTemporaryRelationBessler(final String holderPerson, final String contact) { + return jpaAttempt.transacted(() -> { + context("superuser-alex@hostsharing.net"); + final var givenAnchorPerson = personRepo.findPersonByOptionalNameLike("Erben Bessler").get(0); + final var givenHolderPerson = personRepo.findPersonByOptionalNameLike(holderPerson).get(0); + final var givenContact = contactRepo.findContactByOptionalLabelLike(contact).get(0); + final var newRelation = HsOfficeRelationEntity.builder() + .type(HsOfficeRelationType.REPRESENTATIVE) + .anchor(givenAnchorPerson) + .holder(givenHolderPerson) + .contact(givenContact) + .build(); + + return toCleanup(relationRepo.save(newRelation)); + }).assertSuccessful().returnedValue(); + } + + void exactlyTheseRelationsAreReturned( + final List actualResult, + final String... relationNames) { + assertThat(actualResult) + .extracting(HsOfficeRelationEntity::toString) + .containsExactlyInAnyOrder(relationNames); + } + + void allTheseRelationsAreReturned( + final List actualResult, + final String... relationNames) { + assertThat(actualResult) + .extracting(HsOfficeRelationEntity::toString) + .contains(relationNames); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipEntityUnitTest.java deleted file mode 100644 index 59433fa2..00000000 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipEntityUnitTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package net.hostsharing.hsadminng.hs.office.relationship; - -import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; -import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; - -class HsOfficeRelationshipEntityUnitTest { - - private HsOfficePersonEntity anchor = HsOfficePersonEntity.builder() - .personType(HsOfficePersonType.LEGAL_PERSON) - .tradeName("some trade name") - .build(); - private HsOfficePersonEntity holder = HsOfficePersonEntity.builder() - .personType(HsOfficePersonType.NATURAL_PERSON) - .familyName("Meier") - .givenName("Mellie") - .build(); - - @Test - void toStringReturnsAllProperties() { - final var given = HsOfficeRelationshipEntity.builder() - .relType(HsOfficeRelationshipType.SUBSCRIBER) - .relMark("members-announce") - .relAnchor(anchor) - .relHolder(holder) - .build(); - - assertThat(given.toString()).isEqualTo("rel(relAnchor='LP some trade name', relType='SUBSCRIBER', relMark='members-announce', relHolder='NP Meier, Mellie')"); - } - - @Test - void toShortString() { - final var given = HsOfficeRelationshipEntity.builder() - .relType(HsOfficeRelationshipType.REPRESENTATIVE) - .relAnchor(anchor) - .relHolder(holder) - .build(); - - assertThat(given.toShortString()).isEqualTo("rel(relAnchor='LP some trade name', relType='REPRESENTATIVE', relHolder='NP Meier, Mellie')"); - } -} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipRepositoryIntegrationTest.java deleted file mode 100644 index 8d89479c..00000000 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipRepositoryIntegrationTest.java +++ /dev/null @@ -1,410 +0,0 @@ -package net.hostsharing.hsadminng.hs.office.relationship; - -import net.hostsharing.hsadminng.context.Context; -import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRepository; -import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository; -import net.hostsharing.hsadminng.hs.office.test.ContextBasedTestWithCleanup; -import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository; -import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository; -import net.hostsharing.test.Array; -import net.hostsharing.test.JpaAttempt; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Import; -import org.springframework.orm.jpa.JpaSystemException; - -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import jakarta.servlet.http.HttpServletRequest; -import java.util.Arrays; -import java.util.List; - -import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf; -import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.distinctRoleNamesOf; -import static net.hostsharing.test.JpaAttempt.attempt; -import static org.assertj.core.api.Assertions.assertThat; - -@DataJpaTest -@Import( { Context.class, JpaAttempt.class }) -class HsOfficeRelationshipRepositoryIntegrationTest extends ContextBasedTestWithCleanup { - - @Autowired - HsOfficeRelationshipRepository relationshipRepo; - - @Autowired - HsOfficePersonRepository personRepo; - - @Autowired - HsOfficeContactRepository contactRepo; - - @Autowired - RawRbacRoleRepository rawRoleRepo; - - @Autowired - RawRbacGrantRepository rawGrantRepo; - - @PersistenceContext - EntityManager em; - - @Autowired - JpaAttempt jpaAttempt; - - @MockBean - HttpServletRequest request; - - @Nested - class CreateRelationship { - - @Test - public void testHostsharingAdmin_withoutAssumedRole_canCreateNewRelationship() { - // given - context("superuser-alex@hostsharing.net"); - final var count = relationshipRepo.count(); - final var givenAnchorPerson = personRepo.findPersonByOptionalNameLike("Bessler").get(0); - final var givenHolderPerson = personRepo.findPersonByOptionalNameLike("Anita").get(0); - final var givenContact = contactRepo.findContactByOptionalLabelLike("fourth contact").get(0); - - // when - final var result = attempt(em, () -> { - final var newRelationship = HsOfficeRelationshipEntity.builder() - .relAnchor(givenAnchorPerson) - .relHolder(givenHolderPerson) - .relType(HsOfficeRelationshipType.REPRESENTATIVE) - .contact(givenContact) - .build(); - return toCleanup(relationshipRepo.save(newRelationship)); - }); - - // then - result.assertSuccessful(); - assertThat(result.returnedValue()).isNotNull().extracting(HsOfficeRelationshipEntity::getUuid).isNotNull(); - assertThatRelationshipIsPersisted(result.returnedValue()); - assertThat(relationshipRepo.count()).isEqualTo(count + 1); - } - - @Test - public void createsAndGrantsRoles() { - // given - context("superuser-alex@hostsharing.net"); - final var initialRoleNames = distinctRoleNamesOf(rawRoleRepo.findAll()); - final var initialGrantNames = distinctGrantDisplaysOf(rawGrantRepo.findAll()); - - // when - attempt(em, () -> { - final var givenAnchorPerson = personRepo.findPersonByOptionalNameLike("Bessler").get(0); - final var givenHolderPerson = personRepo.findPersonByOptionalNameLike("Anita").get(0); - final var givenContact = contactRepo.findContactByOptionalLabelLike("fourth contact").get(0); - final var newRelationship = HsOfficeRelationshipEntity.builder() - .relAnchor(givenAnchorPerson) - .relHolder(givenHolderPerson) - .relType(HsOfficeRelationshipType.REPRESENTATIVE) - .contact(givenContact) - .build(); - return toCleanup(relationshipRepo.save(newRelationship)); - }); - - // then - assertThat(distinctRoleNamesOf(rawRoleRepo.findAll())).containsExactlyInAnyOrder(Array.from( - initialRoleNames, - "hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.admin", - "hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.owner", - "hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.tenant")); - assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(Array.fromFormatted( - initialGrantNames, - - "{ grant perm * on hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita to role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.owner by system and assume }", - "{ grant role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.owner to role global#global.admin by system and assume }", - "{ grant role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.owner to role hs_office_person#BesslerAnita.admin by system and assume }", - - "{ grant perm edit on hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita to role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.admin by system and assume }", - "{ grant role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.admin to role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.owner by system and assume }", - - "{ grant perm view on hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita to role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.tenant by system and assume }", - "{ grant role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.tenant to role hs_office_contact#fourthcontact.admin by system and assume }", - "{ grant role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.tenant to role hs_office_person#BesslerAnita.admin by system and assume }", - - "{ grant role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.tenant to role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.admin by system and assume }", - "{ grant role hs_office_contact#fourthcontact.tenant to role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.tenant by system and assume }", - "{ grant role hs_office_person#BesslerAnita.tenant to role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.tenant by system and assume }", - null) - ); - } - - private void assertThatRelationshipIsPersisted(final HsOfficeRelationshipEntity saved) { - final var found = relationshipRepo.findByUuid(saved.getUuid()); - assertThat(found).isNotEmpty().get().usingRecursiveComparison().isEqualTo(saved); - } - } - - @Nested - class FindAllRelationships { - - @Test - public void globalAdmin_withoutAssumedRole_canViewAllRelationshipsOfArbitraryPerson() { - // given - context("superuser-alex@hostsharing.net"); - final var person = personRepo.findPersonByOptionalNameLike("Second e.K.").stream().findFirst().orElseThrow(); - - // when - final var result = relationshipRepo.findRelationshipRelatedToPersonUuid(person.getUuid()); - - // then - allTheseRelationshipsAreReturned( - result, - "rel(relAnchor='LP Hostsharing eG', relType='PARTNER', relHolder='LP Second e.K.', contact='second contact')", - "rel(relAnchor='LP Second e.K.', relType='REPRESENTATIVE', relHolder='NP Smith, Peter', contact='second contact')"); - } - - @Test - public void normalUser_canViewRelationshipsOfOwnedPersons() { - // given: - context("person-FirstGmbH@example.com"); - final var person = personRepo.findPersonByOptionalNameLike("First").stream().findFirst().orElseThrow(); - - // when: - final var result = relationshipRepo.findRelationshipRelatedToPersonUuid(person.getUuid()); - - // then: - exactlyTheseRelationshipsAreReturned( - result, - "rel(relAnchor='LP Hostsharing eG', relType='PARTNER', relHolder='LP First GmbH', contact='first contact')", - "rel(relAnchor='LP First GmbH', relType='REPRESENTATIVE', relHolder='NP Firby, Susan', contact='first contact')"); - } - } - - @Nested - class UpdateRelationship { - - @Test - public void hostsharingAdmin_withoutAssumedRole_canUpdateContactOfArbitraryRelationship() { - // given - context("superuser-alex@hostsharing.net"); - final var givenRelationship = givenSomeTemporaryRelationshipBessler( - "Anita", "fifth contact"); - assertThatRelationshipIsVisibleForUserWithRole( - givenRelationship, - "hs_office_person#ErbenBesslerMelBessler.admin"); - assertThatRelationshipActuallyInDatabase(givenRelationship); - context("superuser-alex@hostsharing.net"); - final var givenContact = contactRepo.findContactByOptionalLabelLike("sixth contact").get(0); - - // when - final var result = jpaAttempt.transacted(() -> { - context("superuser-alex@hostsharing.net"); - givenRelationship.setContact(givenContact); - return toCleanup(relationshipRepo.save(givenRelationship)); - }); - - // then - result.assertSuccessful(); - assertThat(result.returnedValue().getContact().getLabel()).isEqualTo("sixth contact"); - assertThatRelationshipIsVisibleForUserWithRole( - result.returnedValue(), - "global#global.admin"); - assertThatRelationshipIsVisibleForUserWithRole( - result.returnedValue(), - "hs_office_contact#sixthcontact.admin"); - - assertThatRelationshipIsNotVisibleForUserWithRole( - result.returnedValue(), - "hs_office_contact#fifthcontact.admin"); - - relationshipRepo.deleteByUuid(givenRelationship.getUuid()); - } - - @Test - public void relHolderAdmin_canNotUpdateRelatedRelationship() { - // given - context("superuser-alex@hostsharing.net"); - final var givenRelationship = givenSomeTemporaryRelationshipBessler( - "Anita", "eighth"); - assertThatRelationshipIsVisibleForUserWithRole( - givenRelationship, - "hs_office_person#BesslerAnita.admin"); - assertThatRelationshipActuallyInDatabase(givenRelationship); - - // when - final var result = jpaAttempt.transacted(() -> { - context("superuser-alex@hostsharing.net", "hs_office_person#BesslerAnita.admin"); - givenRelationship.setContact(null); - return relationshipRepo.save(givenRelationship); - }); - - // then - result.assertExceptionWithRootCauseMessage(JpaSystemException.class, - "[403] Subject ", " is not allowed to update hs_office_relationship uuid"); - } - - @Test - public void contactAdmin_canNotUpdateRelatedRelationship() { - // given - context("superuser-alex@hostsharing.net"); - final var givenRelationship = givenSomeTemporaryRelationshipBessler( - "Anita", "ninth"); - assertThatRelationshipIsVisibleForUserWithRole( - givenRelationship, - "hs_office_contact#ninthcontact.admin"); - assertThatRelationshipActuallyInDatabase(givenRelationship); - - // when - final var result = jpaAttempt.transacted(() -> { - context("superuser-alex@hostsharing.net", "hs_office_contact#ninthcontact.admin"); - givenRelationship.setContact(null); // TODO - return relationshipRepo.save(givenRelationship); - }); - - // then - result.assertExceptionWithRootCauseMessage(JpaSystemException.class, - "[403] Subject ", " is not allowed to update hs_office_relationship uuid"); - } - - private void assertThatRelationshipActuallyInDatabase(final HsOfficeRelationshipEntity saved) { - final var found = relationshipRepo.findByUuid(saved.getUuid()); - assertThat(found).isNotEmpty().get().isNotSameAs(saved).usingRecursiveComparison().isEqualTo(saved); - } - - private void assertThatRelationshipIsVisibleForUserWithRole( - final HsOfficeRelationshipEntity entity, - final String assumedRoles) { - jpaAttempt.transacted(() -> { - context("superuser-alex@hostsharing.net", assumedRoles); - assertThatRelationshipActuallyInDatabase(entity); - }).assertSuccessful(); - } - - private void assertThatRelationshipIsNotVisibleForUserWithRole( - final HsOfficeRelationshipEntity entity, - final String assumedRoles) { - jpaAttempt.transacted(() -> { - context("superuser-alex@hostsharing.net", assumedRoles); - final var found = relationshipRepo.findByUuid(entity.getUuid()); - assertThat(found).isEmpty(); - }).assertSuccessful(); - } - } - - @Nested - class DeleteByUuid { - - @Test - public void globalAdmin_withoutAssumedRole_canDeleteAnyRelationship() { - // given - context("superuser-alex@hostsharing.net", null); - final var givenRelationship = givenSomeTemporaryRelationshipBessler( - "Anita", "tenth"); - - // when - final var result = jpaAttempt.transacted(() -> { - context("superuser-alex@hostsharing.net"); - relationshipRepo.deleteByUuid(givenRelationship.getUuid()); - }); - - // then - result.assertSuccessful(); - assertThat(jpaAttempt.transacted(() -> { - context("superuser-fran@hostsharing.net", null); - return relationshipRepo.findByUuid(givenRelationship.getUuid()); - }).assertSuccessful().returnedValue()).isEmpty(); - } - - @Test - public void contactUser_canViewButNotDeleteTheirRelatedRelationship() { - // given - context("superuser-alex@hostsharing.net", null); - final var givenRelationship = givenSomeTemporaryRelationshipBessler( - "Anita", "eleventh"); - - // when - final var result = jpaAttempt.transacted(() -> { - context("contact-admin@eleventhcontact.example.com"); - assertThat(relationshipRepo.findByUuid(givenRelationship.getUuid())).isPresent(); - relationshipRepo.deleteByUuid(givenRelationship.getUuid()); - }); - - // then - result.assertExceptionWithRootCauseMessage( - JpaSystemException.class, - "[403] Subject ", " not allowed to delete hs_office_relationship"); - assertThat(jpaAttempt.transacted(() -> { - context("superuser-alex@hostsharing.net"); - return relationshipRepo.findByUuid(givenRelationship.getUuid()); - }).assertSuccessful().returnedValue()).isPresent(); // still there - } - - @Test - public void deletingARelationshipAlsoDeletesRelatedRolesAndGrants() { - // given - context("superuser-alex@hostsharing.net"); - final var initialRoleNames = Array.from(distinctRoleNamesOf(rawRoleRepo.findAll())); - final var initialGrantNames = Array.from(distinctGrantDisplaysOf(rawGrantRepo.findAll())); - final var givenRelationship = givenSomeTemporaryRelationshipBessler( - "Anita", "twelfth"); - - // when - final var result = jpaAttempt.transacted(() -> { - context("superuser-alex@hostsharing.net"); - return relationshipRepo.deleteByUuid(givenRelationship.getUuid()); - }); - - // then - result.assertSuccessful(); - assertThat(result.returnedValue()).isEqualTo(1); - assertThat(distinctRoleNamesOf(rawRoleRepo.findAll())).containsExactlyInAnyOrder(initialRoleNames); - assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(initialGrantNames); - } - } - - @Test - public void auditJournalLogIsAvailable() { - // given - final var query = em.createNativeQuery(""" - select currentTask, targetTable, targetOp - from tx_journal_v - where targettable = 'hs_office_relationship'; - """); - - // when - @SuppressWarnings("unchecked") final List customerLogEntries = query.getResultList(); - - // then - assertThat(customerLogEntries).map(Arrays::toString).contains( - "[creating relationship test-data HostsharingeG-FirstGmbH, hs_office_relationship, INSERT]", - "[creating relationship test-data FirstGmbH-Firby, hs_office_relationship, INSERT]"); - } - - private HsOfficeRelationshipEntity givenSomeTemporaryRelationshipBessler(final String holderPerson, final String contact) { - return jpaAttempt.transacted(() -> { - context("superuser-alex@hostsharing.net"); - final var givenAnchorPerson = personRepo.findPersonByOptionalNameLike("Erben Bessler").get(0); - final var givenHolderPerson = personRepo.findPersonByOptionalNameLike(holderPerson).get(0); - final var givenContact = contactRepo.findContactByOptionalLabelLike(contact).get(0); - final var newRelationship = HsOfficeRelationshipEntity.builder() - .relType(HsOfficeRelationshipType.REPRESENTATIVE) - .relAnchor(givenAnchorPerson) - .relHolder(givenHolderPerson) - .contact(givenContact) - .build(); - - return toCleanup(relationshipRepo.save(newRelationship)); - }).assertSuccessful().returnedValue(); - } - - void exactlyTheseRelationshipsAreReturned( - final List actualResult, - final String... relationshipNames) { - assertThat(actualResult) - .extracting(HsOfficeRelationshipEntity::toString) - .containsExactlyInAnyOrder(relationshipNames); - } - - void allTheseRelationshipsAreReturned( - final List actualResult, - final String... relationshipNames) { - assertThat(actualResult) - .extracting(HsOfficeRelationshipEntity::toString) - .contains(relationshipNames); - } -} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java index 67a731de..33a6810a 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java @@ -1,6 +1,6 @@ package net.hostsharing.hsadminng.hs.office.sepamandate; -import com.vladmihalcea.hibernate.type.range.Range; +import io.hypersistence.utils.hibernate.type.range.Range; import io.restassured.RestAssured; import io.restassured.http.ContentType; import net.hostsharing.hsadminng.HsadminNgApplication; @@ -24,6 +24,7 @@ import jakarta.persistence.PersistenceContext; import java.time.LocalDate; import java.util.UUID; +import static java.util.Optional.ofNullable; import static net.hostsharing.test.IsValidUuidMatcher.isUuidValid; import static net.hostsharing.test.JsonMatcher.lenientlyEquals; import static org.assertj.core.api.Assertions.assertThat; @@ -70,35 +71,27 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl .then().log().all().assertThat() .statusCode(200) .contentType("application/json") + .log().all() .body("", lenientlyEquals(""" [ { - "debitor": { - "debitorNumber": 1000212, - "billingContact": { "label": "second contact" } - }, - "bankAccount": { "holder": "Second e.K." }, - "reference": "refSeconde.K.", - "validFrom": "2022-10-01", - "validTo": "2026-12-31" - }, - { - "debitor": { - "debitorNumber": 1000111, - "billingContact": { "label": "first contact" } - }, + "debitor": { "debitorNumber": 1000111 }, "bankAccount": { "holder": "First GmbH" }, - "reference": "refFirstGmbH", + "reference": "ref-10001-11", "validFrom": "2022-10-01", "validTo": "2026-12-31" }, { - "debitor": { - "debitorNumber": 1000313, - "billingContact": { "label": "third contact" } - }, + "debitor": { "debitorNumber": 1000212 }, + "bankAccount": { "holder": "Second e.K." }, + "reference": "ref-10002-12", + "validFrom": "2022-10-01", + "validTo": "2026-12-31" + }, + { + "debitor": { "debitorNumber": 1000313 }, "bankAccount": { "holder": "Third OHG" }, - "reference": "refThirdOHG", + "reference": "ref-10003-13", "validFrom": "2022-10-01", "validTo": "2026-12-31" } @@ -139,7 +132,7 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl .statusCode(201) .contentType(ContentType.JSON) .body("uuid", isUuidValid()) - .body("debitor.partner.person.tradeName", is("Third OHG")) + .body("debitor.partner.partnerNumber", is(10003)) .body("bankAccount.iban", is("DE02200505501015871393")) .body("reference", is("temp ref CAT A")) .body("validFrom", is("2022-10-13")) @@ -262,15 +255,12 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl .contentType("application/json") .body("", lenientlyEquals(""" { - "debitor": { - "debitorNumber": 1000111, - "billingContact": { "label": "first contact" } - }, + "debitor": { "debitorNumber": 1000111 }, "bankAccount": { "holder": "First GmbH", "iban": "DE02120300000000202051" }, - "reference": "refFirstGmbH", + "reference": "ref-10001-11", "validFrom": "2022-10-01", "validTo": "2026-12-31" } @@ -314,15 +304,12 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl .contentType("application/json") .body("", lenientlyEquals(""" { - "debitor": { - "debitorNumber": 1000111, - "billingContact": { "label": "first contact" } - }, + "debitor": { "debitorNumber": 1000111 }, "bankAccount": { "holder": "First GmbH", "iban": "DE02120300000000202051" }, - "reference": "refFirstGmbH", + "reference": "ref-10001-11", "validFrom": "2022-10-01", "validTo": "2026-12-31" } @@ -337,7 +324,7 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl @Test void globalAdmin_canPatchAllUpdatablePropertiesOfSepaMandate() { - final var givenSepaMandate = givenSomeTemporarySepaMandate(); + final var givenSepaMandate = givenSomeTemporarySepaMandateForDebitorNumber(1000111); final var location = RestAssured // @formatter:off .given() @@ -358,7 +345,7 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl .statusCode(200) .contentType(ContentType.JSON) .body("uuid", isUuidValid()) - .body("debitor.partner.person.tradeName", is("First GmbH")) + .body("debitor.debitorNumber", is(1000111)) .body("bankAccount.iban", is("DE02120300000000202051")) .body("reference", is("temp ref CAT Z - patched")) .body("agreement", is("2020-06-01")) @@ -370,7 +357,7 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl context.define("superuser-alex@hostsharing.net"); assertThat(sepaMandateRepo.findByUuid(givenSepaMandate.getUuid())).isPresent().get() .matches(mandate -> { - assertThat(mandate.getDebitor().toString()).isEqualTo("debitor(D-1000111: LP First GmbH: fir)"); + assertThat(mandate.getDebitor().toString()).isEqualTo("debitor(D-1000111: rel(anchor='LP First GmbH', type='DEBITOR', holder='LP First GmbH'), fir)"); assertThat(mandate.getBankAccount().toShortString()).isEqualTo("First GmbH"); assertThat(mandate.getReference()).isEqualTo("temp ref CAT Z - patched"); assertThat(mandate.getValidFrom()).isEqualTo("2020-06-05"); @@ -383,7 +370,7 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl void globalAdmin_canPatchJustValidToOfArbitrarySepaMandate() { context.define("superuser-alex@hostsharing.net"); - final var givenSepaMandate = givenSomeTemporarySepaMandate(); + final var givenSepaMandate = givenSomeTemporarySepaMandateForDebitorNumber(1000111); final var location = RestAssured // @formatter:off .given() @@ -401,7 +388,7 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl .statusCode(200) .contentType(ContentType.JSON) .body("uuid", isUuidValid()) - .body("debitor.partner.person.tradeName", is("First GmbH")) + .body("debitor.debitorNumber", is(1000111)) .body("bankAccount.iban", is("DE02120300000000202051")) .body("reference", is("temp ref CAT Z")) .body("validFrom", is("2022-11-01")) @@ -411,7 +398,7 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl // finally, the sepaMandate is actually updated assertThat(sepaMandateRepo.findByUuid(givenSepaMandate.getUuid())).isPresent().get() .matches(mandate -> { - assertThat(mandate.getDebitor().toString()).isEqualTo("debitor(D-1000111: LP First GmbH: fir)"); + assertThat(mandate.getDebitor().toString()).isEqualTo("debitor(D-1000111: rel(anchor='LP First GmbH', type='DEBITOR', holder='LP First GmbH'), fir)"); assertThat(mandate.getBankAccount().toShortString()).isEqualTo("First GmbH"); assertThat(mandate.getReference()).isEqualTo("temp ref CAT Z"); assertThat(mandate.getValidity().asString()).isEqualTo("[2022-11-01,2023-01-01)"); @@ -423,7 +410,7 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl void globalAdmin_canNotPatchReferenceOfArbitrarySepaMandate() { context.define("superuser-alex@hostsharing.net"); - final var givenSepaMandate = givenSomeTemporarySepaMandate(); + final var givenSepaMandate = givenSomeTemporarySepaMandateForDebitorNumber(1000111); final var location = RestAssured // @formatter:off .given() @@ -458,7 +445,7 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl @Test void globalAdmin_canDeleteArbitrarySepaMandate() { context.define("superuser-alex@hostsharing.net"); - final var givenSepaMandate = givenSomeTemporarySepaMandate(); + final var givenSepaMandate = givenSomeTemporarySepaMandateForDebitorNumber(1000111); RestAssured // @formatter:off .given() @@ -477,7 +464,7 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl @Accepts({ "SepaMandate:X(Access Control)" }) void bankAccountAdminUser_canNotDeleteRelatedSepaMandate() { context.define("superuser-alex@hostsharing.net"); - final var givenSepaMandate = givenSomeTemporarySepaMandate(); + final var givenSepaMandate = givenSomeTemporarySepaMandateForDebitorNumber(1000111); RestAssured // @formatter:off .given() @@ -496,7 +483,7 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl @Accepts({ "SepaMandate:X(Access Control)" }) void normalUser_canNotDeleteUnrelatedSepaMandate() { context.define("superuser-alex@hostsharing.net"); - final var givenSepaMandate = givenSomeTemporarySepaMandate(); + final var givenSepaMandate = givenSomeTemporarySepaMandateForDebitorNumber(1000111); RestAssured // @formatter:off .given() @@ -512,11 +499,13 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl } } - private HsOfficeSepaMandateEntity givenSomeTemporarySepaMandate() { + private HsOfficeSepaMandateEntity givenSomeTemporarySepaMandateForDebitorNumber(final int debitorNumber) { return jpaAttempt.transacted(() -> { context.define("superuser-alex@hostsharing.net"); - final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike("First").get(0); - final var givenBankAccount = bankAccountRepo.findByOptionalHolderLike("First").get(0); + final var givenDebitor = debitorRepo.findDebitorByDebitorNumber(debitorNumber).get(0); + final var bankAccountHolder = ofNullable(givenDebitor.getPartner().getPartnerRel().getHolder().getTradeName()) + .orElse(givenDebitor.getPartner().getPartnerRel().getHolder().getFamilyName()); + final var givenBankAccount = bankAccountRepo.findByOptionalHolderLike(bankAccountHolder).get(0); final var newSepaMandate = HsOfficeSepaMandateEntity.builder() .uuid(UUID.randomUUID()) .debitor(givenDebitor) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntityPatcherUnitTest.java index 05f4ca07..04ba4fee 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntityPatcherUnitTest.java @@ -1,6 +1,6 @@ package net.hostsharing.hsadminng.hs.office.sepamandate; -import com.vladmihalcea.hibernate.type.range.Range; +import io.hypersistence.utils.hibernate.type.range.Range; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandatePatchResource; import net.hostsharing.test.PatchUnitTestBase; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java index 04b5b5cf..4f558db8 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java @@ -1,6 +1,6 @@ package net.hostsharing.hsadminng.hs.office.sepamandate; -import com.vladmihalcea.hibernate.type.range.Range; +import io.hypersistence.utils.hibernate.type.range.Range; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountRepository; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; @@ -26,6 +26,7 @@ import java.util.List; import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf; import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.distinctRoleNamesOf; +import static net.hostsharing.test.Array.fromFormatted; import static net.hostsharing.test.JpaAttempt.attempt; import static org.assertj.core.api.Assertions.assertThat; @@ -94,8 +95,6 @@ class HsOfficeSepaMandateRepositoryIntegrationTest extends ContextBasedTestWithC context("superuser-alex@hostsharing.net"); final var initialRoleNames = distinctRoleNamesOf(rawRoleRepo.findAll()); final var initialGrantNames = distinctGrantDisplaysOf(rawGrantRepo.findAll()).stream() - .map(s -> s.replace("-firstcontact", "-...")) - .map(s -> s.replace("PaulWinkler", "Paul...")) .map(s -> s.replace("hs_office_", "")) .toList(); @@ -118,41 +117,36 @@ class HsOfficeSepaMandateRepositoryIntegrationTest extends ContextBasedTestWithC final var all = rawRoleRepo.findAll(); assertThat(distinctRoleNamesOf(all)).containsExactlyInAnyOrder(Array.from( initialRoleNames, - "hs_office_sepamandate#temprefB.owner", - "hs_office_sepamandate#temprefB.admin", - "hs_office_sepamandate#temprefB.agent", - "hs_office_sepamandate#temprefB.tenant", - "hs_office_sepamandate#temprefB.guest")); + "hs_office_sepamandate#DE02600501010002034304-[2020-01-01,2023-01-01):ADMIN", + "hs_office_sepamandate#DE02600501010002034304-[2020-01-01,2023-01-01):AGENT", + "hs_office_sepamandate#DE02600501010002034304-[2020-01-01,2023-01-01):OWNER", + "hs_office_sepamandate#DE02600501010002034304-[2020-01-01,2023-01-01):REFERRER")); assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())) - .map(s -> s.replace("-firstcontact", "-...")) - .map(s -> s.replace("PaulWinkler", "Paul...")) .map(s -> s.replace("hs_office_", "")) - .containsExactlyInAnyOrder(Array.fromFormatted( + .containsExactlyInAnyOrder(fromFormatted( initialGrantNames, // owner - "{ grant perm * on sepamandate#temprefB to role sepamandate#temprefB.owner by system and assume }", - "{ grant role sepamandate#temprefB.owner to role global#global.admin by system and assume }", + "{ grant perm:sepamandate#DE02600501010002034304-[2020-01-01,2023-01-01):DELETE to role:sepamandate#DE02600501010002034304-[2020-01-01,2023-01-01):OWNER by system and assume }", + "{ grant role:sepamandate#DE02600501010002034304-[2020-01-01,2023-01-01):OWNER to role:global#global:ADMIN by system and assume }", + "{ grant role:sepamandate#DE02600501010002034304-[2020-01-01,2023-01-01):OWNER to user:superuser-alex@hostsharing.net by sepamandate#DE02600501010002034304-[2020-01-01,2023-01-01):OWNER and assume }", // admin - "{ grant perm edit on sepamandate#temprefB to role sepamandate#temprefB.admin by system and assume }", - "{ grant role sepamandate#temprefB.admin to role sepamandate#temprefB.owner by system and assume }", - "{ grant role bankaccount#Paul....tenant to role sepamandate#temprefB.admin by system and assume }", + "{ grant perm:sepamandate#DE02600501010002034304-[2020-01-01,2023-01-01):UPDATE to role:sepamandate#DE02600501010002034304-[2020-01-01,2023-01-01):ADMIN by system and assume }", + "{ grant role:sepamandate#DE02600501010002034304-[2020-01-01,2023-01-01):ADMIN to role:sepamandate#DE02600501010002034304-[2020-01-01,2023-01-01):OWNER by system and assume }", // agent - "{ grant role sepamandate#temprefB.agent to role sepamandate#temprefB.admin by system and assume }", - "{ grant role debitor#1000111:FirstGmbH-....tenant to role sepamandate#temprefB.agent by system and assume }", - "{ grant role sepamandate#temprefB.agent to role bankaccount#Paul....admin by system and assume }", - "{ grant role sepamandate#temprefB.agent to role debitor#1000111:FirstGmbH-....admin by system and assume }", + "{ grant role:bankaccount#DE02600501010002034304:REFERRER to role:sepamandate#DE02600501010002034304-[2020-01-01,2023-01-01):AGENT by system and assume }", + "{ grant role:sepamandate#DE02600501010002034304-[2020-01-01,2023-01-01):AGENT to role:sepamandate#DE02600501010002034304-[2020-01-01,2023-01-01):ADMIN by system and assume }", + "{ grant role:relation#FirstGmbH-with-DEBITOR-FirstGmbH:AGENT to role:sepamandate#DE02600501010002034304-[2020-01-01,2023-01-01):AGENT by system and assume }", - // tenant - "{ grant role sepamandate#temprefB.tenant to role sepamandate#temprefB.agent by system and assume }", - "{ grant role debitor#1000111:FirstGmbH-....guest to role sepamandate#temprefB.tenant by system and assume }", - "{ grant role bankaccount#Paul....guest to role sepamandate#temprefB.tenant by system and assume }", + // referrer + "{ grant perm:sepamandate#DE02600501010002034304-[2020-01-01,2023-01-01):SELECT to role:sepamandate#DE02600501010002034304-[2020-01-01,2023-01-01):REFERRER by system and assume }", + "{ grant role:sepamandate#DE02600501010002034304-[2020-01-01,2023-01-01):REFERRER to role:sepamandate#DE02600501010002034304-[2020-01-01,2023-01-01):AGENT by system and assume }", + "{ grant role:sepamandate#DE02600501010002034304-[2020-01-01,2023-01-01):REFERRER to role:bankaccount#DE02600501010002034304:ADMIN by system and assume }", + "{ grant role:relation#FirstGmbH-with-DEBITOR-FirstGmbH:TENANT to role:sepamandate#DE02600501010002034304-[2020-01-01,2023-01-01):REFERRER by system and assume }", + "{ grant role:sepamandate#DE02600501010002034304-[2020-01-01,2023-01-01):REFERRER to role:relation#FirstGmbH-with-DEBITOR-FirstGmbH:AGENT by system and assume }", - // guest - "{ grant perm view on sepamandate#temprefB to role sepamandate#temprefB.guest by system and assume }", - "{ grant role sepamandate#temprefB.guest to role sepamandate#temprefB.tenant by system and assume }", null)); } @@ -176,9 +170,9 @@ class HsOfficeSepaMandateRepositoryIntegrationTest extends ContextBasedTestWithC // then allTheseSepaMandatesAreReturned( result, - "SEPA-Mandate(DE02120300000000202051, refFirstGmbH, 2022-09-30, [2022-10-01,2027-01-01))", - "SEPA-Mandate(DE02100500000054540402, refSeconde.K., 2022-09-30, [2022-10-01,2027-01-01))", - "SEPA-Mandate(DE02300209000106531065, refThirdOHG, 2022-09-30, [2022-10-01,2027-01-01))"); + "SEPA-Mandate(DE02100500000054540402, ref-10002-12, 2022-09-30, [2022-10-01,2027-01-01))", + "SEPA-Mandate(DE02120300000000202051, ref-10001-11, 2022-09-30, [2022-10-01,2027-01-01))", + "SEPA-Mandate(DE02300209000106531065, ref-10003-13, 2022-09-30, [2022-10-01,2027-01-01))"); } @Test @@ -192,7 +186,7 @@ class HsOfficeSepaMandateRepositoryIntegrationTest extends ContextBasedTestWithC // then: exactlyTheseSepaMandatesAreReturned( result, - "SEPA-Mandate(DE02120300000000202051, refFirstGmbH, 2022-09-30, [2022-10-01,2027-01-01))"); + "SEPA-Mandate(DE02120300000000202051, ref-10001-11, 2022-09-30, [2022-10-01,2027-01-01))"); } } @@ -210,9 +204,9 @@ class HsOfficeSepaMandateRepositoryIntegrationTest extends ContextBasedTestWithC // then exactlyTheseSepaMandatesAreReturned( result, - "SEPA-Mandate(DE02120300000000202051, refFirstGmbH, 2022-09-30, [2022-10-01,2027-01-01))", - "SEPA-Mandate(DE02100500000054540402, refSeconde.K., 2022-09-30, [2022-10-01,2027-01-01))", - "SEPA-Mandate(DE02300209000106531065, refThirdOHG, 2022-09-30, [2022-10-01,2027-01-01))"); + "SEPA-Mandate(DE02100500000054540402, ref-10002-12, 2022-09-30, [2022-10-01,2027-01-01))", + "SEPA-Mandate(DE02120300000000202051, ref-10001-11, 2022-09-30, [2022-10-01,2027-01-01))", + "SEPA-Mandate(DE02300209000106531065, ref-10003-13, 2022-09-30, [2022-10-01,2027-01-01))"); } @Test @@ -226,7 +220,7 @@ class HsOfficeSepaMandateRepositoryIntegrationTest extends ContextBasedTestWithC // then exactlyTheseSepaMandatesAreReturned( result, - "SEPA-Mandate(DE02300209000106531065, refThirdOHG, 2022-09-30, [2022-10-01,2027-01-01))"); + "SEPA-Mandate(DE02300209000106531065, ref-10003-13, 2022-09-30, [2022-10-01,2027-01-01))"); } } @@ -236,10 +230,10 @@ class HsOfficeSepaMandateRepositoryIntegrationTest extends ContextBasedTestWithC @Test public void hostsharingAdmin_canUpdateArbitrarySepaMandate() { // given - final var givenSepaMandate = givenSomeTemporarySepaMandateBessler("Peter Smith"); + final var givenSepaMandate = givenSomeTemporarySepaMandate("DE02600501010002034304"); assertThatSepaMandateIsVisibleForUserWithRole( givenSepaMandate, - "hs_office_bankaccount#PeterSmith.admin"); + "hs_office_bankaccount#DE02600501010002034304:ADMIN"); // when final var result = jpaAttempt.transacted(() -> { @@ -264,16 +258,18 @@ class HsOfficeSepaMandateRepositoryIntegrationTest extends ContextBasedTestWithC public void bankAccountAdmin_canViewButNotUpdateRelatedSepaMandate() { // given context("superuser-alex@hostsharing.net"); - final var givenSepaMandate = givenSomeTemporarySepaMandateBessler("Anita Bessler"); + + final var givenSepaMandate = givenSomeTemporarySepaMandate("DE02300606010002474689"); assertThatSepaMandateIsVisibleForUserWithRole( givenSepaMandate, - "hs_office_bankaccount#AnitaBessler.admin"); + "hs_office_bankaccount#DE02300606010002474689:ADMIN"); assertThatSepaMandateActuallyInDatabase(givenSepaMandate); final var newValidityEnd = LocalDate.now(); // when final var result = jpaAttempt.transacted(() -> { - context("superuser-alex@hostsharing.net", "hs_office_bankaccount#AnitaBessler.admin"); + context("superuser-alex@hostsharing.net", "hs_office_bankaccount#DE02300606010002474689:ADMIN"); + givenSepaMandate.setValidity(Range.closedOpen( givenSepaMandate.getValidity().lower(), newValidityEnd)); return toCleanup(sepaMandateRepo.save(givenSepaMandate)); @@ -317,7 +313,7 @@ class HsOfficeSepaMandateRepositoryIntegrationTest extends ContextBasedTestWithC public void globalAdmin_withoutAssumedRole_canDeleteAnySepaMandate() { // given context("superuser-alex@hostsharing.net", null); - final var givenSepaMandate = givenSomeTemporarySepaMandateBessler("Fourth eG"); + final var givenSepaMandate = givenSomeTemporarySepaMandate("DE02200505501015871393"); // when final var result = jpaAttempt.transacted(() -> { @@ -337,7 +333,7 @@ class HsOfficeSepaMandateRepositoryIntegrationTest extends ContextBasedTestWithC public void nonGlobalAdmin_canNotDeleteTheirRelatedSepaMandate() { // given context("superuser-alex@hostsharing.net", null); - final var givenSepaMandate = givenSomeTemporarySepaMandateBessler("Third OHG"); + final var givenSepaMandate = givenSomeTemporarySepaMandate("DE02300209000106531065"); // when final var result = jpaAttempt.transacted(() -> { @@ -363,11 +359,7 @@ class HsOfficeSepaMandateRepositoryIntegrationTest extends ContextBasedTestWithC context("superuser-alex@hostsharing.net"); final var initialRoleNames = Array.from(distinctRoleNamesOf(rawRoleRepo.findAll())); final var initialGrantNames = Array.from(distinctGrantDisplaysOf(rawGrantRepo.findAll())); - final var givenSepaMandate = givenSomeTemporarySepaMandateBessler("Mel Bessler"); - assertThat(distinctRoleNamesOf(rawRoleRepo.findAll()).size()).as("precondition failed: unexpected number of roles created") - .isEqualTo(initialRoleNames.length + 5); - assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll()).size()).as("precondition failed: unexpected number of grants created") - .isEqualTo(initialGrantNames.length + 14); + final var givenSepaMandate = givenSomeTemporarySepaMandate("DE02600501010002034304"); // when final var result = jpaAttempt.transacted(() -> { @@ -397,15 +389,16 @@ class HsOfficeSepaMandateRepositoryIntegrationTest extends ContextBasedTestWithC // then assertThat(customerLogEntries).map(Arrays::toString).contains( - "[creating SEPA-mandate test-data FirstGmbH, hs_office_sepamandate, INSERT]", - "[creating SEPA-mandate test-data Seconde.K., hs_office_sepamandate, INSERT]"); + "[creating SEPA-mandate test-data 1000111, hs_office_sepamandate, INSERT]", + "[creating SEPA-mandate test-data 1000212, hs_office_sepamandate, INSERT]", + "[creating SEPA-mandate test-data 1000313, hs_office_sepamandate, INSERT]"); } - private HsOfficeSepaMandateEntity givenSomeTemporarySepaMandateBessler(final String bankAccountHolder) { + private HsOfficeSepaMandateEntity givenSomeTemporarySepaMandate(final String iban) { return jpaAttempt.transacted(() -> { context("superuser-alex@hostsharing.net"); final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike("First").get(0); - final var givenBankAccount = bankAccountRepo.findByOptionalHolderLike(bankAccountHolder).get(0); + final var givenBankAccount = bankAccountRepo.findByIbanOrderByIbanAsc(iban).get(0); final var newSepaMandate = HsOfficeSepaMandateEntity.builder() .debitor(givenDebitor) .bankAccount(givenBankAccount) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/test/ContextBasedTestWithCleanup.java b/src/test/java/net/hostsharing/hsadminng/hs/office/test/ContextBasedTestWithCleanup.java index 9b6c14ed..fc0b81c3 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/test/ContextBasedTestWithCleanup.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/test/ContextBasedTestWithCleanup.java @@ -1,9 +1,10 @@ package net.hostsharing.hsadminng.hs.office.test; import net.hostsharing.hsadminng.context.ContextBasedTest; -import net.hostsharing.hsadminng.persistence.HasUuid; +import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantEntity; import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantRepository; +import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService; import net.hostsharing.hsadminng.rbac.rbacrole.RbacRoleEntity; import net.hostsharing.hsadminng.rbac.rbacrole.RbacRoleRepository; import net.hostsharing.test.JpaAttempt; @@ -16,6 +17,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.Repository; import jakarta.persistence.*; +import java.lang.reflect.Method; import java.util.*; import static java.lang.System.out; @@ -43,7 +45,7 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest { @Autowired JpaAttempt jpaAttempt; - private TreeMap> entitiesToCleanup = new TreeMap<>(); + private TreeMap> entitiesToCleanup = new TreeMap<>(); private static Long latestIntialTestDataSerialId; private static boolean countersInitialized = false; @@ -55,13 +57,21 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest { private Set initialRbacRoles; private Set initialRbacGrants; - public UUID toCleanup(final Class entityClass, final UUID uuidToCleanup) { + private TestInfo testInfo; + + public T refresh(final T entity) { + final var merged = em.merge(entity); + em.refresh(merged); + return merged; + } + + public UUID toCleanup(final Class entityClass, final UUID uuidToCleanup) { out.println("toCleanup(" + entityClass.getSimpleName() + ", " + uuidToCleanup); entitiesToCleanup.put(uuidToCleanup, entityClass); return uuidToCleanup; } - public E toCleanup(final E entity) { + public E toCleanup(final E entity) { out.println("toCleanup(" + entity.getClass() + ", " + entity.getUuid()); if ( entity.getUuid() == null ) { throw new IllegalArgumentException("only persisted entities with valid uuid allowed"); @@ -70,7 +80,7 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest { return entity; } - protected void cleanupAllNew(final Class entityClass) { + protected void cleanupAllNew(final Class entityClass) { if (initialRbacObjects == null) { out.println("skipping cleanupAllNew: " + entityClass.getSimpleName()); return; // TODO: seems @AfterEach is called without any @BeforeEach @@ -151,6 +161,11 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest { return currentCount; } + @BeforeEach + void keepTestInfo(final TestInfo testInfo) { + this.testInfo = testInfo; + } + @AfterEach void cleanupAndCheckCleanup(final TestInfo testInfo) { out.println(ContextBasedTestWithCleanup.class.getSimpleName() + ".cleanupAndCheckCleanup"); @@ -253,6 +268,29 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest { .collect(toSet()); }).assertSuccessful().returnedValue(); } + + /** + * Generates a diagram of the RBAC-Grants to the current subjects (user or assumed roles). + */ + protected void generateRbacDiagramForCurrentSubjects(final EnumSet include) { + final var title = testInfo.getTestMethod().map(Method::getName).orElseThrow(); + RbacGrantsDiagramService.writeToFile( + title, + diagramService.allGrantsToCurrentUser(include), + "doc/" + title + ".md" + ); + } + + /** + * Generates a diagram of the RBAC-Grants for the given object and permission. + */ + protected void generateRbacDiagramForObjectPermission(final UUID targetObject, final String rbacOp, final String name) { + RbacGrantsDiagramService.writeToFile( + name, + diagramService.allGrantsFrom(targetObject, rbacOp, RbacGrantsDiagramService.Include.ALL), + "doc/temp/" + name + ".md" + ); + } } interface RbacObjectRepository extends Repository { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/test/EntityList.java b/src/test/java/net/hostsharing/hsadminng/hs/office/test/EntityList.java new file mode 100644 index 00000000..2cc55e61 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/test/EntityList.java @@ -0,0 +1,15 @@ +package net.hostsharing.hsadminng.hs.office.test; + +import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EntityList { + + public static E one(final List entities) { + assertThat(entities).hasSize(1); + return entities.stream().findFirst().orElseThrow(); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/mapper/PostgresArrayIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/mapper/PostgresArrayIntegrationTest.java index 8f3e95e0..3542caa1 100644 --- a/src/test/java/net/hostsharing/hsadminng/mapper/PostgresArrayIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/mapper/PostgresArrayIntegrationTest.java @@ -7,7 +7,6 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import jakarta.persistence.EntityManager; import java.util.UUID; -import java.util.function.Function; import static org.assertj.core.api.Assertions.assertThat; @@ -30,9 +29,7 @@ class PostgresArrayIntegrationTest { return emptyArray; end; $$; """).executeUpdate(); - final byte[] pgArray = (byte[]) em.createNativeQuery("SELECT returnEmptyArray()", String[].class).getSingleResult(); - - final String[] result = PostgresArray.fromPostgresArray(pgArray, String.class, Function.identity()); + final String[] result = (String[]) em.createNativeQuery("SELECT returnEmptyArray()", String[].class).getSingleResult(); assertThat(result).isEmpty(); } @@ -53,9 +50,7 @@ class PostgresArrayIntegrationTest { return array[text1, text2, text3, null, text4]; end; $$; """).executeUpdate(); - final byte[] pgArray = (byte[]) em.createNativeQuery("SELECT returnStringArray()", String[].class).getSingleResult(); - - final String[] result = PostgresArray.fromPostgresArray(pgArray, String.class, Function.identity()); + final String[] result = (String[]) em.createNativeQuery("SELECT returnStringArray()", String[].class).getSingleResult(); assertThat(result).containsExactly("one", "two, three", "four; five", null, "say \"Hello\" to me"); } @@ -75,9 +70,7 @@ class PostgresArrayIntegrationTest { return ARRAY[uuid1, uuid2, null, uuid3]; end; $$; """).executeUpdate(); - final byte[] pgArray = (byte[]) em.createNativeQuery("SELECT returnUuidArray()", UUID[].class).getSingleResult(); - - final UUID[] result = PostgresArray.fromPostgresArray(pgArray, UUID.class, UUID::fromString); + final UUID[] result = (UUID[]) em.createNativeQuery("SELECT returnUuidArray()", UUID[].class).getSingleResult(); assertThat(result).containsExactly( UUID.fromString("f47ac10b-58cc-4372-a567-0e02b2c3d479"), diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantControllerAcceptanceTest.java index 6f0abc93..15738504 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantControllerAcceptanceTest.java @@ -73,36 +73,38 @@ class RbacGrantControllerAcceptanceTest extends ContextBasedTest { .contentType("application/json") .body("", hasItem( allOf( - hasEntry("grantedByRoleIdName", "global#global.admin"), - hasEntry("grantedRoleIdName", "test_customer#xxx.admin"), + // TODO: should there be a grantedByRole or just a grantedByTrigger? + hasEntry("grantedByRoleIdName", "test_customer#xxx:OWNER"), + hasEntry("grantedRoleIdName", "test_customer#xxx:ADMIN"), hasEntry("granteeUserName", "customer-admin@xxx.example.com") ) )) .body("", hasItem( allOf( - hasEntry("grantedByRoleIdName", "global#global.admin"), - hasEntry("grantedRoleIdName", "test_customer#yyy.admin"), + // TODO: should there be a grantedByRole or just a grantedByTrigger? + hasEntry("grantedByRoleIdName", "test_customer#yyy:OWNER"), + hasEntry("grantedRoleIdName", "test_customer#yyy:ADMIN"), hasEntry("granteeUserName", "customer-admin@yyy.example.com") ) )) .body("", hasItem( allOf( - hasEntry("grantedByRoleIdName", "global#global.admin"), - hasEntry("grantedRoleIdName", "global#global.admin"), + hasEntry("grantedByRoleIdName", "global#global:ADMIN"), + hasEntry("grantedRoleIdName", "global#global:ADMIN"), hasEntry("granteeUserName", "superuser-fran@hostsharing.net") ) )) .body("", hasItem( allOf( - hasEntry("grantedByRoleIdName", "test_customer#xxx.admin"), - hasEntry("grantedRoleIdName", "test_package#xxx00.admin"), + hasEntry("grantedByRoleIdName", "test_customer#xxx:ADMIN"), + hasEntry("grantedRoleIdName", "test_package#xxx00:ADMIN"), hasEntry("granteeUserName", "pac-admin-xxx00@xxx.example.com") ) )) .body("", hasItem( allOf( - hasEntry("grantedByRoleIdName", "test_customer#zzz.admin"), - hasEntry("grantedRoleIdName", "test_package#zzz02.admin"), + hasEntry("grantedByRoleIdName", "test_customer#zzz:ADMIN"), + hasEntry("grantedRoleIdName", "test_package#zzz02:ADMIN"), hasEntry("granteeUserName", "pac-admin-zzz02@zzz.example.com") ) )) @@ -116,7 +118,7 @@ class RbacGrantControllerAcceptanceTest extends ContextBasedTest { RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") - .header("assumed-roles", "test_package#yyy00.admin") + .header("assumed-roles", "test_package#yyy00:ADMIN") .port(port) .when() .get("http://localhost/api/rbac/grants") @@ -125,8 +127,8 @@ class RbacGrantControllerAcceptanceTest extends ContextBasedTest { .contentType("application/json") .body("", hasItem( allOf( - hasEntry("grantedByRoleIdName", "test_customer#yyy.admin"), - hasEntry("grantedRoleIdName", "test_package#yyy00.admin"), + hasEntry("grantedByRoleIdName", "test_customer#yyy:ADMIN"), + hasEntry("grantedRoleIdName", "test_package#yyy00:ADMIN"), hasEntry("granteeUserName", "pac-admin-yyy00@yyy.example.com") ) )) @@ -148,13 +150,13 @@ class RbacGrantControllerAcceptanceTest extends ContextBasedTest { .contentType("application/json") .body("", hasItem( allOf( - hasEntry("grantedByRoleIdName", "test_customer#yyy.admin"), - hasEntry("grantedRoleIdName", "test_package#yyy00.admin"), + hasEntry("grantedByRoleIdName", "test_customer#yyy:ADMIN"), + hasEntry("grantedRoleIdName", "test_package#yyy00:ADMIN"), hasEntry("granteeUserName", "pac-admin-yyy00@yyy.example.com") ) )) - .body("[0].grantedByRoleIdName", is("test_customer#yyy.admin")) - .body("[0].grantedRoleIdName", is("test_package#yyy00.admin")) + .body("[0].grantedByRoleIdName", is("test_customer#yyy:ADMIN")) + .body("[0].grantedRoleIdName", is("test_package#yyy00:ADMIN")) .body("[0].granteeUserName", is("pac-admin-yyy00@yyy.example.com")); // @formatter:on } @@ -169,7 +171,7 @@ class RbacGrantControllerAcceptanceTest extends ContextBasedTest { // given final var givenCurrentUserAsPackageAdmin = new Subject("customer-admin@xxx.example.com"); final var givenGranteeUser = findRbacUserByName("pac-admin-xxx00@xxx.example.com"); - final var givenGrantedRole = findRbacRoleByName("test_package#xxx00.admin"); + final var givenGrantedRole = getRbacRoleByName("test_package#xxx00:ADMIN"); // when final var grant = givenCurrentUserAsPackageAdmin.getGrantById() @@ -178,8 +180,8 @@ class RbacGrantControllerAcceptanceTest extends ContextBasedTest { // then grant.assertThat() .statusCode(200) - .body("grantedByRoleIdName", is("test_customer#xxx.admin")) - .body("grantedRoleIdName", is("test_package#xxx00.admin")) + .body("grantedByRoleIdName", is("test_customer#xxx:ADMIN")) + .body("grantedRoleIdName", is("test_package#xxx00:ADMIN")) .body("granteeUserName", is("pac-admin-xxx00@xxx.example.com")); } @@ -189,7 +191,7 @@ class RbacGrantControllerAcceptanceTest extends ContextBasedTest { // given final var givenCurrentUserAsPackageAdmin = new Subject("pac-admin-xxx00@xxx.example.com"); final var givenGranteeUser = findRbacUserByName("pac-admin-xxx00@xxx.example.com"); - final var givenGrantedRole = findRbacRoleByName("test_package#xxx00.admin"); + final var givenGrantedRole = getRbacRoleByName("test_package#xxx00:ADMIN"); // when final var grant = givenCurrentUserAsPackageAdmin.getGrantById() @@ -198,8 +200,8 @@ class RbacGrantControllerAcceptanceTest extends ContextBasedTest { // then grant.assertThat() .statusCode(200) - .body("grantedByRoleIdName", is("test_customer#xxx.admin")) - .body("grantedRoleIdName", is("test_package#xxx00.admin")) + .body("grantedByRoleIdName", is("test_customer#xxx:ADMIN")) + .body("grantedRoleIdName", is("test_package#xxx00:ADMIN")) .body("granteeUserName", is("pac-admin-xxx00@xxx.example.com")); } @@ -209,9 +211,9 @@ class RbacGrantControllerAcceptanceTest extends ContextBasedTest { // given final var givenCurrentUserAsPackageAdmin = new Subject( "pac-admin-xxx00@xxx.example.com", - "test_package#xxx00.admin"); + "test_package#xxx00:ADMIN"); final var givenGranteeUser = findRbacUserByName("pac-admin-xxx00@xxx.example.com"); - final var givenGrantedRole = findRbacRoleByName("test_package#xxx00.admin"); + final var givenGrantedRole = getRbacRoleByName("test_package#xxx00:ADMIN"); // when final var grant = givenCurrentUserAsPackageAdmin.getGrantById() @@ -220,8 +222,8 @@ class RbacGrantControllerAcceptanceTest extends ContextBasedTest { // then grant.assertThat() .statusCode(200) - .body("grantedByRoleIdName", is("test_customer#xxx.admin")) - .body("grantedRoleIdName", is("test_package#xxx00.admin")) + .body("grantedByRoleIdName", is("test_customer#xxx:ADMIN")) + .body("grantedRoleIdName", is("test_package#xxx00:ADMIN")) .body("granteeUserName", is("pac-admin-xxx00@xxx.example.com")); } @@ -232,9 +234,9 @@ class RbacGrantControllerAcceptanceTest extends ContextBasedTest { // given final var givenCurrentUserAsPackageAdmin = new Subject( "pac-admin-xxx00@xxx.example.com", - "test_package#xxx00.tenant"); + "test_package#xxx00:TENANT"); final var givenGranteeUser = findRbacUserByName("pac-admin-xxx00@xxx.example.com"); - final var givenGrantedRole = findRbacRoleByName("test_package#xxx00.admin"); + final var givenGrantedRole = getRbacRoleByName("test_package#xxx00:ADMIN"); final var grant = givenCurrentUserAsPackageAdmin.getGrantById() .forGrantedRole(givenGrantedRole).toGranteeUser(givenGranteeUser); @@ -253,10 +255,10 @@ class RbacGrantControllerAcceptanceTest extends ContextBasedTest { // given final var givenNewUser = createRBacUser(); - final var givenRoleToGrant = "test_package#xxx00.admin"; + final var givenRoleToGrant = "test_package#xxx00:ADMIN"; final var givenCurrentUserAsPackageAdmin = new Subject("pac-admin-xxx00@xxx.example.com", givenRoleToGrant); final var givenOwnPackageAdminRole = - findRbacRoleByName(givenCurrentUserAsPackageAdmin.assumedRole); + getRbacRoleByName(givenCurrentUserAsPackageAdmin.assumedRole); // when final var response = givenCurrentUserAsPackageAdmin @@ -266,15 +268,15 @@ class RbacGrantControllerAcceptanceTest extends ContextBasedTest { // then response.assertThat() .statusCode(201) - .body("grantedByRoleIdName", is("test_package#xxx00.admin")) + .body("grantedByRoleIdName", is("test_package#xxx00:ADMIN")) .body("assumed", is(true)) - .body("grantedRoleIdName", is("test_package#xxx00.admin")) + .body("grantedRoleIdName", is("test_package#xxx00:ADMIN")) .body("granteeUserName", is(givenNewUser.getName())); assertThat(findAllGrantsOf(givenCurrentUserAsPackageAdmin)) .extracting(RbacGrantEntity::toDisplay) - .contains("{ grant role " + givenOwnPackageAdminRole.getRoleName() + - " to user " + givenNewUser.getName() + - " by role " + givenRoleToGrant + " and assume }"); + .contains("{ grant role:" + givenOwnPackageAdminRole.getRoleName() + + " to user:" + givenNewUser.getName() + + " by role:" + givenRoleToGrant + " and assume }"); } @Test @@ -283,9 +285,9 @@ class RbacGrantControllerAcceptanceTest extends ContextBasedTest { // given final var givenNewUser = createRBacUser(); - final var givenRoleToGrant = "test_package#xxx00.admin"; + final var givenRoleToGrant = "test_package#xxx00:ADMIN"; final var givenCurrentUserAsPackageAdmin = new Subject("pac-admin-xxx00@xxx.example.com", givenRoleToGrant); - final var givenAlienPackageAdminRole = findRbacRoleByName("test_package#yyy00.admin"); + final var givenAlienPackageAdminRole = getRbacRoleByName("test_package#yyy00:ADMIN"); // when final var result = givenCurrentUserAsPackageAdmin @@ -296,7 +298,7 @@ class RbacGrantControllerAcceptanceTest extends ContextBasedTest { result.assertThat() .statusCode(403) .body("message", containsString("Access to granted role")) - .body("message", containsString("forbidden for {test_package#xxx00.admin}")); + .body("message", containsString("forbidden for test_package#xxx00:ADMIN")); assertThat(findAllGrantsOf(givenCurrentUserAsPackageAdmin)) .extracting(RbacGrantEntity::getGranteeUserName) .doesNotContain(givenNewUser.getName()); @@ -313,9 +315,9 @@ class RbacGrantControllerAcceptanceTest extends ContextBasedTest { // given final var givenArbitraryUser = createRBacUser(); - final var givenRoleToGrant = "test_package#xxx00.admin"; + final var givenRoleToGrant = "test_package#xxx00:ADMIN"; final var givenCurrentUserAsPackageAdmin = new Subject("pac-admin-xxx00@xxx.example.com", givenRoleToGrant); - final var givenOwnPackageAdminRole = findRbacRoleByName("test_package#xxx00.admin"); + final var givenOwnPackageAdminRole = getRbacRoleByName("test_package#xxx00:ADMIN"); // and given an existing grant assumeCreated(givenCurrentUserAsPackageAdmin @@ -323,7 +325,7 @@ class RbacGrantControllerAcceptanceTest extends ContextBasedTest { .toUser(givenArbitraryUser)); assumeGrantExists( givenCurrentUserAsPackageAdmin, - "{ grant role %s to user %s by role %s and assume }".formatted( + "{ grant role:%s to user:%s by role:%s and assume }".formatted( givenOwnPackageAdminRole.getRoleName(), givenArbitraryUser.getName(), givenCurrentUserAsPackageAdmin.assumedRole)); @@ -502,13 +504,13 @@ class RbacGrantControllerAcceptanceTest extends ContextBasedTest { return jpaAttempt.transacted(() -> { context("superuser-alex@hostsharing.net", null); return rbacUserRepository.findByName(userName); - }).returnedValue(); + }).assertNotNull().returnedValue(); } - RbacRoleEntity findRbacRoleByName(final String roleName) { + RbacRoleEntity getRbacRoleByName(final String roleName) { return jpaAttempt.transacted(() -> { context("superuser-alex@hostsharing.net", null); return rbacRoleRepository.findByRoleName(roleName); - }).returnedValue(); + }).assertNotNull().returnedValue(); } } diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantEntityUnitTest.java index eea18932..c0bd82cc 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantEntityUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantEntityUnitTest.java @@ -34,13 +34,13 @@ class RbacGrantEntityUnitTest { "GrantEE", UUID.randomUUID(), true, "ObjectTable", "ObjectId", UUID.randomUUID(), - RbacRoleType.admin); // @formatter:on + RbacRoleType.ADMIN); // @formatter:on // when final var display = entity.toDisplay(); // then - assertThat(display).isEqualTo("{ grant role GrantED to user GrantEE by role GrantER and assume }"); + assertThat(display).isEqualTo("{ grant role:GrantED to user:GrantEE by role:GrantER and assume }"); } @Test @@ -52,12 +52,12 @@ class RbacGrantEntityUnitTest { "GrantEE", UUID.randomUUID(), false, "ObjectTable", "ObjectId", UUID.randomUUID(), - RbacRoleType.owner); // @formatter:on + RbacRoleType.OWNER); // @formatter:on // when final var display = entity.toDisplay(); // then - assertThat(display).isEqualTo("{ grant role GrantED to user GrantEE by role GrantER }"); + assertThat(display).isEqualTo("{ grant role:GrantED to user:GrantEE by role:GrantER }"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantRepositoryIntegrationTest.java index 3b09e861..0ee1f297 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantRepositoryIntegrationTest.java @@ -69,7 +69,7 @@ class RbacGrantRepositoryIntegrationTest extends ContextBasedTest { // then exactlyTheseRbacGrantsAreReturned( result, - "{ grant role test_package#xxx00.admin to user pac-admin-xxx00@xxx.example.com by role test_customer#xxx.admin and assume }"); + "{ grant role:test_package#xxx00:ADMIN to user:pac-admin-xxx00@xxx.example.com by role:test_customer#xxx:ADMIN and assume }"); } @Test @@ -84,17 +84,17 @@ class RbacGrantRepositoryIntegrationTest extends ContextBasedTest { // then exactlyTheseRbacGrantsAreReturned( result, - "{ grant role test_customer#xxx.admin to user customer-admin@xxx.example.com by role global#global.admin and assume }", - "{ grant role test_package#xxx00.admin to user pac-admin-xxx00@xxx.example.com by role test_customer#xxx.admin and assume }", - "{ grant role test_package#xxx01.admin to user pac-admin-xxx01@xxx.example.com by role test_customer#xxx.admin and assume }", - "{ grant role test_package#xxx02.admin to user pac-admin-xxx02@xxx.example.com by role test_customer#xxx.admin and assume }"); + "{ grant role:test_customer#xxx:ADMIN to user:customer-admin@xxx.example.com by role:test_customer#xxx:OWNER and assume }", + "{ grant role:test_package#xxx00:ADMIN to user:pac-admin-xxx00@xxx.example.com by role:test_customer#xxx:ADMIN and assume }", + "{ grant role:test_package#xxx01:ADMIN to user:pac-admin-xxx01@xxx.example.com by role:test_customer#xxx:ADMIN and assume }", + "{ grant role:test_package#xxx02:ADMIN to user:pac-admin-xxx02@xxx.example.com by role:test_customer#xxx:ADMIN and assume }"); } @Test @Accepts({ "GRT:L(List)" }) public void customerAdmin_withAssumedRole_canOnlyViewRbacGrantsVisibleByAssumedRole() { // given: - context("customer-admin@xxx.example.com", "test_package#xxx00.admin"); + context("customer-admin@xxx.example.com", "test_package#xxx00:ADMIN"); // when final var result = rbacGrantRepository.findAll(); @@ -102,7 +102,7 @@ class RbacGrantRepositoryIntegrationTest extends ContextBasedTest { // then exactlyTheseRbacGrantsAreReturned( result, - "{ grant role test_package#xxx00.admin to user pac-admin-xxx00@xxx.example.com by role test_customer#xxx.admin and assume }"); + "{ grant role:test_package#xxx00:ADMIN to user:pac-admin-xxx00@xxx.example.com by role:test_customer#xxx:ADMIN and assume }"); } } @@ -112,9 +112,9 @@ class RbacGrantRepositoryIntegrationTest extends ContextBasedTest { @Test public void customerAdmin_canGrantOwnPackageAdminRole_toArbitraryUser() { // given - context("customer-admin@xxx.example.com", "test_customer#xxx.admin"); + context("customer-admin@xxx.example.com", "test_customer#xxx:ADMIN"); final var givenArbitraryUserUuid = rbacUserRepository.findByName("pac-admin-zzz00@zzz.example.com").getUuid(); - final var givenOwnPackageRoleUuid = rbacRoleRepository.findByRoleName("test_package#xxx00.admin").getUuid(); + final var givenOwnPackageRoleUuid = rbacRoleRepository.findByRoleName("test_package#xxx00:ADMIN").getUuid(); // when final var grant = RbacGrantEntity.builder() @@ -130,7 +130,7 @@ class RbacGrantRepositoryIntegrationTest extends ContextBasedTest { assertThat(rbacGrantRepository.findAll()) .extracting(RbacGrantEntity::toDisplay) .contains( - "{ grant role test_package#xxx00.admin to user pac-admin-zzz00@zzz.example.com by role test_customer#xxx.admin and assume }"); + "{ grant role:test_package#xxx00:ADMIN to user:pac-admin-zzz00@zzz.example.com by role:test_customer#xxx:ADMIN and assume }"); } @Test @@ -143,14 +143,14 @@ class RbacGrantRepositoryIntegrationTest extends ContextBasedTest { context("customer-admin@xxx.example.com", null); return new Given( createNewUser(), - rbacRoleRepository.findByRoleName("test_package#xxx00.owner").getUuid() + rbacRoleRepository.findByRoleName("test_package#xxx00:OWNER").getUuid() ); }).assumeSuccessful().returnedValue(); // when final var attempt = jpaAttempt.transacted(() -> { // now we try to use these uuids as a less privileged user - context("pac-admin-xxx00@xxx.example.com", "test_package#xxx00.admin"); + context("pac-admin-xxx00@xxx.example.com", "test_package#xxx00:ADMIN"); final var grant = RbacGrantEntity.builder() .granteeUserUuid(given.arbitraryUser.getUuid()) .grantedRoleUuid(given.packageOwnerRoleUuid) @@ -162,8 +162,8 @@ class RbacGrantRepositoryIntegrationTest extends ContextBasedTest { // then attempt.assertExceptionWithRootCauseMessage( JpaSystemException.class, - "ERROR: [403] Access to granted role " + given.packageOwnerRoleUuid - + " forbidden for {test_package#xxx00.admin}"); + "ERROR: [403] Access to granted role test_package#xxx00:OWNER", + "forbidden for test_package#xxx00:ADMIN"); jpaAttempt.transacted(() -> { // finally, we use the new user to make sure, no roles were granted context(given.arbitraryUser.getName(), null); @@ -180,16 +180,16 @@ class RbacGrantRepositoryIntegrationTest extends ContextBasedTest { public void customerAdmin_canRevokeSelfGrantedPackageAdminRole() { // given final var grant = create(grant() - .byUser("customer-admin@xxx.example.com").withAssumedRole("test_customer#xxx.admin") - .grantingRole("test_package#xxx00.admin").toUser("pac-admin-zzz00@zzz.example.com")); + .byUser("customer-admin@xxx.example.com").withAssumedRole("test_customer#xxx:ADMIN") + .grantingRole("test_package#xxx00:ADMIN").toUser("pac-admin-zzz00@zzz.example.com")); // when - context("customer-admin@xxx.example.com", "test_customer#xxx.admin"); + context("customer-admin@xxx.example.com", "test_customer#xxx:ADMIN"); final var revokeAttempt = attempt(em, () -> rbacGrantRepository.deleteByRbacGrantId(grant.getRbacGrantId())); // then - context("customer-admin@xxx.example.com", "test_customer#xxx.admin"); + context("customer-admin@xxx.example.com", "test_customer#xxx:ADMIN"); assertThat(revokeAttempt.caughtExceptionsRootCause()).isNull(); assertThat(rbacGrantRepository.findAll()) .extracting(RbacGrantEntity::getGranteeUserName) @@ -201,17 +201,17 @@ class RbacGrantRepositoryIntegrationTest extends ContextBasedTest { // given final var newUser = createNewUserTransacted(); final var grant = create(grant() - .byUser("customer-admin@xxx.example.com").withAssumedRole("test_package#xxx00.admin") - .grantingRole("test_package#xxx00.admin").toUser(newUser.getName())); + .byUser("customer-admin@xxx.example.com").withAssumedRole("test_package#xxx00:ADMIN") + .grantingRole("test_package#xxx00:ADMIN").toUser(newUser.getName())); // when - context("pac-admin-xxx00@xxx.example.com", "test_package#xxx00.admin"); + context("pac-admin-xxx00@xxx.example.com", "test_package#xxx00:ADMIN"); final var revokeAttempt = attempt(em, () -> rbacGrantRepository.deleteByRbacGrantId(grant.getRbacGrantId())); // then assertThat(revokeAttempt.caughtExceptionsRootCause()).isNull(); - context("customer-admin@xxx.example.com", "test_customer#xxx.admin"); + context("customer-admin@xxx.example.com", "test_customer#xxx:ADMIN"); assertThat(rbacGrantRepository.findAll()) .extracting(RbacGrantEntity::getGranteeUserName) .doesNotContain("pac-admin-zzz00@zzz.example.com"); @@ -221,19 +221,19 @@ class RbacGrantRepositoryIntegrationTest extends ContextBasedTest { public void packageAdmin_canNotRevokeOwnPackageAdminRoleGrantedByOwnerRoleOfThatPackage() { // given final var grant = create(grant() - .byUser("customer-admin@xxx.example.com").withAssumedRole("test_package#xxx00.owner") - .grantingRole("test_package#xxx00.admin").toUser("pac-admin-zzz00@zzz.example.com")); - final var grantedByRole = rbacRoleRepository.findByRoleName("test_package#xxx00.owner"); + .byUser("customer-admin@xxx.example.com").withAssumedRole("test_package#xxx00:OWNER") + .grantingRole("test_package#xxx00:ADMIN").toUser("pac-admin-zzz00@zzz.example.com")); + final var grantedByRole = rbacRoleRepository.findByRoleName("test_package#xxx00:OWNER"); // when - context("pac-admin-xxx00@xxx.example.com", "test_package#xxx00.admin"); + context("pac-admin-xxx00@xxx.example.com", "test_package#xxx00:ADMIN"); final var revokeAttempt = attempt(em, () -> rbacGrantRepository.deleteByRbacGrantId(grant.getRbacGrantId())); // then revokeAttempt.assertExceptionWithRootCauseMessage( JpaSystemException.class, - "ERROR: [403] Revoking role created by %s is forbidden for {test_package#xxx00.admin}.".formatted( + "ERROR: [403] Revoking role created by %s is forbidden for {test_package#xxx00:ADMIN}.".formatted( grantedByRole.getUuid() )); } @@ -254,7 +254,7 @@ class RbacGrantRepositoryIntegrationTest extends ContextBasedTest { assertThat(grantAttempt.caughtException()).isNull(); assertThat(rawRbacGrantRepository.findAll()) .extracting(RawRbacGrantEntity::toDisplay) - .contains("{ grant role %s to user %s by %s and assume }".formatted( + .contains("{ grant role:%s to user:%s by %s and assume }".formatted( with.grantedRole, with.granteeUserName, with.assumedRole )); diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramServiceIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramServiceIntegrationTest.java new file mode 100644 index 00000000..5d228314 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramServiceIntegrationTest.java @@ -0,0 +1,103 @@ +package net.hostsharing.hsadminng.rbac.rbacgrant; + +import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.hsadminng.hs.office.test.ContextBasedTestWithCleanup; +import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService.Include; +import net.hostsharing.test.JpaAttempt; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.EnumSet; +import java.util.UUID; + +import static java.lang.String.join; +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@Import( { Context.class, JpaAttempt.class, RbacGrantsDiagramService.class}) +class RbacGrantsDiagramServiceIntegrationTest extends ContextBasedTestWithCleanup { + + @Autowired + RbacGrantsDiagramService grantsMermaidService; + + @MockBean + HttpServletRequest request; + + @Autowired + Context context; + + @Autowired + RbacGrantsDiagramService diagramService; + + TestInfo test; + + @BeforeEach + void init(TestInfo testInfo) { + this.test = testInfo; + } + + protected void context(final String currentUser, final String assumedRoles) { + context.define(test.getDisplayName(), null, currentUser, assumedRoles); + } + + protected void context(final String currentUser) { + context(currentUser, null); + } + + @Test + void allGrantsToCurrentUser() { + context("superuser-alex@hostsharing.net", "test_domain#xxx00-aaaa:OWNER"); + final var graph = grantsMermaidService.allGrantsToCurrentUser(EnumSet.of(Include.TEST_ENTITIES)); + + assertThat(graph).isEqualTo(""" + flowchart TB + + role:test_domain#xxx00-aaaa:ADMIN --> role:test_package#xxx00:TENANT + role:test_domain#xxx00-aaaa:OWNER --> role:test_domain#xxx00-aaaa:ADMIN + role:test_domain#xxx00-aaaa:OWNER --> role:test_package#xxx00:TENANT + role:test_package#xxx00:TENANT --> role:test_customer#xxx:TENANT + """.trim()); + } + + @Test + void allGrantsToCurrentUserIncludingPermissions() { + context("superuser-alex@hostsharing.net", "test_domain#xxx00-aaaa:OWNER"); + final var graph = grantsMermaidService.allGrantsToCurrentUser(EnumSet.of(Include.TEST_ENTITIES, Include.PERMISSIONS)); + + assertThat(graph).isEqualTo(""" + flowchart TB + + role:test_customer#xxx:TENANT --> perm:test_customer#xxx:SELECT + role:test_domain#xxx00-aaaa:ADMIN --> perm:test_domain#xxx00-aaaa:SELECT + role:test_domain#xxx00-aaaa:ADMIN --> role:test_package#xxx00:TENANT + role:test_domain#xxx00-aaaa:OWNER --> perm:test_domain#xxx00-aaaa:DELETE + role:test_domain#xxx00-aaaa:OWNER --> perm:test_domain#xxx00-aaaa:UPDATE + role:test_domain#xxx00-aaaa:OWNER --> role:test_domain#xxx00-aaaa:ADMIN + role:test_domain#xxx00-aaaa:OWNER --> role:test_package#xxx00:TENANT + role:test_package#xxx00:TENANT --> perm:test_package#xxx00:SELECT + role:test_package#xxx00:TENANT --> role:test_customer#xxx:TENANT + """.trim()); + } + + @Test + @Disabled // enable to generate from a real database + void print() throws IOException { + //context("superuser-alex@hostsharing.net", "hs_office_person#FirbySusan:ADMIN"); + context("superuser-alex@hostsharing.net"); + + //final var graph = grantsMermaidService.allGrantsToCurrentUser(EnumSet.of(Include.NON_TEST_ENTITIES, Include.PERMISSIONS)); + + final var targetObject = (UUID) em.createNativeQuery("SELECT uuid FROM hs_office_coopassetstransaction WHERE reference='ref 1000101-1'").getSingleResult(); + final var graph = grantsMermaidService.allGrantsFrom(targetObject, "view", EnumSet.of(Include.USERS)); + + RbacGrantsDiagramService.writeToFile(join(";", context.getAssumedRoles()), graph, "doc/all-grants.md"); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RawRbacObjectEntity.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RawRbacObjectEntity.java new file mode 100644 index 00000000..d4256e56 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RawRbacObjectEntity.java @@ -0,0 +1,31 @@ +package net.hostsharing.hsadminng.rbac.rbacrole; + +import lombok.*; +import org.jetbrains.annotations.NotNull; +import org.springframework.data.annotation.Immutable; + +import jakarta.persistence.*; +import java.util.List; +import java.util.UUID; + +@Entity +@Table(name = "rbacobject") // TODO: create view rbacobject_ev +@Getter +@Setter +@ToString +@Immutable +@NoArgsConstructor +@AllArgsConstructor +public class RawRbacObjectEntity { + + @Id + private UUID uuid; + + @Column(name="objecttable") + private String objectTable; + + @NotNull + public static List objectDisplaysOf(@NotNull final List roles) { + return roles.stream().map(e -> e.objectTable+ "#" + e.uuid).sorted().toList(); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RawRbacObjectRepository.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RawRbacObjectRepository.java new file mode 100644 index 00000000..ab645316 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RawRbacObjectRepository.java @@ -0,0 +1,11 @@ +package net.hostsharing.hsadminng.rbac.rbacrole; + +import org.springframework.data.repository.Repository; + +import java.util.List; +import java.util.UUID; + +public interface RawRbacObjectRepository extends Repository { + + List findAll(); +} diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RawRbacRoleEntity.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RawRbacRoleEntity.java index 2f4d15f5..e80f8ce6 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RawRbacRoleEntity.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RawRbacRoleEntity.java @@ -35,7 +35,7 @@ public class RawRbacRoleEntity { @Enumerated(EnumType.STRING) private RbacRoleType roleType; - @Formula("objectTable||'#'||objectIdName||'.'||roleType") + @Formula("objectTable||'#'||objectIdName||':'||roleType") private String roleName; @NotNull diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleControllerAcceptanceTest.java index 5de93348..d318cc04 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleControllerAcceptanceTest.java @@ -45,14 +45,14 @@ class RbacRoleControllerAcceptanceTest { .then().assertThat() .statusCode(200) .contentType("application/json") - .body("", hasItem(hasEntry("roleName", "test_customer#xxx.admin"))) - .body("", hasItem(hasEntry("roleName", "test_customer#xxx.owner"))) - .body("", hasItem(hasEntry("roleName", "test_customer#xxx.tenant"))) + .body("", hasItem(hasEntry("roleName", "test_customer#xxx:ADMIN"))) + .body("", hasItem(hasEntry("roleName", "test_customer#xxx:OWNER"))) + .body("", hasItem(hasEntry("roleName", "test_customer#xxx:TENANT"))) // ... - .body("", hasItem(hasEntry("roleName", "global#global.admin"))) - .body("", hasItem(hasEntry("roleName", "test_customer#yyy.admin"))) - .body("", hasItem(hasEntry("roleName", "test_package#yyy00.admin"))) - .body("", hasItem(hasEntry("roleName", "test_domain#yyy00-aaaa.owner"))) + .body("", hasItem(hasEntry("roleName", "global#global:ADMIN"))) + .body("", hasItem(hasEntry("roleName", "test_customer#yyy:ADMIN"))) + .body("", hasItem(hasEntry("roleName", "test_package#yyy00:ADMIN"))) + .body("", hasItem(hasEntry("roleName", "test_domain#yyy00-aaaa:OWNER"))) .body( "size()", greaterThanOrEqualTo(73)); // increases with new test data // @formatter:on } @@ -65,7 +65,7 @@ class RbacRoleControllerAcceptanceTest { RestAssured .given() .header("current-user", "superuser-alex@hostsharing.net") - .header("assumed-roles", "test_package#yyy00.admin") + .header("assumed-roles", "test_package#yyy00:ADMIN") .port(port) .when() .get("http://localhost/api/rbac/roles") @@ -75,18 +75,18 @@ class RbacRoleControllerAcceptanceTest { .statusCode(200) .contentType("application/json") - .body("", hasItem(hasEntry("roleName", "test_customer#yyy.tenant"))) - .body("", hasItem(hasEntry("roleName", "test_domain#yyy00-aaaa.owner"))) - .body("", hasItem(hasEntry("roleName", "test_domain#yyy00-aaaa.admin"))) - .body("", hasItem(hasEntry("roleName", "test_domain#yyy00-aaab.owner"))) - .body("", hasItem(hasEntry("roleName", "test_domain#yyy00-aaab.admin"))) - .body("", hasItem(hasEntry("roleName", "test_package#yyy00.admin"))) - .body("", hasItem(hasEntry("roleName", "test_package#yyy00.tenant"))) + .body("", hasItem(hasEntry("roleName", "test_customer#yyy:TENANT"))) + .body("", hasItem(hasEntry("roleName", "test_domain#yyy00-aaaa:OWNER"))) + .body("", hasItem(hasEntry("roleName", "test_domain#yyy00-aaaa:ADMIN"))) + .body("", hasItem(hasEntry("roleName", "test_domain#yyy00-aaab:OWNER"))) + .body("", hasItem(hasEntry("roleName", "test_domain#yyy00-aaab:ADMIN"))) + .body("", hasItem(hasEntry("roleName", "test_package#yyy00:ADMIN"))) + .body("", hasItem(hasEntry("roleName", "test_package#yyy00:TENANT"))) - .body("", not(hasItem(hasEntry("roleName", "test_customer#xxx.tenant")))) - .body("", not(hasItem(hasEntry("roleName", "test_domain#xxx00-aaaa.admin")))) - .body("", not(hasItem(hasEntry("roleName", "test_package#xxx00.admin")))) - .body("", not(hasItem(hasEntry("roleName", "test_package#xxx00.tenant")))) + .body("", not(hasItem(hasEntry("roleName", "test_customer#xxx:TENANT")))) + .body("", not(hasItem(hasEntry("roleName", "test_domain#xxx00-aaaa:ADMIN")))) + .body("", not(hasItem(hasEntry("roleName", "test_package#xxx00:ADMIN")))) + .body("", not(hasItem(hasEntry("roleName", "test_package#xxx00:TENANT")))) ; // @formatter:on } @@ -106,15 +106,15 @@ class RbacRoleControllerAcceptanceTest { .statusCode(200) .contentType("application/json") - .body("", hasItem(hasEntry("roleName", "test_customer#zzz.tenant"))) - .body("", hasItem(hasEntry("roleName", "test_domain#zzz00-aaaa.admin"))) - .body("", hasItem(hasEntry("roleName", "test_package#zzz00.admin"))) - .body("", hasItem(hasEntry("roleName", "test_package#zzz00.tenant"))) + .body("", hasItem(hasEntry("roleName", "test_customer#zzz:TENANT"))) + .body("", hasItem(hasEntry("roleName", "test_domain#zzz00-aaaa:ADMIN"))) + .body("", hasItem(hasEntry("roleName", "test_package#zzz00:ADMIN"))) + .body("", hasItem(hasEntry("roleName", "test_package#zzz00:TENANT"))) - .body("", not(hasItem(hasEntry("roleName", "test_customer#yyy.tenant")))) - .body("", not(hasItem(hasEntry("roleName", "test_domain#yyy00-aaaa.admin")))) - .body("", not(hasItem(hasEntry("roleName", "test_package#yyy00.admin")))) - .body("", not(hasItem(hasEntry("roleName", "test_package#yyy00.tenant")))); + .body("", not(hasItem(hasEntry("roleName", "test_customer#yyy:TENANT")))) + .body("", not(hasItem(hasEntry("roleName", "test_domain#yyy00-aaaa:ADMIN")))) + .body("", not(hasItem(hasEntry("roleName", "test_package#yyy00:ADMIN")))) + .body("", not(hasItem(hasEntry("roleName", "test_package#yyy00:TENANT")))); // @formatter:on } } diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleControllerRestTest.java index c10a9cbc..44b3885e 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleControllerRestTest.java @@ -73,9 +73,9 @@ class RbacRoleControllerRestTest { // then .andExpect(status().isOk()) .andExpect(jsonPath("$", hasSize(3))) - .andExpect(jsonPath("$[0].roleName", is("global#global.admin"))) - .andExpect(jsonPath("$[1].roleName", is("test_customer#xxx.owner"))) - .andExpect(jsonPath("$[2].roleName", is("test_customer#xxx.admin"))) + .andExpect(jsonPath("$[0].roleName", is("global#global:ADMIN"))) + .andExpect(jsonPath("$[1].roleName", is("test_customer#xxx:OWNER"))) + .andExpect(jsonPath("$[2].roleName", is("test_customer#xxx:ADMIN"))) .andExpect(jsonPath("$[2].uuid", is(customerXxxAdmin.getUuid().toString()))) .andExpect(jsonPath("$[2].objectUuid", is(customerXxxAdmin.getObjectUuid().toString()))) .andExpect(jsonPath("$[2].objectTable", is(customerXxxAdmin.getObjectTable().toString()))) diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleRepositoryIntegrationTest.java index 197e0bc0..4d873fa6 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleRepositoryIntegrationTest.java @@ -39,19 +39,19 @@ class RbacRoleRepositoryIntegrationTest { private static final String[] ALL_TEST_DATA_ROLES = Array.of( // @formatter:off - "global#global.admin", - "test_customer#xxx.admin", "test_customer#xxx.owner", "test_customer#xxx.tenant", - "test_package#xxx00.admin", "test_package#xxx00.owner", "test_package#xxx00.tenant", - "test_package#xxx01.admin", "test_package#xxx01.owner", "test_package#xxx01.tenant", - "test_package#xxx02.admin", "test_package#xxx02.owner", "test_package#xxx02.tenant", - "test_customer#yyy.admin", "test_customer#yyy.owner", "test_customer#yyy.tenant", - "test_package#yyy00.admin", "test_package#yyy00.owner", "test_package#yyy00.tenant", - "test_package#yyy01.admin", "test_package#yyy01.owner", "test_package#yyy01.tenant", - "test_package#yyy02.admin", "test_package#yyy02.owner", "test_package#yyy02.tenant", - "test_customer#zzz.admin", "test_customer#zzz.owner", "test_customer#zzz.tenant", - "test_package#zzz00.admin", "test_package#zzz00.owner", "test_package#zzz00.tenant", - "test_package#zzz01.admin", "test_package#zzz01.owner", "test_package#zzz01.tenant", - "test_package#zzz02.admin", "test_package#zzz02.owner", "test_package#zzz02.tenant" + "global#global:ADMIN", + "test_customer#xxx:ADMIN", "test_customer#xxx:OWNER", "test_customer#xxx:TENANT", + "test_package#xxx00:ADMIN", "test_package#xxx00:OWNER", "test_package#xxx00:TENANT", + "test_package#xxx01:ADMIN", "test_package#xxx01:OWNER", "test_package#xxx01:TENANT", + "test_package#xxx02:ADMIN", "test_package#xxx02:OWNER", "test_package#xxx02:TENANT", + "test_customer#yyy:ADMIN", "test_customer#yyy:OWNER", "test_customer#yyy:TENANT", + "test_package#yyy00:ADMIN", "test_package#yyy00:OWNER", "test_package#yyy00:TENANT", + "test_package#yyy01:ADMIN", "test_package#yyy01:OWNER", "test_package#yyy01:TENANT", + "test_package#yyy02:ADMIN", "test_package#yyy02:OWNER", "test_package#yyy02:TENANT", + "test_customer#zzz:ADMIN", "test_customer#zzz:OWNER", "test_customer#zzz:TENANT", + "test_package#zzz00:ADMIN", "test_package#zzz00:OWNER", "test_package#zzz00:TENANT", + "test_package#zzz01:ADMIN", "test_package#zzz01:OWNER", "test_package#zzz01:TENANT", + "test_package#zzz02:ADMIN", "test_package#zzz02:OWNER", "test_package#zzz02:TENANT" // @formatter:on ); @@ -70,7 +70,7 @@ class RbacRoleRepositoryIntegrationTest { @Test public void globalAdmin_withAssumedglobalAdminRole_canViewAllRbacRoles() { given: - context.define("superuser-alex@hostsharing.net", "global#global.admin"); + context.define("superuser-alex@hostsharing.net", "global#global:ADMIN"); // when final var result = rbacRoleRepository.findAll(); @@ -91,49 +91,49 @@ class RbacRoleRepositoryIntegrationTest { allTheseRbacRolesAreReturned( result, // @formatter:off - "test_customer#xxx.admin", - "test_customer#xxx.tenant", - "test_package#xxx00.admin", - "test_package#xxx00.owner", - "test_package#xxx00.tenant", - "test_package#xxx01.admin", - "test_package#xxx01.owner", - "test_package#xxx01.tenant", + "test_customer#xxx:ADMIN", + "test_customer#xxx:TENANT", + "test_package#xxx00:ADMIN", + "test_package#xxx00:OWNER", + "test_package#xxx00:TENANT", + "test_package#xxx01:ADMIN", + "test_package#xxx01:OWNER", + "test_package#xxx01:TENANT", // ... - "test_domain#xxx00-aaaa.admin", - "test_domain#xxx00-aaaa.owner", + "test_domain#xxx00-aaaa:ADMIN", + "test_domain#xxx00-aaaa:OWNER", // .. - "test_domain#xxx01-aaab.admin", - "test_domain#xxx01-aaab.owner" + "test_domain#xxx01-aaab:ADMIN", + "test_domain#xxx01-aaab:OWNER" // @formatter:on ); noneOfTheseRbacRolesIsReturned( result, // @formatter:off - "global#global.admin", - "test_customer#xxx.owner", - "test_package#yyy00.admin", - "test_package#yyy00.owner", - "test_package#yyy00.tenant" + "global#global:ADMIN", + "test_customer#xxx:OWNER", + "test_package#yyy00:ADMIN", + "test_package#yyy00:OWNER", + "test_package#yyy00:TENANT" // @formatter:on ); } @Test public void customerAdmin_withAssumedOwnedPackageAdminRole_canViewOnlyItsOwnRbacRole() { - context.define("customer-admin@xxx.example.com", "test_package#xxx00.admin"); + context.define("customer-admin@xxx.example.com", "test_package#xxx00:ADMIN"); final var result = rbacRoleRepository.findAll(); exactlyTheseRbacRolesAreReturned( result, - "test_customer#xxx.tenant", - "test_package#xxx00.admin", - "test_package#xxx00.tenant", - "test_domain#xxx00-aaaa.admin", - "test_domain#xxx00-aaaa.owner", - "test_domain#xxx00-aaab.admin", - "test_domain#xxx00-aaab.owner"); + "test_customer#xxx:TENANT", + "test_package#xxx00:ADMIN", + "test_package#xxx00:TENANT", + "test_domain#xxx00-aaaa:ADMIN", + "test_domain#xxx00-aaaa:OWNER", + "test_domain#xxx00-aaab:ADMIN", + "test_domain#xxx00-aaab:OWNER"); } @Test @@ -157,19 +157,19 @@ class RbacRoleRepositoryIntegrationTest { void customerAdmin_withoutAssumedRole_canFindItsOwnRolesByName() { context.define("customer-admin@xxx.example.com"); - final var result = rbacRoleRepository.findByRoleName("test_customer#xxx.admin"); + final var result = rbacRoleRepository.findByRoleName("test_customer#xxx:ADMIN"); assertThat(result).isNotNull(); assertThat(result.getObjectTable()).isEqualTo("test_customer"); assertThat(result.getObjectIdName()).isEqualTo("xxx"); - assertThat(result.getRoleType()).isEqualTo(RbacRoleType.admin); + assertThat(result.getRoleType()).isEqualTo(RbacRoleType.ADMIN); } @Test void customerAdmin_withoutAssumedRole_canNotFindAlienRolesByName() { context.define("customer-admin@xxx.example.com"); - final var result = rbacRoleRepository.findByRoleName("test_customer#bbb.admin"); + final var result = rbacRoleRepository.findByRoleName("test_customer#bbb:ADMIN"); assertThat(result).isNull(); } diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/TestRbacRole.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/TestRbacRole.java index 652679f3..73e30a1b 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/TestRbacRole.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/TestRbacRole.java @@ -4,11 +4,11 @@ import static java.util.UUID.randomUUID; public class TestRbacRole { - public static final RbacRoleEntity hostmasterRole = rbacRole("global", "global", RbacRoleType.admin); - static final RbacRoleEntity customerXxxOwner = rbacRole("test_customer", "xxx", RbacRoleType.owner); - static final RbacRoleEntity customerXxxAdmin = rbacRole("test_customer", "xxx", RbacRoleType.admin); + public static final RbacRoleEntity hostmasterRole = rbacRole("global", "global", RbacRoleType.ADMIN); + static final RbacRoleEntity customerXxxOwner = rbacRole("test_customer", "xxx", RbacRoleType.OWNER); + static final RbacRoleEntity customerXxxAdmin = rbacRole("test_customer", "xxx", RbacRoleType.ADMIN); static public RbacRoleEntity rbacRole(final String objectTable, final String objectIdName, final RbacRoleType roleType) { - return new RbacRoleEntity(randomUUID(), randomUUID(), objectTable, objectIdName, roleType, objectTable+'#'+objectIdName+'.'+roleType); + return new RbacRoleEntity(randomUUID(), randomUUID(), objectTable, objectIdName, roleType, objectTable+'#'+objectIdName+':'+roleType); } } diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerAcceptanceTest.java index b13bcb76..6faa28ff 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerAcceptanceTest.java @@ -104,7 +104,7 @@ class RbacUserControllerAcceptanceTest { RestAssured .given() .header("current-user", "superuser-alex@hostsharing.net") - .header("assumed-roles", "test_customer#yyy.admin") + .header("assumed-roles", "test_customer#yyy:ADMIN") .port(port) .when() .get("http://localhost/api/rbac/users/" + givenUser.getUuid()) @@ -210,7 +210,7 @@ class RbacUserControllerAcceptanceTest { RestAssured .given() .header("current-user", "superuser-alex@hostsharing.net") - .header("assumed-roles", "test_customer#yyy.admin") + .header("assumed-roles", "test_customer#yyy:ADMIN") .port(port) .when() .get("http://localhost/api/rbac/users") @@ -287,20 +287,16 @@ class RbacUserControllerAcceptanceTest { .contentType("application/json") .body("", hasItem( allOf( - hasEntry("roleName", "test_customer#yyy.tenant"), - hasEntry("op", "view")) + hasEntry("roleName", "test_customer#yyy:TENANT"), + hasEntry("op", "SELECT")) )) .body("", hasItem( allOf( - hasEntry("roleName", "test_package#yyy00.admin"), - hasEntry("op", "add-domain")) + hasEntry("roleName", "test_domain#yyy00-aaaa:OWNER"), + hasEntry("op", "DELETE")) )) - .body("", hasItem( - allOf( - hasEntry("roleName", "test_domain#yyy00-aaaa.owner"), - hasEntry("op", "*")) - )) - .body("size()", is(7)); + // actual content tested in integration test, so this is enough for here: + .body("size()", greaterThanOrEqualTo(6)); // @formatter:on } @@ -313,7 +309,7 @@ class RbacUserControllerAcceptanceTest { RestAssured .given() .header("current-user", "superuser-alex@hostsharing.net") - .header("assumed-roles", "test_package#yyy00.admin") + .header("assumed-roles", "test_customer#yyy:ADMIN") .port(port) .when() .get("http://localhost/api/rbac/users/" + givenUser.getUuid() + "/permissions") @@ -322,20 +318,16 @@ class RbacUserControllerAcceptanceTest { .contentType("application/json") .body("", hasItem( allOf( - hasEntry("roleName", "test_customer#yyy.tenant"), - hasEntry("op", "view")) + hasEntry("roleName", "test_customer#yyy:TENANT"), + hasEntry("op", "SELECT")) )) .body("", hasItem( allOf( - hasEntry("roleName", "test_package#yyy00.admin"), - hasEntry("op", "add-domain")) + hasEntry("roleName", "test_domain#yyy00-aaaa:OWNER"), + hasEntry("op", "DELETE")) )) - .body("", hasItem( - allOf( - hasEntry("roleName", "test_domain#yyy00-aaaa.owner"), - hasEntry("op", "*")) - )) - .body("size()", is(7)); + // actual content tested in integration test, so this is enough for here: + .body("size()", greaterThanOrEqualTo(6)); // @formatter:on } @@ -356,20 +348,16 @@ class RbacUserControllerAcceptanceTest { .contentType("application/json") .body("", hasItem( allOf( - hasEntry("roleName", "test_customer#yyy.tenant"), - hasEntry("op", "view")) + hasEntry("roleName", "test_customer#yyy:TENANT"), + hasEntry("op", "SELECT")) )) .body("", hasItem( allOf( - hasEntry("roleName", "test_package#yyy00.admin"), - hasEntry("op", "add-domain")) + hasEntry("roleName", "test_domain#yyy00-aaaa:OWNER"), + hasEntry("op", "DELETE")) )) - .body("", hasItem( - allOf( - hasEntry("roleName", "test_domain#yyy00-aaaa.owner"), - hasEntry("op", "*")) - )) - .body("size()", is(7)); + // actual content tested in integration test, so this is enough for here: + .body("size()", greaterThanOrEqualTo(6)); // @formatter:on } diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java index ea0a3109..43c8bff1 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java @@ -20,6 +20,7 @@ import jakarta.servlet.http.HttpServletRequest; import java.util.List; import java.util.UUID; +import static java.util.Comparator.comparing; import static net.hostsharing.test.JpaAttempt.attempt; import static org.assertj.core.api.Assertions.assertThat; @@ -115,7 +116,7 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { @Test public void globalAdmin_withAssumedglobalAdminRole_canViewAllRbacUsers() { given: - context("superuser-alex@hostsharing.net", "global#global.admin"); + context("superuser-alex@hostsharing.net", "global#global:ADMIN"); // when final var result = rbacUserRepository.findByOptionalNameLike(null); @@ -127,7 +128,7 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { @Test public void globalAdmin_withAssumedCustomerAdminRole_canViewOnlyUsersHavingRolesInThatCustomersRealm() { given: - context("superuser-alex@hostsharing.net", "test_customer#xxx.admin"); + context("superuser-alex@hostsharing.net", "test_customer#xxx:ADMIN"); // when final var result = rbacUserRepository.findByOptionalNameLike(null); @@ -158,7 +159,7 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { @Test public void customerAdmin_withAssumedOwnedPackageAdminRole_canViewOnlyUsersHavingRolesInThatPackage() { - context("customer-admin@xxx.example.com", "test_package#xxx00.admin"); + context("customer-admin@xxx.example.com", "test_package#xxx00:ADMIN"); final var result = rbacUserRepository.findByOptionalNameLike(null); @@ -181,50 +182,48 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { private static final String[] ALL_USER_PERMISSIONS = Array.of( // @formatter:off - "global#global.admin -> global#global: add-customer", + "test_customer#xxx:ADMIN -> test_customer#xxx: SELECT", + "test_customer#xxx:OWNER -> test_customer#xxx: DELETE", + "test_customer#xxx:TENANT -> test_customer#xxx: SELECT", + "test_customer#xxx:ADMIN -> test_customer#xxx: INSERT:test_package", + "test_package#xxx00:ADMIN -> test_package#xxx00: INSERT:test_domain", + "test_package#xxx00:ADMIN -> test_package#xxx00: INSERT:test_domain", + "test_package#xxx00:TENANT -> test_package#xxx00: SELECT", + "test_package#xxx01:ADMIN -> test_package#xxx01: INSERT:test_domain", + "test_package#xxx01:ADMIN -> test_package#xxx01: INSERT:test_domain", + "test_package#xxx01:TENANT -> test_package#xxx01: SELECT", + "test_package#xxx02:ADMIN -> test_package#xxx02: INSERT:test_domain", + "test_package#xxx02:ADMIN -> test_package#xxx02: INSERT:test_domain", + "test_package#xxx02:TENANT -> test_package#xxx02: SELECT", - "test_customer#xxx.admin -> test_customer#xxx: add-package", - "test_customer#xxx.admin -> test_customer#xxx: view", - "test_customer#xxx.owner -> test_customer#xxx: *", - "test_customer#xxx.tenant -> test_customer#xxx: view", - "test_package#xxx00.admin -> test_package#xxx00: add-domain", - "test_package#xxx00.admin -> test_package#xxx00: add-domain", - "test_package#xxx00.tenant -> test_package#xxx00: view", - "test_package#xxx01.admin -> test_package#xxx01: add-domain", - "test_package#xxx01.admin -> test_package#xxx01: add-domain", - "test_package#xxx01.tenant -> test_package#xxx01: view", - "test_package#xxx02.admin -> test_package#xxx02: add-domain", - "test_package#xxx02.admin -> test_package#xxx02: add-domain", - "test_package#xxx02.tenant -> test_package#xxx02: view", + "test_customer#yyy:ADMIN -> test_customer#yyy: SELECT", + "test_customer#yyy:OWNER -> test_customer#yyy: DELETE", + "test_customer#yyy:TENANT -> test_customer#yyy: SELECT", + "test_customer#yyy:ADMIN -> test_customer#yyy: INSERT:test_package", + "test_package#yyy00:ADMIN -> test_package#yyy00: INSERT:test_domain", + "test_package#yyy00:ADMIN -> test_package#yyy00: INSERT:test_domain", + "test_package#yyy00:TENANT -> test_package#yyy00: SELECT", + "test_package#yyy01:ADMIN -> test_package#yyy01: INSERT:test_domain", + "test_package#yyy01:ADMIN -> test_package#yyy01: INSERT:test_domain", + "test_package#yyy01:TENANT -> test_package#yyy01: SELECT", + "test_package#yyy02:ADMIN -> test_package#yyy02: INSERT:test_domain", + "test_package#yyy02:ADMIN -> test_package#yyy02: INSERT:test_domain", + "test_package#yyy02:TENANT -> test_package#yyy02: SELECT", - "test_customer#yyy.admin -> test_customer#yyy: add-package", - "test_customer#yyy.admin -> test_customer#yyy: view", - "test_customer#yyy.owner -> test_customer#yyy: *", - "test_customer#yyy.tenant -> test_customer#yyy: view", - "test_package#yyy00.admin -> test_package#yyy00: add-domain", - "test_package#yyy00.admin -> test_package#yyy00: add-domain", - "test_package#yyy00.tenant -> test_package#yyy00: view", - "test_package#yyy01.admin -> test_package#yyy01: add-domain", - "test_package#yyy01.admin -> test_package#yyy01: add-domain", - "test_package#yyy01.tenant -> test_package#yyy01: view", - "test_package#yyy02.admin -> test_package#yyy02: add-domain", - "test_package#yyy02.admin -> test_package#yyy02: add-domain", - "test_package#yyy02.tenant -> test_package#yyy02: view", - - "test_customer#zzz.admin -> test_customer#zzz: add-package", - "test_customer#zzz.admin -> test_customer#zzz: view", - "test_customer#zzz.owner -> test_customer#zzz: *", - "test_customer#zzz.tenant -> test_customer#zzz: view", - "test_package#zzz00.admin -> test_package#zzz00: add-domain", - "test_package#zzz00.admin -> test_package#zzz00: add-domain", - "test_package#zzz00.tenant -> test_package#zzz00: view", - "test_package#zzz01.admin -> test_package#zzz01: add-domain", - "test_package#zzz01.admin -> test_package#zzz01: add-domain", - "test_package#zzz01.tenant -> test_package#zzz01: view", - "test_package#zzz02.admin -> test_package#zzz02: add-domain", - "test_package#zzz02.admin -> test_package#zzz02: add-domain", - "test_package#zzz02.tenant -> test_package#zzz02: view" - // @formatter:on + "test_customer#zzz:ADMIN -> test_customer#zzz: SELECT", + "test_customer#zzz:OWNER -> test_customer#zzz: DELETE", + "test_customer#zzz:TENANT -> test_customer#zzz: SELECT", + "test_customer#zzz:ADMIN -> test_customer#zzz: INSERT:test_package", + "test_package#zzz00:ADMIN -> test_package#zzz00: INSERT:test_domain", + "test_package#zzz00:ADMIN -> test_package#zzz00: INSERT:test_domain", + "test_package#zzz00:TENANT -> test_package#zzz00: SELECT", + "test_package#zzz01:ADMIN -> test_package#zzz01: INSERT:test_domain", + "test_package#zzz01:ADMIN -> test_package#zzz01: INSERT:test_domain", + "test_package#zzz01:TENANT -> test_package#zzz01: SELECT", + "test_package#zzz02:ADMIN -> test_package#zzz02: INSERT:test_domain", + "test_package#zzz02:ADMIN -> test_package#zzz02: INSERT:test_domain", + "test_package#zzz02:TENANT -> test_package#zzz02: SELECT" + // @formatter:on ); @Test @@ -233,7 +232,9 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { context("superuser-alex@hostsharing.net"); // when - final var result = rbacUserRepository.findPermissionsOfUserByUuid(userUUID("superuser-alex@hostsharing.net")); + final var result = rbacUserRepository.findPermissionsOfUserByUuid(userUUID("superuser-fran@hostsharing.net")) + .stream().filter(p -> p.getObjectTable().contains("test_")) + .sorted(comparing(RbacUserPermission::toString)).toList(); // then allTheseRbacPermissionsAreReturned(result, ALL_USER_PERMISSIONS); @@ -251,32 +252,32 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { allTheseRbacPermissionsAreReturned( result, // @formatter:off - "test_customer#xxx.admin -> test_customer#xxx: add-package", - "test_customer#xxx.admin -> test_customer#xxx: view", - "test_customer#xxx.tenant -> test_customer#xxx: view", + "test_customer#xxx:ADMIN -> test_customer#xxx: INSERT:test_package", + "test_customer#xxx:ADMIN -> test_customer#xxx: SELECT", + "test_customer#xxx:TENANT -> test_customer#xxx: SELECT", - "test_package#xxx00.admin -> test_package#xxx00: add-domain", - "test_package#xxx00.admin -> test_package#xxx00: add-domain", - "test_package#xxx00.tenant -> test_package#xxx00: view", - "test_domain#xxx00-aaaa.owner -> test_domain#xxx00-aaaa: *", + "test_package#xxx00:ADMIN -> test_package#xxx00: INSERT:test_domain", + "test_package#xxx00:ADMIN -> test_package#xxx00: INSERT:test_domain", + "test_package#xxx00:TENANT -> test_package#xxx00: SELECT", + "test_domain#xxx00-aaaa:OWNER -> test_domain#xxx00-aaaa: DELETE", - "test_package#xxx01.admin -> test_package#xxx01: add-domain", - "test_package#xxx01.admin -> test_package#xxx01: add-domain", - "test_package#xxx01.tenant -> test_package#xxx01: view", - "test_domain#xxx01-aaaa.owner -> test_domain#xxx01-aaaa: *", + "test_package#xxx01:ADMIN -> test_package#xxx01: INSERT:test_domain", + "test_package#xxx01:ADMIN -> test_package#xxx01: INSERT:test_domain", + "test_package#xxx01:TENANT -> test_package#xxx01: SELECT", + "test_domain#xxx01-aaaa:OWNER -> test_domain#xxx01-aaaa: DELETE", - "test_package#xxx02.admin -> test_package#xxx02: add-domain", - "test_package#xxx02.admin -> test_package#xxx02: add-domain", - "test_package#xxx02.tenant -> test_package#xxx02: view", - "test_domain#xxx02-aaaa.owner -> test_domain#xxx02-aaaa: *" + "test_package#xxx02:ADMIN -> test_package#xxx02: INSERT:test_domain", + "test_package#xxx02:ADMIN -> test_package#xxx02: INSERT:test_domain", + "test_package#xxx02:TENANT -> test_package#xxx02: SELECT", + "test_domain#xxx02-aaaa:OWNER -> test_domain#xxx02-aaaa: DELETE" // @formatter:on ); noneOfTheseRbacPermissionsAreReturned( result, // @formatter:off - "test_customer#yyy.admin -> test_customer#yyy: add-package", - "test_customer#yyy.admin -> test_customer#yyy: view", - "test_customer#yyy.tenant -> test_customer#yyy: view" + "test_customer#yyy:ADMIN -> test_customer#yyy: INSERT:test_package", + "test_customer#yyy:ADMIN -> test_customer#yyy: SELECT", + "test_customer#yyy:TENANT -> test_customer#yyy: SELECT" // @formatter:on ); } @@ -311,26 +312,26 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { allTheseRbacPermissionsAreReturned( result, // @formatter:off - "test_customer#xxx.tenant -> test_customer#xxx: view", - // "test_customer#xxx.admin -> test_customer#xxx: view" - Not permissions through the customer admin! - "test_package#xxx00.admin -> test_package#xxx00: add-domain", - "test_package#xxx00.admin -> test_package#xxx00: add-domain", - "test_package#xxx00.tenant -> test_package#xxx00: view", - "test_domain#xxx00-aaaa.owner -> test_domain#xxx00-aaaa: *", - "test_domain#xxx00-aaab.owner -> test_domain#xxx00-aaab: *" + "test_customer#xxx:TENANT -> test_customer#xxx: SELECT", + // "test_customer#xxx:ADMIN -> test_customer#xxx: view" - Not permissions through the customer admin! + "test_package#xxx00:ADMIN -> test_package#xxx00: INSERT:test_domain", + "test_package#xxx00:ADMIN -> test_package#xxx00: INSERT:test_domain", + "test_package#xxx00:TENANT -> test_package#xxx00: SELECT", + "test_domain#xxx00-aaaa:OWNER -> test_domain#xxx00-aaaa: DELETE", + "test_domain#xxx00-aaab:OWNER -> test_domain#xxx00-aaab: DELETE" // @formatter:on ); noneOfTheseRbacPermissionsAreReturned( result, // @formatter:off - "test_customer#yyy.admin -> test_customer#yyy: add-package", - "test_customer#yyy.admin -> test_customer#yyy: view", - "test_customer#yyy.tenant -> test_customer#yyy: view", - "test_package#yyy00.admin -> test_package#yyy00: add-domain", - "test_package#yyy00.admin -> test_package#yyy00: add-domain", - "test_package#yyy00.tenant -> test_package#yyy00: view", - "test_domain#yyy00-aaaa.owner -> test_domain#yyy00-aaaa: *", - "test_domain#yyy00-aaab.owner -> test_domain#yyy00-aaab: *" + "test_customer#yyy:ADMIN -> test_customer#yyy: INSERT:test_package", + "test_customer#yyy:ADMIN -> test_customer#yyy: SELECT", + "test_customer#yyy:TENANT -> test_customer#yyy: SELECT", + "test_package#yyy00:ADMIN -> test_package#yyy00: INSERT:test_domain", + "test_package#yyy00:ADMIN -> test_package#yyy00: INSERT:test_domain", + "test_package#yyy00:TENANT -> test_package#yyy00: SELECT", + "test_domain#yyy00-aaaa:OWNER -> test_domain#yyy00-aaaa: DELETE", + "test_domain#yyy00-aaab:OWNER -> test_domain#yyy00-aaab: DELETE" // @formatter:on ); } @@ -359,27 +360,26 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { allTheseRbacPermissionsAreReturned( result, // @formatter:off - "test_customer#xxx.tenant -> test_customer#xxx: view", - // "test_customer#xxx.admin -> test_customer#xxx: view" - Not permissions through the customer admin! - "test_package#xxx00.admin -> test_package#xxx00: add-domain", - "test_package#xxx00.admin -> test_package#xxx00: add-domain", - "test_package#xxx00.tenant -> test_package#xxx00: view" + "test_customer#xxx:TENANT -> test_customer#xxx: SELECT", + // "test_customer#xxx:ADMIN -> test_customer#xxx: view" - Not permissions through the customer admin! + "test_package#xxx00:ADMIN -> test_package#xxx00: INSERT:test_domain", + "test_package#xxx00:TENANT -> test_package#xxx00: SELECT" // @formatter:on ); noneOfTheseRbacPermissionsAreReturned( result, // @formatter:off // no customer admin permissions - "test_customer#xxx.admin -> test_customer#xxx: add-package", + "test_customer#xxx:ADMIN -> test_customer#xxx: add-package", // no permissions on other customer's objects - "test_customer#yyy.admin -> test_customer#yyy: add-package", - "test_customer#yyy.admin -> test_customer#yyy: view", - "test_customer#yyy.tenant -> test_customer#yyy: view", - "test_package#yyy00.admin -> test_package#yyy00: add-domain", - "test_package#yyy00.admin -> test_package#yyy00: add-domain", - "test_package#yyy00.tenant -> test_package#yyy00: view", - "test_domain#yyy00-aaaa.owner -> test_domain#yyy00-aaaa: *", - "test_domain#yyy00-xxxb.owner -> test_domain#yyy00-xxxb: *" + "test_customer#yyy:ADMIN -> test_customer#yyy: add-package", + "test_customer#yyy:ADMIN -> test_customer#yyy: SELECT", + "test_customer#yyy:TENANT -> test_customer#yyy: SELECT", + "test_package#yyy00:ADMIN -> test_package#yyy00: INSERT:test_domain", + "test_package#yyy00:ADMIN -> test_package#yyy00: INSERT:test_domain", + "test_package#yyy00:TENANT -> test_package#yyy00: SELECT", + "test_domain#yyy00-aaaa:OWNER -> test_domain#yyy00-aaaa: DELETE", + "test_domain#yyy00-xxxb:OWNER -> test_domain#yyy00-xxxb: DELETE" // @formatter:on ); } @@ -432,7 +432,8 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { final List actualResult, final String... expectedRoleNames) { assertThat(actualResult) - .extracting(p -> p.getRoleName() + " -> " + p.getObjectTable() + "#" + p.getObjectIdName() + ": " + p.getOp()) + .extracting(p -> p.getRoleName() + " -> " + p.getObjectTable() + "#" + p.getObjectIdName() + ": " + p.getOp() + + (p.getOpTableName() != null ? (":"+p.getOpTableName()) : "" )) .contains(expectedRoleNames); } diff --git a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerControllerAcceptanceTest.java index 6c695caa..1d7bf4e5 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerControllerAcceptanceTest.java @@ -89,7 +89,7 @@ class TestCustomerControllerAcceptanceTest { RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") - .header("assumed-roles", "test_customer#yyy.admin") + .header("assumed-roles", "test_customer#yyy:ADMIN") .port(port) .when() .get("http://localhost/api/test/customers") @@ -148,7 +148,7 @@ class TestCustomerControllerAcceptanceTest { // finally, the new customer can be viewed by its own admin final var newUserUuid = UUID.fromString( location.substring(location.lastIndexOf('/') + 1)); - context.define("customer-admin@uuu.example.com"); + context.define("superuser-fran@hostsharing.net", "test_customer#uuu:ADMIN"); assertThat(testCustomerRepository.findByUuid(newUserUuid)) .hasValueSatisfying(c -> assertThat(c.getPrefix()).isEqualTo("uuu")); } @@ -159,7 +159,7 @@ class TestCustomerControllerAcceptanceTest { RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") - .header("assumed-roles", "test_customer#xxx.admin") + .header("assumed-roles", "test_customer#xxx:ADMIN") .contentType(ContentType.JSON) .body(""" { @@ -175,7 +175,7 @@ class TestCustomerControllerAcceptanceTest { .statusCode(403) .contentType(ContentType.JSON) .statusCode(403) - .body("message", containsString("add-customer not permitted for test_customer#xxx.admin")); + .body("message", containsString("insert into test_customer not allowed for current subjects {test_customer#xxx:ADMIN}")); // @formatter:on // finally, the new customer was not created @@ -204,7 +204,7 @@ class TestCustomerControllerAcceptanceTest { .statusCode(403) .contentType(ContentType.JSON) .statusCode(403) - .body("message", containsString("add-customer not permitted for customer-admin@yyy.example.com")); + .body("message", containsString("ERROR: [403] insert into test_customer not allowed for current subjects {customer-admin@yyy.example.com}")); // @formatter:on // finally, the new customer was not created diff --git a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityUnitTest.java new file mode 100644 index 00000000..962cef38 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityUnitTest.java @@ -0,0 +1,54 @@ +package net.hostsharing.hsadminng.test.cust; + +import net.hostsharing.hsadminng.rbac.rbacdef.RbacViewMermaidFlowchartGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class TestCustomerEntityUnitTest { + + @Test + void definesRbac() { + final var rbacFlowchart = new RbacViewMermaidFlowchartGenerator(TestCustomerEntity.rbac()).toString(); + assertThat(rbacFlowchart).isEqualTo(""" + %%{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/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java index ca535142..591ce0eb 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java @@ -10,8 +10,6 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceException; import jakarta.servlet.http.HttpServletRequest; import java.util.List; @@ -27,9 +25,6 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest { @Autowired TestCustomerRepository testCustomerRepository; - @PersistenceContext - EntityManager em; - @MockBean HttpServletRequest request; @@ -43,7 +38,6 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest { final var count = testCustomerRepository.count(); // when - final var result = attempt(em, () -> { final var newCustomer = new TestCustomerEntity( UUID.randomUUID(), "www", 90001, "customer-admin@www.example.com"); @@ -60,7 +54,7 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest { @Test public void globalAdmin_withAssumedCustomerRole_cannotCreateNewCustomer() { // given - context("superuser-alex@hostsharing.net", "test_customer#xxx.admin"); + context("superuser-alex@hostsharing.net", "test_customer#xxx:ADMIN"); // when final var result = attempt(em, () -> { @@ -72,7 +66,7 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest { // then result.assertExceptionWithRootCauseMessage( PersistenceException.class, - "add-customer not permitted for test_customer#xxx.admin"); + "ERROR: [403] insert into test_customer not allowed for current subjects {test_customer#xxx:ADMIN}"); } @Test @@ -90,7 +84,7 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest { // then result.assertExceptionWithRootCauseMessage( PersistenceException.class, - "add-customer not permitted for customer-admin@xxx.example.com"); + "ERROR: [403] insert into test_customer not allowed for current subjects {customer-admin@xxx.example.com}"); } @@ -116,15 +110,15 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest { } @Test - public void globalAdmin_withAssumedglobalAdminRole_canViewAllCustomers() { + public void globalAdmin_withAssumedCustomerOwnerRole_canViewExactlyThatCustomer() { given: - context("superuser-alex@hostsharing.net", "global#global.admin"); + context("superuser-alex@hostsharing.net", "test_customer#yyy:OWNER"); // when final var result = testCustomerRepository.findCustomerByOptionalPrefixLike(null); then: - allTheseCustomersAreReturned(result, "xxx", "yyy", "zzz"); + allTheseCustomersAreReturned(result, "yyy"); } @Test @@ -141,7 +135,9 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest { @Test public void customerAdmin_withAssumedOwnedPackageAdminRole_canViewOnlyItsOwnCustomer() { - context("customer-admin@xxx.example.com", "test_package#xxx00.admin"); + context("customer-admin@xxx.example.com"); + + context("customer-admin@xxx.example.com", "test_package#xxx00:ADMIN"); final var result = testCustomerRepository.findCustomerByOptionalPrefixLike(null); diff --git a/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageControllerAcceptanceTest.java index fd51ebf8..0e52cc40 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageControllerAcceptanceTest.java @@ -44,7 +44,7 @@ class TestPackageControllerAcceptanceTest { RestAssured .given() .header("current-user", "superuser-alex@hostsharing.net") - .header("assumed-roles", "test_customer#xxx.admin") + .header("assumed-roles", "test_customer#xxx:ADMIN") .port(port) .when() .get("http://localhost/api/test/packages") @@ -66,7 +66,7 @@ class TestPackageControllerAcceptanceTest { RestAssured .given() .header("current-user", "superuser-alex@hostsharing.net") - .header("assumed-roles", "test_customer#xxx.admin") + .header("assumed-roles", "test_customer#xxx:ADMIN") .port(port) .when() .get("http://localhost/api/test/packages?name=xxx01") @@ -95,7 +95,7 @@ class TestPackageControllerAcceptanceTest { RestAssured .given() .header("current-user", "superuser-alex@hostsharing.net") - .header("assumed-roles", "test_customer#xxx.admin") + .header("assumed-roles", "test_customer#xxx:ADMIN") .contentType(ContentType.JSON) .body(format(""" { @@ -126,7 +126,7 @@ class TestPackageControllerAcceptanceTest { RestAssured .given() .header("current-user", "superuser-alex@hostsharing.net") - .header("assumed-roles", "test_customer#xxx.admin") + .header("assumed-roles", "test_customer#xxx:ADMIN") .contentType(ContentType.JSON) .body(""" { @@ -156,7 +156,7 @@ class TestPackageControllerAcceptanceTest { RestAssured .given() .header("current-user", "superuser-alex@hostsharing.net") - .header("assumed-roles", "test_customer#xxx.admin") + .header("assumed-roles", "test_customer#xxx:ADMIN") .contentType(ContentType.JSON) .body("{}") .port(port) @@ -176,7 +176,7 @@ class TestPackageControllerAcceptanceTest { return UUID.fromString(RestAssured .given() .header("current-user", "superuser-alex@hostsharing.net") - .header("assumed-roles", "test_customer#xxx.admin") + .header("assumed-roles", "test_customer#xxx:ADMIN") .port(port) .when() .get("http://localhost/api/test/packages?name={packageName}", packageName) @@ -188,7 +188,7 @@ class TestPackageControllerAcceptanceTest { } String getDescriptionOfPackage(final String packageName) { - context.define("superuser-alex@hostsharing.net","test_customer#xxx.admin"); + context.define("superuser-alex@hostsharing.net","test_customer#xxx:ADMIN"); return testPackageRepository.findAllByOptionalNameLike(packageName).get(0).getDescription(); } } diff --git a/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityUnitTest.java new file mode 100644 index 00000000..79dcfec2 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityUnitTest.java @@ -0,0 +1,68 @@ +package net.hostsharing.hsadminng.test.pac; + +import net.hostsharing.hsadminng.rbac.rbacdef.RbacViewMermaidFlowchartGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class TestPackageEntityUnitTest { + + @Test + void definesRbac() { + final var rbacFlowchart = new RbacViewMermaidFlowchartGenerator(TestPackageEntity.rbac()).toString(); + assertThat(rbacFlowchart).isEqualTo(""" + %%{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/test/java/net/hostsharing/hsadminng/test/pac/TestPackageRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageRepositoryIntegrationTest.java index 53d28e0c..49412b3b 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageRepositoryIntegrationTest.java @@ -1,6 +1,7 @@ package net.hostsharing.hsadminng.test.pac; import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.hsadminng.context.ContextBasedTest; import net.hostsharing.test.JpaAttempt; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -19,10 +20,7 @@ import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest @Import( { Context.class, JpaAttempt.class }) -class TestPackageRepositoryIntegrationTest { - - @Autowired - Context context; +class TestPackageRepositoryIntegrationTest extends ContextBasedTest { @Autowired TestPackageRepository testPackageRepository; @@ -40,9 +38,10 @@ class TestPackageRepositoryIntegrationTest { class FindAllByOptionalNameLike { @Test - public void globalAdmin_withoutAssumedRole_canNotViewAnyPackages_becauseThoseGrantsAreNotassumedd() { + public void globalAdmin_withoutAssumedRole_canNotViewAnyPackages_becauseThoseGrantsAreNotAssumed() { // given - context.define("superuser-alex@hostsharing.net"); + // alex is not just global-admin but lso the creating user, thus we use fran + context.define("superuser-fran@hostsharing.net"); // when final var result = testPackageRepository.findAllByOptionalNameLike(null); @@ -52,9 +51,9 @@ class TestPackageRepositoryIntegrationTest { } @Test - public void globalAdmin_withAssumedglobalAdminRole__canNotViewAnyPackages_becauseThoseGrantsAreNotassumedd() { + public void globalAdmin_withAssumedglobalAdminRole__canNotViewAnyPackages_becauseThoseGrantsAreNotAssumed() { given: - context.define("superuser-alex@hostsharing.net", "global#global.admin"); + context.define("superuser-alex@hostsharing.net", "global#global:ADMIN"); // when final var result = testPackageRepository.findAllByOptionalNameLike(null); @@ -77,7 +76,7 @@ class TestPackageRepositoryIntegrationTest { @Test public void customerAdmin_withAssumedOwnedPackageAdminRole_canViewOnlyItsOwnPackages() { - context.define("customer-admin@xxx.example.com", "test_package#xxx00.admin"); + context.define("customer-admin@xxx.example.com", "test_package#xxx00:ADMIN"); final var result = testPackageRepository.findAllByOptionalNameLike(null); @@ -89,19 +88,19 @@ class TestPackageRepositoryIntegrationTest { class OptimisticLocking { @Test - public void supportsOptimisticLocking() throws InterruptedException { + public void supportsOptimisticLocking() { // given - globalAdminWithAssumedRole("test_package#xxx00.admin"); + globalAdminWithAssumedRole("test_package#xxx00:ADMIN"); final var pac = testPackageRepository.findAllByOptionalNameLike("%").get(0); // when final var result1 = jpaAttempt.transacted(() -> { - globalAdminWithAssumedRole("test_package#xxx00.owner"); + globalAdminWithAssumedRole("test_package#xxx00:OWNER"); pac.setDescription("description set by thread 1"); testPackageRepository.save(pac); }); final var result2 = jpaAttempt.transacted(() -> { - globalAdminWithAssumedRole("test_package#xxx00.owner"); + globalAdminWithAssumedRole("test_package#xxx00:OWNER"); pac.setDescription("description set by thread 2"); testPackageRepository.save(pac); sleep(1500); diff --git a/src/test/java/net/hostsharing/test/JpaAttempt.java b/src/test/java/net/hostsharing/test/JpaAttempt.java index 3d5c50ee..d0ddd040 100644 --- a/src/test/java/net/hostsharing/test/JpaAttempt.java +++ b/src/test/java/net/hostsharing/test/JpaAttempt.java @@ -130,7 +130,6 @@ public class JpaAttempt { final Class expectedExceptionClass, final String... expectedRootCauseMessages) { assertThat(wasSuccessful()).as("wasSuccessful").isFalse(); - // TODO: also check the expected exception class itself final String firstRootCauseMessageLine = firstRootCauseMessageLineOf(caughtException(expectedExceptionClass)); for (String expectedRootCauseMessage : expectedRootCauseMessages) { assertThat(firstRootCauseMessageLine).contains(expectedRootCauseMessage); @@ -154,6 +153,11 @@ public class JpaAttempt { return this; } + public JpaResult assertNotNull() { + assertThat(returnedValue()).isNotNull(); + return this; + } + private String firstRootCauseMessageLineOf(final RuntimeException exception) { final var rootCause = NestedExceptionUtils.getRootCause(exception); return Optional.ofNullable(rootCause) diff --git a/src/test/java/net/hostsharing/test/PatchUnitTestBase.java b/src/test/java/net/hostsharing/test/PatchUnitTestBase.java index ce7ff865..56f97938 100644 --- a/src/test/java/net/hostsharing/test/PatchUnitTestBase.java +++ b/src/test/java/net/hostsharing/test/PatchUnitTestBase.java @@ -1,6 +1,6 @@ package net.hostsharing.test; -import net.hostsharing.hsadminng.persistence.HasUuid; +import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.mapper.EntityPatcher; import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Test; @@ -233,7 +233,7 @@ public abstract class PatchUnitTestBase { } } - protected static class JsonNullableProperty extends Property { + protected static class JsonNullableProperty extends Property { private final BiConsumer> resourceSetter; public final RV givenPatchValue; diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index a4f570f9..40ae85bb 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -4,8 +4,9 @@ spring: platform: postgres datasource: - url: jdbc:tc:postgresql:15.5-bookworm:///spring_boot_testcontainers + url-tc: jdbc:tc:postgresql:15.5-bookworm:///spring_boot_testcontainers url-local: jdbc:postgresql://localhost:5432/postgres + url: ${spring.datasource.url-tc} username: postgres password: password diff --git a/tools/generate b/tools/generate deleted file mode 100755 index 93aa5c7c..00000000 --- a/tools/generate +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash - -sourceLower=partner -targetLower=relationship - -sourceStudly=Partner -targetStudly=Relationship - -## for source in `find src -iname ""*$sourceLower*"" -type f \( -iname \*.yaml -o -iname \*.sql -o -iname \*.java \)`; do -for source in `find src -iname ""*$sourceLower*"" -type f \( -iname \*.yaml \)`; do - target=`echo $source | sed -e "s/$sourceStudly/$targetStudly/g" -e "s/$sourceLower/$targetLower/g"` - echo "Generating $target from $source:" - - mkdir -p `dirname $target` - - sed -e 's/hs-office-partner/hs-office-relationship/g' \ - -e 's/hs_office_partner/hs_office_relationship/g' \ - -e 's/HsOfficePartner/HsOfficeRelationship/g' \ - -e 's/hsOfficePartner/hsOfficeRelationship/g' \ - -e 's/partner/relationship/g' \ - \ - -e 's/addPartner/addRelationship/g' \ - -e 's/listPartners/listRelationships/g' \ - -e 's/getPartnerByUuid/getRelationshipByUuid/g' \ - -e 's/patchPartner/patchRelationship/g' \ - -e 's/person/relHolder/g' \ - -e 's/registrationOffice/relType/g' \ - <$source >$target - -done - -exit - -cat >>src/main/resources/db/changelog/db.changelog-master.yaml <