Compare commits
14 Commits
cc7b8fcf9b
...
ca952ce748
Author | SHA1 | Date | |
---|---|---|---|
|
ca952ce748 | ||
|
4438e7abd5 | ||
73c378b456 | |||
ad04faa21d | |||
277369a960 | |||
87af20a3a1 | |||
7f418c12a1 | |||
f8fb273918 | |||
d3ca2b7e23 | |||
4572c6bda0 | |||
67c1b50239 | |||
3faf2ea99e | |||
907e27ec19 | |||
187c0db8e2 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,7 +4,6 @@
|
|||||||
/build/www/**
|
/build/www/**
|
||||||
/src/test/javascript/coverage/
|
/src/test/javascript/coverage/
|
||||||
/worktrees/
|
/worktrees/
|
||||||
TODO-progress.png
|
|
||||||
|
|
||||||
######################
|
######################
|
||||||
# Node
|
# Node
|
||||||
|
11
README.md
11
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:
|
# the following command should return a JSON array with just all packages visible for the admin of the customer yyy:
|
||||||
curl \
|
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
|
http://localhost:8080/api/test/packages
|
||||||
|
|
||||||
# add a new customer
|
# add a new customer
|
||||||
@ -380,12 +380,6 @@ You can explore the prototype as follows:
|
|||||||
`src/`
|
`src/`
|
||||||
The actual source-code, see [Source Code Package Structure](#source-code-package-structure) for details.
|
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/`
|
`tools/`
|
||||||
Some shell-scripts to useful tasks.
|
Some shell-scripts to useful tasks.
|
||||||
|
|
||||||
@ -765,5 +759,4 @@ The output will list the generated files.
|
|||||||
## Further Documentation
|
## Further Documentation
|
||||||
|
|
||||||
- the `doc` directory contains architecture concepts and a glossary
|
- the `doc` directory contains architecture concepts and a glossary
|
||||||
- TODO.md tracks requirements and progress for the contract of the initial project,
|
- the `ideas` directory contains unstructured ideas for future development or documentation
|
||||||
please do not amend anything in this document
|
|
||||||
|
34
build.gradle
34
build.gradle
@ -1,15 +1,15 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'java'
|
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.spring.dependency-management' version '1.1.4'
|
||||||
id 'io.openapiprocessor.openapi-processor' version '2023.2'
|
id 'io.openapiprocessor.openapi-processor' version '2023.2'
|
||||||
id 'com.github.jk1.dependency-license-report' version '2.5'
|
id 'com.github.jk1.dependency-license-report' version '2.6'
|
||||||
id "org.owasp.dependencycheck" version "9.0.7"
|
id "org.owasp.dependencycheck" version "9.0.10"
|
||||||
id "com.diffplug.spotless" version "6.23.3"
|
id "com.diffplug.spotless" version "6.25.0"
|
||||||
id 'jacoco'
|
id 'jacoco'
|
||||||
id 'info.solidsoft.pitest' version '1.15.0'
|
id 'info.solidsoft.pitest' version '1.15.0'
|
||||||
id 'se.patrikerdes.use-latest-versions' version '0.2.18'
|
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'
|
group = 'net.hostsharing'
|
||||||
@ -59,28 +59,16 @@ dependencies {
|
|||||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||||
implementation 'com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.1'
|
implementation 'com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.1'
|
||||||
implementation 'org.springdoc:springdoc-openapi:2.3.0'
|
implementation 'org.springdoc:springdoc-openapi:2.4.0'
|
||||||
implementation 'org.postgresql:postgresql:42.7.1'
|
implementation 'org.postgresql:postgresql:42.7.3'
|
||||||
implementation 'org.liquibase:liquibase-core:4.25.1'
|
implementation 'org.liquibase:liquibase-core:4.27.0'
|
||||||
implementation 'com.vladmihalcea:hibernate-types-60:2.21.1'
|
implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.7.3'
|
||||||
implementation 'io.hypersistence:hypersistence-utils-hibernate-62:3.7.0'
|
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.0'
|
||||||
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.16.1'
|
|
||||||
implementation 'org.openapitools:jackson-databind-nullable:0.2.6'
|
implementation 'org.openapitools:jackson-databind-nullable:0.2.6'
|
||||||
implementation 'org.apache.commons:commons-text:1.11.0'
|
implementation 'org.apache.commons:commons-text:1.11.0'
|
||||||
implementation 'org.modelmapper:modelmapper:3.2.0'
|
implementation 'org.modelmapper:modelmapper:3.2.0'
|
||||||
implementation 'org.iban4j:iban4j:3.2.7-RELEASE'
|
implementation 'org.iban4j:iban4j:3.2.7-RELEASE'
|
||||||
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
|
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.4.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')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOnly 'org.projectlombok:lombok'
|
compileOnly 'org.projectlombok:lombok'
|
||||||
testCompileOnly 'org.projectlombok:lombok'
|
testCompileOnly 'org.projectlombok:lombok'
|
||||||
|
@ -10,7 +10,7 @@ classDiagram
|
|||||||
|
|
||||||
namespace Partner {
|
namespace Partner {
|
||||||
class partner-MeierGmbH
|
class partner-MeierGmbH
|
||||||
class role-MeierGmbH
|
class rel-MeierGmbH
|
||||||
class personDetails-MeierGmbH
|
class personDetails-MeierGmbH
|
||||||
class contactData-MeierGmbH
|
class contactData-MeierGmbH
|
||||||
class person-MeierGmbH
|
class person-MeierGmbH
|
||||||
@ -19,28 +19,29 @@ classDiagram
|
|||||||
namespace Representatives {
|
namespace Representatives {
|
||||||
class person-FrankMeier
|
class person-FrankMeier
|
||||||
class contactData-FrankMeier
|
class contactData-FrankMeier
|
||||||
class role-MeierGmbH-FrankMeier
|
class rel-MeierGmbH-FrankMeier
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Debitors {
|
namespace Debitors {
|
||||||
class debitor-MeierGmbH
|
class debitor-MeierGmbH
|
||||||
class contactData-MeierGmbH-Buha
|
class contactData-MeierGmbH-Buha
|
||||||
class role-MeierGmbH-Buha
|
class rel-MeierGmbH-Buha
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Operations {
|
namespace Operations {
|
||||||
class person-SabineMeier
|
class person-SabineMeier
|
||||||
class contactData-SabineMeier
|
class contactData-SabineMeier
|
||||||
class role-MeierGmbH-SabineMeier
|
class rel-MeierGmbH-SabineMeier
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Enums {
|
namespace Enums {
|
||||||
|
|
||||||
class RoleType {
|
class RelationType {
|
||||||
<<enumeration>>
|
<<enumeration>>
|
||||||
UNKNOWN
|
UNKNOWN
|
||||||
|
PARTNER
|
||||||
|
DEBITOR
|
||||||
REPRESENTATIVE
|
REPRESENTATIVE
|
||||||
ACCOUNTING
|
|
||||||
OPERATIONS
|
OPERATIONS
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,9 +65,9 @@ classDiagram
|
|||||||
|
|
||||||
class partner-MeierGmbH {
|
class partner-MeierGmbH {
|
||||||
+Numeric partnerNumber: 12345
|
+Numeric partnerNumber: 12345
|
||||||
+Role partnerRole
|
+Relation partnerRel
|
||||||
}
|
}
|
||||||
partner-MeierGmbH *-- role-MeierGmbH
|
partner-MeierGmbH *-- rel-MeierGmbH
|
||||||
|
|
||||||
class person-MeierGmbH {
|
class person-MeierGmbH {
|
||||||
+personType: LEGAL
|
+personType: LEGAL
|
||||||
@ -90,22 +91,22 @@ classDiagram
|
|||||||
+emailAddresses: office@meier-gmbh.de
|
+emailAddresses: office@meier-gmbh.de
|
||||||
}
|
}
|
||||||
|
|
||||||
class role-MeierGmbH {
|
class rel-MeierGmbH {
|
||||||
+RoleType RoleType PARTNER
|
+RelationType type PARTNER
|
||||||
+Person anchor
|
+Person anchor
|
||||||
+Person holder
|
+Person holder
|
||||||
+Contact roleContact
|
+Contact contact
|
||||||
}
|
}
|
||||||
role-MeierGmbH o-- person-HostsharingEG : anchor
|
rel-MeierGmbH o-- person-HostsharingEG : anchor
|
||||||
role-MeierGmbH o-- person-MeierGmbH : holder
|
rel-MeierGmbH o-- person-MeierGmbH : holder
|
||||||
role-MeierGmbH o-- contactData-MeierGmbH
|
rel-MeierGmbH o-- contactData-MeierGmbH
|
||||||
|
|
||||||
%% --- Debitors ---
|
%% --- Debitors ---
|
||||||
|
|
||||||
class debitor-MeierGmbH {
|
class debitor-MeierGmbH {
|
||||||
+Partner partner
|
+Partner partner
|
||||||
+Numeric[2] debitorNumberSuffix: 00
|
+Numeric[2] debitorNumberSuffix: 00
|
||||||
+Role billingRole
|
+Relation debitorRel
|
||||||
+boolean billable: true
|
+boolean billable: true
|
||||||
+String vatId: ID123456789
|
+String vatId: ID123456789
|
||||||
+String vatCountryCode: DE
|
+String vatCountryCode: DE
|
||||||
@ -115,7 +116,7 @@ classDiagram
|
|||||||
+String defaultPrefix: mei
|
+String defaultPrefix: mei
|
||||||
}
|
}
|
||||||
debitor-MeierGmbH o-- partner-MeierGmbH
|
debitor-MeierGmbH o-- partner-MeierGmbH
|
||||||
debitor-MeierGmbH *-- role-MeierGmbH-Buha
|
debitor-MeierGmbH *-- rel-MeierGmbH-Buha
|
||||||
|
|
||||||
class contactData-MeierGmbH-Buha {
|
class contactData-MeierGmbH-Buha {
|
||||||
+postalAddress: Hauptstraße 5, 22345 Hamburg
|
+postalAddress: Hauptstraße 5, 22345 Hamburg
|
||||||
@ -123,15 +124,15 @@ classDiagram
|
|||||||
+emailAddresses: buha@meier-gmbh.de
|
+emailAddresses: buha@meier-gmbh.de
|
||||||
}
|
}
|
||||||
|
|
||||||
class role-MeierGmbH-Buha {
|
class rel-MeierGmbH-Buha {
|
||||||
+RoleType RoleType ACCOUNTING
|
+RelationType type DEBITOR
|
||||||
+Person anchor
|
+Person anchor
|
||||||
+Person holder
|
+Person holder
|
||||||
+Contact roleContact
|
+Contact contact
|
||||||
}
|
}
|
||||||
role-MeierGmbH-Buha o-- person-MeierGmbH : anchor
|
rel-MeierGmbH-Buha o-- person-MeierGmbH : anchor
|
||||||
role-MeierGmbH-Buha o-- person-MeierGmbH : holder
|
rel-MeierGmbH-Buha o-- person-MeierGmbH : holder
|
||||||
role-MeierGmbH-Buha o-- contactData-MeierGmbH-Buha
|
rel-MeierGmbH-Buha o-- contactData-MeierGmbH-Buha
|
||||||
|
|
||||||
%% --- Representatives ---
|
%% --- Representatives ---
|
||||||
|
|
||||||
@ -148,15 +149,15 @@ classDiagram
|
|||||||
+emailAddresses: frank.meier@meier-gmbh.de
|
+emailAddresses: frank.meier@meier-gmbh.de
|
||||||
}
|
}
|
||||||
|
|
||||||
class role-MeierGmbH-FrankMeier {
|
class rel-MeierGmbH-FrankMeier {
|
||||||
+RoleType RoleType REPRESENTATIVE
|
+RelationType type REPRESENTATIVE
|
||||||
+Person anchor
|
+Person anchor
|
||||||
+Person holder
|
+Person holder
|
||||||
+Contact roleContact
|
+Contact contact
|
||||||
}
|
}
|
||||||
role-MeierGmbH-FrankMeier o-- person-MeierGmbH : anchor
|
rel-MeierGmbH-FrankMeier o-- person-MeierGmbH : anchor
|
||||||
role-MeierGmbH-FrankMeier o-- person-FrankMeier : holder
|
rel-MeierGmbH-FrankMeier o-- person-FrankMeier : holder
|
||||||
role-MeierGmbH-FrankMeier o-- contactData-FrankMeier
|
rel-MeierGmbH-FrankMeier o-- contactData-FrankMeier
|
||||||
|
|
||||||
%% --- Operations ---
|
%% --- Operations ---
|
||||||
|
|
||||||
@ -173,14 +174,14 @@ classDiagram
|
|||||||
+emailAddresses: sabine.meier@meier-gmbh.de
|
+emailAddresses: sabine.meier@meier-gmbh.de
|
||||||
}
|
}
|
||||||
|
|
||||||
class role-MeierGmbH-SabineMeier {
|
class rel-MeierGmbH-SabineMeier {
|
||||||
+RoleType RoleType OPERATIONAL
|
+RelationType type OPERATIONAL
|
||||||
+Person anchor
|
+Person anchor
|
||||||
+Person holder
|
+Person holder
|
||||||
+Contact roleContact
|
+Contact contact
|
||||||
}
|
}
|
||||||
role-MeierGmbH-SabineMeier o-- person-MeierGmbH : anchor
|
rel-MeierGmbH-SabineMeier o-- person-MeierGmbH : anchor
|
||||||
role-MeierGmbH-SabineMeier o-- person-SabineMeier : holder
|
rel-MeierGmbH-SabineMeier o-- person-SabineMeier : holder
|
||||||
role-MeierGmbH-SabineMeier o-- contactData-SabineMeier
|
rel-MeierGmbH-SabineMeier o-- contactData-SabineMeier
|
||||||
|
|
||||||
```
|
```
|
||||||
|
82
doc/ideas/rbac-schema-f.md
Normal file
82
doc/ideas/rbac-schema-f.md
Normal file
@ -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
|
||||||
|
```
|
29
doc/ideas/simplified-grant-structure.md
Normal file
29
doc/ideas/simplified-grant-structure.md
Normal file
@ -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.
|
196
doc/rbac.md
196
doc/rbac.md
@ -1,6 +1,6 @@
|
|||||||
## *hsadmin-ng*'s Role-Based-Access-Management (RBAC)
|
## *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.
|
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.
|
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.
|
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.
|
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:
|
You can find the entity structure as a UML class diagram as follows:
|
||||||
|
|
||||||
@ -101,13 +101,12 @@ package RBAC {
|
|||||||
RbacPermission *-- RbacObject
|
RbacPermission *-- RbacObject
|
||||||
|
|
||||||
enum RbacOperation {
|
enum RbacOperation {
|
||||||
add-package
|
INSERT:package
|
||||||
add-domain
|
INSERT:domain
|
||||||
add-domain
|
|
||||||
...
|
...
|
||||||
view
|
SELECT
|
||||||
edit
|
UPDATE
|
||||||
delete
|
DELETE
|
||||||
}
|
}
|
||||||
|
|
||||||
entity RbacObject {
|
entity RbacObject {
|
||||||
@ -172,11 +171,10 @@ An *RbacPermission* allows a specific *RbacOperation* on a specific *RbacObject*
|
|||||||
An *RbacOperation* determines, <u>what</u> an *RbacPermission* allows to do.
|
An *RbacOperation* determines, <u>what</u> an *RbacPermission* allows to do.
|
||||||
It can be one of:
|
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"
|
- **'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'
|
||||||
- **'view'** - permits reading the contents of the object specified by the permission
|
- **'SELECT'** - permits selecting the row specified by the permission, is included in all other permissions
|
||||||
- **'edit'** - change the contents of the object specified by the permission
|
- **'UPDATE'** - permits updating (only the updatable columns of) the row specified by the permission, includes 'SELECT'
|
||||||
- **'delete'** - delete the object specified by the permission
|
- **'DELETE'** - permits deleting the row specified by the permission, includes 'SELECT'
|
||||||
- **'\*'**
|
|
||||||
|
|
||||||
This list is extensible according to the needs of the access rule system.
|
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'.
|
There can be global roles like 'administrators'.
|
||||||
Most roles, though, are specific for certain business-objects and automatically generated as such:
|
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'),
|
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')
|
*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
|
#### owner
|
||||||
|
|
||||||
The owner-role is granted to the subject which created the business object.
|
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.
|
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'.
|
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.
|
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.
|
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.
|
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).
|
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,
|
E.g. a package-admin is allowed to see the related debitor-business-object,
|
||||||
but not its banking data.
|
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.
|
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.
|
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.
|
If the referrer-role exists, the SELECT-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.
|
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
|
### 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.
|
But not in all cases role-depreciation takes place.
|
||||||
E.g. often a tenant-role is granted another tenant-role,
|
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.
|
The same for the agent-role, often it is granted another agent-role.
|
||||||
|
|
||||||
|
|
||||||
@ -297,14 +299,14 @@ package RbacRoles {
|
|||||||
RbacUsers -[hidden]> RbacRoles
|
RbacUsers -[hidden]> RbacRoles
|
||||||
|
|
||||||
package RbacPermissions {
|
package RbacPermissions {
|
||||||
object PermCustXyz_View
|
object PermCustXyz_SELECT
|
||||||
object PermCustXyz_Edit
|
object PermCustXyz_UPDATE
|
||||||
object PermCustXyz_Delete
|
object PermCustXyz_DELETE
|
||||||
object PermCustXyz_AddPackage
|
object PermCustXyz_INSERT:Package
|
||||||
object PermPackXyz00_View
|
object PermPackXyz00_SELECT
|
||||||
object PermPackXyz00_Edit
|
object PermPackXyz00_EDIT
|
||||||
object PermPackXyz00_Delete
|
object PermPackXyz00_DELETE
|
||||||
object PermPackXyz00_AddUser
|
object PermPackXyz00_INSERT:USER
|
||||||
}
|
}
|
||||||
RbacRoles -[hidden]> RbacPermissions
|
RbacRoles -[hidden]> RbacPermissions
|
||||||
|
|
||||||
@ -322,23 +324,23 @@ RoleAdministrators o..> RoleCustXyz_Owner
|
|||||||
RoleCustXyz_Owner o-> RoleCustXyz_Admin
|
RoleCustXyz_Owner o-> RoleCustXyz_Admin
|
||||||
RoleCustXyz_Admin o-> RolePackXyz00_Owner
|
RoleCustXyz_Admin o-> RolePackXyz00_Owner
|
||||||
|
|
||||||
RoleCustXyz_Owner o--> PermCustXyz_Edit
|
RoleCustXyz_Owner o--> PermCustXyz_UPDATE
|
||||||
RoleCustXyz_Owner o--> PermCustXyz_Delete
|
RoleCustXyz_Owner o--> PermCustXyz_DELETE
|
||||||
RoleCustXyz_Admin o--> PermCustXyz_View
|
RoleCustXyz_Admin o--> PermCustXyz_SELECT
|
||||||
RoleCustXyz_Admin o--> PermCustXyz_AddPackage
|
RoleCustXyz_Admin o--> PermCustXyz_INSERT:Package
|
||||||
RolePackXyz00_Owner o--> PermPackXyz00_View
|
RolePackXyz00_Owner o--> PermPackXyz00_SELECT
|
||||||
RolePackXyz00_Owner o--> PermPackXyz00_Edit
|
RolePackXyz00_Owner o--> PermPackXyz00_UPDATE
|
||||||
RolePackXyz00_Owner o--> PermPackXyz00_Delete
|
RolePackXyz00_Owner o--> PermPackXyz00_DELETE
|
||||||
RolePackXyz00_Owner o--> PermPackXyz00_AddUser
|
RolePackXyz00_Owner o--> PermPackXyz00_INSERT:User
|
||||||
|
|
||||||
PermCustXyz_View o--> CustXyz
|
PermCustXyz_SELECT o--> CustXyz
|
||||||
PermCustXyz_Edit o--> CustXyz
|
PermCustXyz_UPDATE o--> CustXyz
|
||||||
PermCustXyz_Delete o--> CustXyz
|
PermCustXyz_DELETE o--> CustXyz
|
||||||
PermCustXyz_AddPackage o--> CustXyz
|
PermCustXyz_INSERT:Package o--> CustXyz
|
||||||
PermPackXyz00_View o--> PackXyz00
|
PermPackXyz00_SELECT o--> PackXyz00
|
||||||
PermPackXyz00_Edit o--> PackXyz00
|
PermPackXyz00_UPDATE o--> PackXyz00
|
||||||
PermPackXyz00_Delete o--> PackXyz00
|
PermPackXyz00_DELETE o--> PackXyz00
|
||||||
PermPackXyz00_AddUser o--> PackXyz00
|
PermPackXyz00_INSERT:User o--> PackXyz00
|
||||||
|
|
||||||
@enduml
|
@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:
|
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 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 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 'add-..' right to the parent-business-object.
|
- 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.
|
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
|
### 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.
|
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:
|
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.
|
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.
|
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;
|
BEGIN TRANSACTION;
|
||||||
SET SESSION SESSION AUTHORIZATION restricted;
|
SET SESSION SESSION AUTHORIZATION restricted;
|
||||||
SET LOCAL hsadminng.currentUser = 'mike@hostsharing.net';
|
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"
|
SELECT c.prefix, p.name as "package", ema.localPart || '@' || dom.name as "email-address"
|
||||||
FROM emailaddress_rv ema
|
FROM emailaddress_rv ema
|
||||||
@ -458,26 +460,26 @@ allow_mixing
|
|||||||
entity "BObj customer#xyz" as boCustXyz
|
entity "BObj customer#xyz" as boCustXyz
|
||||||
|
|
||||||
together {
|
together {
|
||||||
entity "Perm customer#xyz *" as permCustomerXyzAll
|
entity "Perm customer#xyz *" as permCustomerXyzDELETE
|
||||||
permCustomerXyzAll --> boCustXyz
|
permCustomerXyzDELETE --> boCustXyz
|
||||||
|
|
||||||
entity "Perm customer#xyz add-package" as permCustomerXyzAddPack
|
entity "Perm customer#xyz INSERT:package" as permCustomerXyzINSERT:package
|
||||||
permCustomerXyzAddPack --> boCustXyz
|
permCustomerXyzINSERT:package --> boCustXyz
|
||||||
|
|
||||||
entity "Perm customer#xyz view" as permCustomerXyzView
|
entity "Perm customer#xyz SELECT" as permCustomerXyzSELECT
|
||||||
permCustomerXyzView --> boCustXyz
|
permCustomerXyzSELECT--> boCustXyz
|
||||||
}
|
}
|
||||||
|
|
||||||
entity "Role customer#xyz.tenant" as roleCustXyzTenant
|
entity "Role customer#xyz:TENANT" as roleCustXyzTenant
|
||||||
roleCustXyzTenant --> permCustomerXyzView
|
roleCustXyzTenant --> permCustomerXyzSELECT
|
||||||
|
|
||||||
entity "Role customer#xyz.admin" as roleCustXyzAdmin
|
entity "Role customer#xyz:ADMIN" as roleCustXyzAdmin
|
||||||
roleCustXyzAdmin --> roleCustXyzTenant
|
roleCustXyzAdmin --> roleCustXyzTenant
|
||||||
roleCustXyzAdmin --> permCustomerXyzAddPack
|
roleCustXyzAdmin --> permCustomerXyzINSERT:package
|
||||||
|
|
||||||
entity "Role customer#xyz.owner" as roleCustXyzOwner
|
entity "Role customer#xyz:OWNER" as roleCustXyzOwner
|
||||||
roleCustXyzOwner ..> roleCustXyzAdmin
|
roleCustXyzOwner ..> roleCustXyzAdmin
|
||||||
roleCustXyzOwner --> permCustomerXyzAll
|
roleCustXyzOwner --> permCustomerXyzDELETE
|
||||||
|
|
||||||
actor "Customer XYZ Admin" as actorCustXyzAdmin
|
actor "Customer XYZ Admin" as actorCustXyzAdmin
|
||||||
actorCustXyzAdmin --> roleCustXyzAdmin
|
actorCustXyzAdmin --> roleCustXyzAdmin
|
||||||
@ -487,13 +489,11 @@ roleAdmins --> roleCustXyzOwner
|
|||||||
|
|
||||||
actor "Any Hostmaster" as actorHostmaster
|
actor "Any Hostmaster" as actorHostmaster
|
||||||
actorHostmaster --> roleAdmins
|
actorHostmaster --> roleAdmins
|
||||||
|
|
||||||
|
|
||||||
@enduml
|
@enduml
|
||||||
```
|
```
|
||||||
|
|
||||||
As you can see, there something special:
|
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.
|
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.
|
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
|
entity "BObj package#xyz00" as boPacXyz00
|
||||||
|
|
||||||
together {
|
together {
|
||||||
entity "Perm package#xyz00 *" as permPackageXyzAll
|
entity "Perm package#xyz00 *" as permPackageXyzDELETE
|
||||||
permPackageXyzAll --> boPacXyz00
|
permPackageXyzDELETE --> boPacXyz00
|
||||||
|
|
||||||
entity "Perm package#xyz00 add-domain" as permPacXyz00AddUser
|
entity "Perm package#xyz00 INSERT:domain" as permPacXyz00INSERT:user
|
||||||
permPacXyz00AddUser --> boPacXyz00
|
permPacXyz00INSERT:user --> boPacXyz00
|
||||||
|
|
||||||
entity "Perm package#xyz00 edit" as permPacXyz00Edit
|
entity "Perm package#xyz00 UPDATE" as permPacXyz00UPDATE
|
||||||
permPacXyz00Edit --> boPacXyz00
|
permPacXyz00UPDATE --> boPacXyz00
|
||||||
|
|
||||||
entity "Perm package#xyz00 view" as permPacXyz00View
|
entity "Perm package#xyz00 SELECT" as permPacXyz00SELECT
|
||||||
permPacXyz00View --> boPacXyz00
|
permPacXyz00SELECT --> boPacXyz00
|
||||||
}
|
}
|
||||||
|
|
||||||
package {
|
package {
|
||||||
entity "Role customer#xyz.tenant" as roleCustXyzTenant
|
entity "Role customer#xyz:TENANT" as roleCustXyzTenant
|
||||||
entity "Role customer#xyz.admin" as roleCustXyzAdmin
|
entity "Role customer#xyz:ADMIN" as roleCustXyzAdmin
|
||||||
entity "Role customer#xyz.owner" as roleCustXyzOwner
|
entity "Role customer#xyz:OWNER" as roleCustXyzOwner
|
||||||
}
|
}
|
||||||
|
|
||||||
package {
|
package {
|
||||||
entity "Role package#xyz00.owner" as rolePacXyz00Owner
|
entity "Role package#xyz00:OWNER" as rolePacXyz00Owner
|
||||||
entity "Role package#xyz00.admin" as rolePacXyz00Admin
|
entity "Role package#xyz00:ADMIN" as rolePacXyz00Admin
|
||||||
entity "Role package#xyz00.tenant" as rolePacXyz00Tenant
|
entity "Role package#xyz00:TENANT" as rolePacXyz00Tenant
|
||||||
}
|
}
|
||||||
|
|
||||||
rolePacXyz00Tenant --> permPacXyz00View
|
rolePacXyz00Tenant --> permPacXyz00SELECT
|
||||||
rolePacXyz00Tenant --> roleCustXyzTenant
|
rolePacXyz00Tenant --> roleCustXyzTenant
|
||||||
|
|
||||||
rolePacXyz00Owner --> rolePacXyz00Admin
|
rolePacXyz00Owner --> rolePacXyz00Admin
|
||||||
rolePacXyz00Owner --> permPackageXyzAll
|
rolePacXyz00Owner --> permPackageXyzDELETE
|
||||||
|
|
||||||
roleCustXyzAdmin --> rolePacXyz00Owner
|
roleCustXyzAdmin --> rolePacXyz00Owner
|
||||||
roleCustXyzAdmin --> roleCustXyzTenant
|
roleCustXyzAdmin --> roleCustXyzTenant
|
||||||
@ -564,8 +564,8 @@ roleCustXyzAdmin --> roleCustXyzTenant
|
|||||||
roleCustXyzOwner ..> roleCustXyzAdmin
|
roleCustXyzOwner ..> roleCustXyzAdmin
|
||||||
|
|
||||||
rolePacXyz00Admin --> rolePacXyz00Tenant
|
rolePacXyz00Admin --> rolePacXyz00Tenant
|
||||||
rolePacXyz00Admin --> permPacXyz00AddUser
|
rolePacXyz00Admin --> permPacXyz00INSERT:user
|
||||||
rolePacXyz00Admin --> permPacXyz00Edit
|
rolePacXyz00Admin --> permPacXyz00UPDATE
|
||||||
|
|
||||||
actor "Package XYZ00 Admin" as actorPacXyzAdmin
|
actor "Package XYZ00 Admin" as actorPacXyzAdmin
|
||||||
actorPacXyzAdmin -l-> rolePacXyz00Admin
|
actorPacXyzAdmin -l-> rolePacXyz00Admin
|
||||||
@ -624,10 +624,10 @@ Let's have a look at the two view queries:
|
|||||||
WHERE target.uuid IN (
|
WHERE target.uuid IN (
|
||||||
SELECT uuid
|
SELECT uuid
|
||||||
FROM queryAccessibleObjectUuidsOfSubjectIds(
|
FROM queryAccessibleObjectUuidsOfSubjectIds(
|
||||||
'view', 'customer', currentSubjectsUuids()));
|
'SELECT, 'customer', currentSubjectsUuids()));
|
||||||
|
|
||||||
This view should be automatically updatable.
|
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.
|
With the larger dataset, the test suite initially needed over 7 seconds with this view query.
|
||||||
At this point the second variant was tried.
|
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.*
|
SELECT DISTINCT target.*
|
||||||
FROM customer AS target
|
FROM customer AS target
|
||||||
JOIN queryAccessibleObjectUuidsOfSubjectIds(
|
JOIN queryAccessibleObjectUuidsOfSubjectIds(
|
||||||
'view', 'customer', currentSubjectsUuids()) AS allowedObjId
|
'SELECT, 'customer', currentSubjectsUuids()) AS allowedObjId
|
||||||
ON target.uuid = allowedObjId;
|
ON target.uuid = allowedObjId;
|
||||||
|
|
||||||
This view cannot is not updatable automatically,
|
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.
|
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
|
## RbacGrant
|
||||||
|
|
||||||
Grant can be `empowered`, this means that the grantee user can grant the granted role to other users
|
Grant can be `empowered`, this means that the grantee user can grant the granted role to other users
|
||||||
and revoke grants to that role.
|
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.
|
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.
|
If a grant is not managed, it was created by an empowered user and can be deleted by empowered users.
|
||||||
|
@ -87,7 +87,7 @@ Acceptance-Tests run on a fully integrated and deployed system with deployed dou
|
|||||||
|
|
||||||
Acceptance-tests, are blackbox-tests and do <u>not</u> count into test-code-coverage.
|
Acceptance-tests, are blackbox-tests and do <u>not</u> count into test-code-coverage.
|
||||||
|
|
||||||
TODO: Complete the Acceptance-Tests test concept.
|
TODO.test: Complete the Acceptance-Tests test concept.
|
||||||
|
|
||||||
|
|
||||||
#### Performance-Tests
|
#### 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 <u>not</u> count into test-code-coverage.
|
System-Integration-tests, are blackbox-tests and do <u>not</u> count into test-code-coverage.
|
||||||
|
|
||||||
TODO: Complete the System-Integration-Tests test concept.
|
TODO.test: Complete the System-Integration-Tests test concept.
|
||||||
|
@ -1,33 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd">
|
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd">
|
||||||
<suppress>
|
|
||||||
<notes><![CDATA[
|
|
||||||
We don't use the Spring HTTP invoker which causes this vulnerability due to Java deserialization.
|
|
||||||
]]></notes>
|
|
||||||
<packageUrl regex="true">^pkg:maven/org\.springframework/spring-web@.*$</packageUrl>
|
|
||||||
<cve>CVE-2016-1000027</cve>
|
|
||||||
</suppress>
|
|
||||||
<suppress>
|
|
||||||
<notes><![CDATA[
|
|
||||||
We don't use the UNWRAP_SINGLE_VALUE_ARRAYS feature and thus are not affected.
|
|
||||||
]]></notes>
|
|
||||||
<packageUrl regex="true">^pkg:maven/com\.fasterxml\.jackson\.core/jackson\-databind@.*$</packageUrl>
|
|
||||||
<cve>CVE-2022-42003</cve>
|
|
||||||
</suppress>
|
|
||||||
<suppress>
|
|
||||||
<notes><![CDATA[
|
|
||||||
We don't parse external XML.
|
|
||||||
]]></notes>
|
|
||||||
<packageUrl regex="true">^pkg:maven/org\.eclipse\.angus/angus\-activation@.*$</packageUrl>
|
|
||||||
<cpe>cpe:/a:eclipse:eclipse_ide</cpe>
|
|
||||||
</suppress>
|
|
||||||
<suppress>
|
|
||||||
<notes><![CDATA[
|
|
||||||
We don't parse external XML.
|
|
||||||
]]></notes>
|
|
||||||
<packageUrl regex="true">^pkg:maven/jakarta\.activation/jakarta\.activation\-api@.*$</packageUrl>
|
|
||||||
<cpe>cpe:/a:eclipse:eclipse_ide</cpe>
|
|
||||||
</suppress>
|
|
||||||
<suppress>
|
<suppress>
|
||||||
<notes><![CDATA[
|
<notes><![CDATA[
|
||||||
Cyclic references are not possible if file comes in JSON text format.
|
Cyclic references are not possible if file comes in JSON text format.
|
||||||
@ -35,13 +7,6 @@
|
|||||||
<packageUrl regex="true">^pkg:maven/com\.fasterxml\.jackson\.core/jackson\-databind@.*$</packageUrl>
|
<packageUrl regex="true">^pkg:maven/com\.fasterxml\.jackson\.core/jackson\-databind@.*$</packageUrl>
|
||||||
<cpe>cpe:/a:fasterxml:jackson-databind</cpe>
|
<cpe>cpe:/a:fasterxml:jackson-databind</cpe>
|
||||||
</suppress>
|
</suppress>
|
||||||
<suppress>
|
|
||||||
<notes><![CDATA[
|
|
||||||
As far as I see Criteria.parse(...) cannot be reached with external data.
|
|
||||||
]]></notes>
|
|
||||||
<packageUrl regex="true">^pkg:maven/com\.jayway\.jsonpath/json\-path@.*$</packageUrl>
|
|
||||||
<vulnerabilityName>CVE-2023-51074</vulnerabilityName>
|
|
||||||
</suppress>
|
|
||||||
<suppress>
|
<suppress>
|
||||||
<notes><![CDATA[
|
<notes><![CDATA[
|
||||||
Internal tooling, not exposed to the Internet.
|
Internal tooling, not exposed to the Internet.
|
||||||
@ -49,17 +14,4 @@
|
|||||||
<packageUrl regex="true">^pkg:maven/org\.pitest/pitest\-command\-line@.*$</packageUrl>
|
<packageUrl regex="true">^pkg:maven/org\.pitest/pitest\-command\-line@.*$</packageUrl>
|
||||||
<cpe>cpe:/a:line:line</cpe>
|
<cpe>cpe:/a:line:line</cpe>
|
||||||
</suppress>
|
</suppress>
|
||||||
<suppress>
|
|
||||||
<notes><![CDATA[
|
|
||||||
Spring Boot 3.1.x has a transient dependency to snakeyaml 1.3
|
|
||||||
which contains this vulnerability.
|
|
||||||
|
|
||||||
We've explicitly bumped to 2.2, but the vulnerability checker does not seem to notice that.
|
|
||||||
|
|
||||||
TODO: Remove this suppression once we are on SpringBoot 3.2,
|
|
||||||
as well as the explicit version bump and the transient dependency exclude.
|
|
||||||
]]></notes>
|
|
||||||
<packageUrl regex="true">^pkg:maven/org\.yaml/snakeyaml@.*$</packageUrl>
|
|
||||||
<cve>CVE-2022-1471</cve>
|
|
||||||
</suppress>
|
|
||||||
</suppressions>
|
</suppressions>
|
||||||
|
@ -11,28 +11,4 @@ plugins {
|
|||||||
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.7.0'
|
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'
|
rootProject.name = 'hsadmin-ng'
|
||||||
|
@ -18,8 +18,8 @@ CREATE OR REPLACE FUNCTION historicize()
|
|||||||
RETURNS trigger
|
RETURNS trigger
|
||||||
LANGUAGE plpgsql STRICT AS $$
|
LANGUAGE plpgsql STRICT AS $$
|
||||||
DECLARE
|
DECLARE
|
||||||
currentUser VARCHAR(64);
|
currentUser VARCHAR(63);
|
||||||
currentTask varchar;
|
currentTask VARCHAR(127);
|
||||||
"row" RECORD;
|
"row" RECORD;
|
||||||
"alive" BOOLEAN;
|
"alive" BOOLEAN;
|
||||||
"sql" varchar;
|
"sql" varchar;
|
||||||
@ -37,27 +37,27 @@ END IF;
|
|||||||
|
|
||||||
-- determine task
|
-- determine task
|
||||||
currentTask = current_setting('hsadminng.currentTask');
|
currentTask = current_setting('hsadminng.currentTask');
|
||||||
IF (currentTask IS NULL OR length(currentTask) < 12) THEN
|
assert currentTask IS NOT NULL AND length(currentTask) >= 12,
|
||||||
RAISE EXCEPTION 'hsadminng.currentTask (%) must be defined and min 12 characters long, please use "SET LOCAL ...;"', currentTask;
|
format('hsadminng.currentTask (%s) must be defined and min 12 characters long, please use "SET LOCAL ...;"', currentTask);
|
||||||
END IF;
|
assert length(currentTask) <= 127,
|
||||||
RAISE NOTICE 'currentTask: %', currentTask;
|
format('hsadminng.currentTask (%s) must not be longer than 127 characters"', currentTask);
|
||||||
|
|
||||||
IF (TG_OP = 'INSERT') OR (TG_OP = 'UPDATE') THEN
|
IF (TG_OP = 'INSERT') OR (TG_OP = 'UPDATE') THEN
|
||||||
"row" := NEW;
|
"row" := NEW;
|
||||||
"alive" := TRUE;
|
"alive" := TRUE;
|
||||||
ELSE -- DELETE or TRUNCATE
|
ELSE -- DELETE or TRUNCATE
|
||||||
"row" := OLD;
|
"row" := OLD;
|
||||||
"alive" := FALSE;
|
"alive" := FALSE;
|
||||||
END IF;
|
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;
|
RAISE NOTICE 'sql: %', sql;
|
||||||
EXECUTE 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);
|
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;
|
RAISE NOTICE 'sql: %', sql;
|
||||||
EXECUTE sql USING "row";
|
EXECUTE sql USING "row";
|
||||||
|
|
||||||
RETURN "row";
|
RETURN "row";
|
||||||
END; $$;
|
END; $$;
|
||||||
|
|
||||||
CREATE OR REPLACE PROCEDURE create_historical_view(baseTable varchar)
|
CREATE OR REPLACE PROCEDURE create_historical_view(baseTable varchar)
|
||||||
|
@ -3,10 +3,10 @@
|
|||||||
-- --------------------------------------------------------
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
select isGranted(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'));
|
select isGranted(findRoleId('test_package#aaa00:OWNER'), findRoleId('administrators'));
|
||||||
-- call grantRoleToRole(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'));
|
-- call grantRoleToRole(findRoleId('administrators'), findRoleId('test_package#aaa00:OWNER'));
|
||||||
|
|
||||||
select count(*)
|
select count(*)
|
||||||
FROM queryAllPermissionsOfSubjectIdForObjectUuids(findRbacUser('superuser-fran@hostsharing.net'),
|
FROM queryAllPermissionsOfSubjectIdForObjectUuids(findRbacUser('superuser-fran@hostsharing.net'),
|
||||||
@ -25,7 +25,7 @@ FROM queryAllRbacUsersWithPermissionsFor(findEffectivePermissionId('customer',
|
|||||||
select *
|
select *
|
||||||
FROM queryAllRbacUsersWithPermissionsFor(findEffectivePermissionId('package',
|
FROM queryAllRbacUsersWithPermissionsFor(findEffectivePermissionId('package',
|
||||||
(SELECT uuid FROM RbacObject WHERE objectTable = 'package' LIMIT 1),
|
(SELECT uuid FROM RbacObject WHERE objectTable = 'package' LIMIT 1),
|
||||||
'delete'));
|
'DELETE'));
|
||||||
|
|
||||||
DO LANGUAGE plpgsql
|
DO LANGUAGE plpgsql
|
||||||
$$
|
$$
|
||||||
@ -34,12 +34,12 @@ $$
|
|||||||
result bool;
|
result bool;
|
||||||
BEGIN
|
BEGIN
|
||||||
userId = findRbacUser('superuser-alex@hostsharing.net');
|
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
|
IF (result) THEN
|
||||||
RAISE EXCEPTION 'expected permission NOT to be granted, but it is';
|
RAISE EXCEPTION 'expected permission NOT to be granted, but it is';
|
||||||
end if;
|
end if;
|
||||||
|
|
||||||
result = (SELECT * FROM isPermissionGrantedToSubject(findEffectivePermissionId('package', 94928, 'view'), userId));
|
result = (SELECT * FROM isPermissionGrantedToSubject(findPermissionId('package', 94928, 'SELECT'), userId));
|
||||||
IF (NOT result) THEN
|
IF (NOT result) THEN
|
||||||
RAISE EXCEPTION 'expected permission to be granted, but it is NOT';
|
RAISE EXCEPTION 'expected permission to be granted, but it is NOT';
|
||||||
end if;
|
end if;
|
||||||
|
@ -20,7 +20,7 @@ CREATE POLICY customer_policy ON customer
|
|||||||
TO restricted
|
TO restricted
|
||||||
USING (
|
USING (
|
||||||
-- id=1000
|
-- id=1000
|
||||||
isPermissionGrantedToSubject(findEffectivePermissionId('test_customer', id, 'view'), currentUserUuid())
|
isPermissionGrantedToSubject(findEffectivePermissionId('test_customer', id, 'SELECT'), currentUserUuid())
|
||||||
);
|
);
|
||||||
|
|
||||||
SET SESSION AUTHORIZATION restricted;
|
SET SESSION AUTHORIZATION restricted;
|
||||||
@ -35,7 +35,7 @@ SELECT * FROM customer;
|
|||||||
CREATE OR REPLACE RULE "_RETURN" AS
|
CREATE OR REPLACE RULE "_RETURN" AS
|
||||||
ON SELECT TO cust_view
|
ON SELECT TO cust_view
|
||||||
DO INSTEAD
|
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 * from cust_view LIMIT 10;
|
||||||
|
|
||||||
select queryAllPermissionsOfSubjectId(findRbacUser('superuser-alex@hostsharing.net'));
|
select queryAllPermissionsOfSubjectId(findRbacUser('superuser-alex@hostsharing.net'));
|
||||||
@ -52,7 +52,7 @@ CREATE OR REPLACE RULE "_RETURN" AS
|
|||||||
DO INSTEAD
|
DO INSTEAD
|
||||||
SELECT c.uuid, c.reference, c.prefix FROM customer AS c
|
SELECT c.uuid, c.reference, c.prefix FROM customer AS c
|
||||||
JOIN queryAllPermissionsOfSubjectId(currentUserUuid()) AS p
|
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;
|
GRANT ALL PRIVILEGES ON cust_view TO restricted;
|
||||||
|
|
||||||
SET SESSION SESSION AUTHORIZATION restricted;
|
SET SESSION SESSION AUTHORIZATION restricted;
|
||||||
@ -68,7 +68,7 @@ CREATE OR REPLACE VIEW cust_view AS
|
|||||||
SELECT c.uuid, c.reference, c.prefix
|
SELECT c.uuid, c.reference, c.prefix
|
||||||
FROM customer AS c
|
FROM customer AS c
|
||||||
JOIN queryAllPermissionsOfSubjectId(currentUserUuid()) AS p
|
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;
|
GRANT ALL PRIVILEGES ON cust_view TO restricted;
|
||||||
|
|
||||||
SET SESSION SESSION AUTHORIZATION 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
|
join RbacReference RR on g.ascendantUuid = RR.uuid
|
||||||
where g.descendantUuid in (
|
where g.descendantUuid in (
|
||||||
select uuid from queryAllPermissionsOfSubjectId(findRbacUser('alex@example.com'))
|
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'));
|
select queryAllPermissionsOfSubjectId(findRbacUser('aaaaouq@example.com'));
|
||||||
|
|
||||||
|
@ -15,11 +15,9 @@ import java.util.Collections;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static java.util.function.Predicate.not;
|
import static java.util.function.Predicate.not;
|
||||||
import static net.hostsharing.hsadminng.mapper.PostgresArray.fromPostgresArray;
|
|
||||||
import static org.springframework.transaction.annotation.Propagation.MANDATORY;
|
import static org.springframework.transaction.annotation.Propagation.MANDATORY;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@ -55,16 +53,15 @@ public class Context {
|
|||||||
final String currentRequest,
|
final String currentRequest,
|
||||||
final String currentUser,
|
final String currentUser,
|
||||||
final String assumedRoles) {
|
final String assumedRoles) {
|
||||||
final var query = em.createNativeQuery(
|
final var query = em.createNativeQuery("""
|
||||||
"""
|
|
||||||
call defineContext(
|
call defineContext(
|
||||||
cast(:currentTask as varchar),
|
cast(:currentTask as varchar(127)),
|
||||||
cast(:currentRequest as varchar),
|
cast(:currentRequest as text),
|
||||||
cast(:currentUser as varchar),
|
cast(:currentUser as varchar(63)),
|
||||||
cast(:assumedRoles as varchar));
|
cast(:assumedRoles as varchar(1023)));
|
||||||
""");
|
""");
|
||||||
query.setParameter("currentTask", shortenToMaxLength(currentTask, 96));
|
query.setParameter("currentTask", shortenToMaxLength(currentTask, 127));
|
||||||
query.setParameter("currentRequest", shortenToMaxLength(currentRequest, 512)); // TODO.spec: length?
|
query.setParameter("currentRequest", currentRequest);
|
||||||
query.setParameter("currentUser", currentUser);
|
query.setParameter("currentUser", currentUser);
|
||||||
query.setParameter("assumedRoles", assumedRoles != null ? assumedRoles : "");
|
query.setParameter("assumedRoles", assumedRoles != null ? assumedRoles : "");
|
||||||
query.executeUpdate();
|
query.executeUpdate();
|
||||||
@ -83,14 +80,11 @@ public class Context {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String[] getAssumedRoles() {
|
public String[] getAssumedRoles() {
|
||||||
final byte[] result = (byte[]) em.createNativeQuery("select assumedRoles() as roles", String[].class).getSingleResult();
|
return (String[]) em.createNativeQuery("select assumedRoles() as roles", String[].class).getSingleResult();
|
||||||
return fromPostgresArray(result, String.class, Function.identity());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public UUID[] currentSubjectsUuids() {
|
public UUID[] currentSubjectsUuids() {
|
||||||
final byte[] result = (byte[]) em.createNativeQuery("select currentSubjectsUuids() as uuids", UUID[].class)
|
return (UUID[]) em.createNativeQuery("select currentSubjectsUuids() as uuids", UUID[].class).getSingleResult();
|
||||||
.getSingleResult();
|
|
||||||
return fromPostgresArray(result, UUID.class, UUID::fromString);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getCallerMethodNameFromStackFrame(final int skipFrames) {
|
public static String getCallerMethodNameFromStackFrame(final int skipFrames) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package net.hostsharing.hsadminng.errors;
|
package net.hostsharing.hsadminng.errors;
|
||||||
|
|
||||||
import net.hostsharing.hsadminng.persistence.HasUuid;
|
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@ -8,7 +8,7 @@ public class ReferenceNotFoundException extends RuntimeException {
|
|||||||
|
|
||||||
private final Class<?> entityClass;
|
private final Class<?> entityClass;
|
||||||
private final UUID uuid;
|
private final UUID uuid;
|
||||||
public <E extends HasUuid> ReferenceNotFoundException(final Class<E> entityClass, final UUID uuid, final Throwable exc) {
|
public <E extends RbacObject> ReferenceNotFoundException(final Class<E> entityClass, final UUID uuid, final Throwable exc) {
|
||||||
super(exc);
|
super(exc);
|
||||||
this.entityClass = entityClass;
|
this.entityClass = entityClass;
|
||||||
this.uuid = uuid;
|
this.uuid = uuid;
|
||||||
|
@ -11,16 +11,18 @@ import org.springframework.http.converter.HttpMessageNotReadableException;
|
|||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.orm.jpa.JpaObjectRetrievalFailureException;
|
import org.springframework.orm.jpa.JpaObjectRetrievalFailureException;
|
||||||
import org.springframework.orm.jpa.JpaSystemException;
|
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.MethodArgumentNotValidException;
|
||||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.context.request.WebRequest;
|
import org.springframework.web.context.request.WebRequest;
|
||||||
|
import org.springframework.web.method.annotation.HandlerMethodValidationException;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
|
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
|
||||||
|
|
||||||
import jakarta.persistence.EntityNotFoundException;
|
import jakarta.persistence.EntityNotFoundException;
|
||||||
import jakarta.validation.ValidationException;
|
import jakarta.validation.ValidationException;
|
||||||
import java.util.NoSuchElementException;
|
import java.util.*;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import static net.hostsharing.hsadminng.errors.CustomErrorResponse.*;
|
import static net.hostsharing.hsadminng.errors.CustomErrorResponse.*;
|
||||||
@ -119,6 +121,28 @@ public class RestResponseEntityExceptionHandler
|
|||||||
return errorResponse(request, HttpStatus.BAD_REQUEST, errorList.toString());
|
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) {
|
private String userReadableEntityClassName(final String exceptionMessage) {
|
||||||
final var regex = "(net.hostsharing.hsadminng.[a-z0-9_.]*.[A-Za-z0-9_$]*Entity) ";
|
final var regex = "(net.hostsharing.hsadminng.[a-z0-9_.]*.[A-Za-z0-9_$]*Entity) ";
|
||||||
final var pattern = Pattern.compile(regex);
|
final var pattern = Pattern.compile(regex);
|
||||||
|
@ -3,7 +3,8 @@ package net.hostsharing.hsadminng.hs.office.bankaccount;
|
|||||||
import lombok.*;
|
import lombok.*;
|
||||||
import lombok.experimental.FieldNameConstants;
|
import lombok.experimental.FieldNameConstants;
|
||||||
import net.hostsharing.hsadminng.errors.DisplayName;
|
import net.hostsharing.hsadminng.errors.DisplayName;
|
||||||
import net.hostsharing.hsadminng.persistence.HasUuid;
|
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
|
||||||
|
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||||
|
|
||||||
@ -11,8 +12,13 @@ import jakarta.persistence.Entity;
|
|||||||
import jakarta.persistence.GeneratedValue;
|
import jakarta.persistence.GeneratedValue;
|
||||||
import jakarta.persistence.Id;
|
import jakarta.persistence.Id;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.UUID;
|
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;
|
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@ -24,11 +30,11 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@FieldNameConstants
|
@FieldNameConstants
|
||||||
@DisplayName("BankAccount")
|
@DisplayName("BankAccount")
|
||||||
public class HsOfficeBankAccountEntity implements HasUuid, Stringifyable {
|
public class HsOfficeBankAccountEntity implements RbacObject, Stringifyable {
|
||||||
|
|
||||||
private static Stringify<HsOfficeBankAccountEntity> toString = stringify(HsOfficeBankAccountEntity.class, "bankAccount")
|
private static Stringify<HsOfficeBankAccountEntity> toString = stringify(HsOfficeBankAccountEntity.class, "bankAccount")
|
||||||
|
.withIdProp(HsOfficeBankAccountEntity::getIban)
|
||||||
.withProp(Fields.holder, HsOfficeBankAccountEntity::getHolder)
|
.withProp(Fields.holder, HsOfficeBankAccountEntity::getHolder)
|
||||||
.withProp(Fields.iban, HsOfficeBankAccountEntity::getIban)
|
|
||||||
.withProp(Fields.bic, HsOfficeBankAccountEntity::getBic);
|
.withProp(Fields.bic, HsOfficeBankAccountEntity::getBic);
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@ -50,4 +56,28 @@ public class HsOfficeBankAccountEntity implements HasUuid, Stringifyable {
|
|||||||
public String toShortString() {
|
public String toShortString() {
|
||||||
return holder;
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,14 +3,22 @@ package net.hostsharing.hsadminng.hs.office.contact;
|
|||||||
import lombok.*;
|
import lombok.*;
|
||||||
import lombok.experimental.FieldNameConstants;
|
import lombok.experimental.FieldNameConstants;
|
||||||
import net.hostsharing.hsadminng.errors.DisplayName;
|
import net.hostsharing.hsadminng.errors.DisplayName;
|
||||||
import net.hostsharing.hsadminng.persistence.HasUuid;
|
import net.hostsharing.hsadminng.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.Stringify;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||||
import org.hibernate.annotations.GenericGenerator;
|
import org.hibernate.annotations.GenericGenerator;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.UUID;
|
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;
|
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@ -22,13 +30,12 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@FieldNameConstants
|
@FieldNameConstants
|
||||||
@DisplayName("Contact")
|
@DisplayName("Contact")
|
||||||
public class HsOfficeContactEntity implements Stringifyable, HasUuid {
|
public class HsOfficeContactEntity implements Stringifyable, RbacObject {
|
||||||
|
|
||||||
private static Stringify<HsOfficeContactEntity> toString = stringify(HsOfficeContactEntity.class, "contact")
|
private static Stringify<HsOfficeContactEntity> toString = stringify(HsOfficeContactEntity.class, "contact")
|
||||||
.withProp(Fields.label, HsOfficeContactEntity::getLabel)
|
.withProp(Fields.label, HsOfficeContactEntity::getLabel)
|
||||||
.withProp(Fields.emailAddresses, HsOfficeContactEntity::getEmailAddresses);
|
.withProp(Fields.emailAddresses, HsOfficeContactEntity::getEmailAddresses);
|
||||||
|
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(generator = "UUID")
|
@GeneratedValue(generator = "UUID")
|
||||||
@GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
|
@GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
|
||||||
@ -36,13 +43,13 @@ public class HsOfficeContactEntity implements Stringifyable, HasUuid {
|
|||||||
private String label;
|
private String label;
|
||||||
|
|
||||||
@Column(name = "postaladdress")
|
@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")
|
@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")
|
@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
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
@ -53,4 +60,26 @@ public class HsOfficeContactEntity implements Stringifyable, HasUuid {
|
|||||||
public String toShortString() {
|
public String toShortString() {
|
||||||
return label;
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,6 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
|
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import jakarta.validation.ValidationException;
|
import jakarta.validation.ValidationException;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -59,7 +58,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
|||||||
public ResponseEntity<HsOfficeCoopAssetsTransactionResource> addCoopAssetsTransaction(
|
public ResponseEntity<HsOfficeCoopAssetsTransactionResource> addCoopAssetsTransaction(
|
||||||
final String currentUser,
|
final String currentUser,
|
||||||
final String assumedRoles,
|
final String assumedRoles,
|
||||||
@Valid final HsOfficeCoopAssetsTransactionInsertResource requestBody) {
|
final HsOfficeCoopAssetsTransactionInsertResource requestBody) {
|
||||||
|
|
||||||
context.define(currentUser, assumedRoles);
|
context.define(currentUser, assumedRoles);
|
||||||
validate(requestBody);
|
validate(requestBody);
|
||||||
|
@ -1,20 +1,47 @@
|
|||||||
|
|
||||||
package net.hostsharing.hsadminng.hs.office.coopassets;
|
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.errors.DisplayName;
|
||||||
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
|
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.Stringify;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||||
import org.hibernate.annotations.GenericGenerator;
|
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.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import static java.util.Optional.ofNullable;
|
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;
|
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@ -25,16 +52,15 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
|||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@DisplayName("CoopAssetsTransaction")
|
@DisplayName("CoopAssetsTransaction")
|
||||||
public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, HasUuid {
|
public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, RbacObject {
|
||||||
|
|
||||||
private static Stringify<HsOfficeCoopAssetsTransactionEntity> stringify = stringify(HsOfficeCoopAssetsTransactionEntity.class)
|
private static Stringify<HsOfficeCoopAssetsTransactionEntity> stringify = stringify(HsOfficeCoopAssetsTransactionEntity.class)
|
||||||
.withProp(HsOfficeCoopAssetsTransactionEntity::getMemberNumber)
|
.withIdProp(HsOfficeCoopAssetsTransactionEntity::getTaggedMemberNumber)
|
||||||
.withProp(HsOfficeCoopAssetsTransactionEntity::getValueDate)
|
.withProp(HsOfficeCoopAssetsTransactionEntity::getValueDate)
|
||||||
.withProp(HsOfficeCoopAssetsTransactionEntity::getTransactionType)
|
.withProp(HsOfficeCoopAssetsTransactionEntity::getTransactionType)
|
||||||
.withProp(HsOfficeCoopAssetsTransactionEntity::getAssetValue)
|
.withProp(HsOfficeCoopAssetsTransactionEntity::getAssetValue)
|
||||||
.withProp(HsOfficeCoopAssetsTransactionEntity::getReference)
|
.withProp(HsOfficeCoopAssetsTransactionEntity::getReference)
|
||||||
.withProp(HsOfficeCoopAssetsTransactionEntity::getComment)
|
.withProp(HsOfficeCoopAssetsTransactionEntity::getComment)
|
||||||
.withSeparator(", ")
|
|
||||||
.quotedValues(false);
|
.quotedValues(false);
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@ -76,8 +102,8 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, HasUu
|
|||||||
private String comment;
|
private String comment;
|
||||||
|
|
||||||
|
|
||||||
public Integer getMemberNumber() {
|
public String getTaggedMemberNumber() {
|
||||||
return ofNullable(membership).map(HsOfficeMembershipEntity::getMemberNumber).orElse(null);
|
return ofNullable(membership).map(HsOfficeMembershipEntity::toShortString).orElse("M-?????");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -87,6 +113,24 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, HasUu
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toShortString() {
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,6 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
|
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import jakarta.validation.ValidationException;
|
import jakarta.validation.ValidationException;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -60,7 +59,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
|
|||||||
public ResponseEntity<HsOfficeCoopSharesTransactionResource> addCoopSharesTransaction(
|
public ResponseEntity<HsOfficeCoopSharesTransactionResource> addCoopSharesTransaction(
|
||||||
final String currentUser,
|
final String currentUser,
|
||||||
final String assumedRoles,
|
final String assumedRoles,
|
||||||
@Valid final HsOfficeCoopSharesTransactionInsertResource requestBody) {
|
final HsOfficeCoopSharesTransactionInsertResource requestBody) {
|
||||||
|
|
||||||
context.define(currentUser, assumedRoles);
|
context.define(currentUser, assumedRoles);
|
||||||
validate(requestBody);
|
validate(requestBody);
|
||||||
|
@ -1,17 +1,41 @@
|
|||||||
package net.hostsharing.hsadminng.hs.office.coopshares;
|
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.errors.DisplayName;
|
||||||
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
|
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.Stringify;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
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.time.LocalDate;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import static java.util.Optional.ofNullable;
|
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;
|
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@ -22,7 +46,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
|||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@DisplayName("CoopShareTransaction")
|
@DisplayName("CoopShareTransaction")
|
||||||
public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, HasUuid {
|
public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, RbacObject {
|
||||||
|
|
||||||
private static Stringify<HsOfficeCoopSharesTransactionEntity> stringify = stringify(HsOfficeCoopSharesTransactionEntity.class)
|
private static Stringify<HsOfficeCoopSharesTransactionEntity> stringify = stringify(HsOfficeCoopSharesTransactionEntity.class)
|
||||||
.withProp(HsOfficeCoopSharesTransactionEntity::getMemberNumberTagged)
|
.withProp(HsOfficeCoopSharesTransactionEntity::getMemberNumberTagged)
|
||||||
@ -31,7 +55,6 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, HasUu
|
|||||||
.withProp(HsOfficeCoopSharesTransactionEntity::getShareCount)
|
.withProp(HsOfficeCoopSharesTransactionEntity::getShareCount)
|
||||||
.withProp(HsOfficeCoopSharesTransactionEntity::getReference)
|
.withProp(HsOfficeCoopSharesTransactionEntity::getReference)
|
||||||
.withProp(HsOfficeCoopSharesTransactionEntity::getComment)
|
.withProp(HsOfficeCoopSharesTransactionEntity::getComment)
|
||||||
.withSeparator(", ")
|
|
||||||
.quotedValues(false);
|
.quotedValues(false);
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@ -84,4 +107,22 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, HasUu
|
|||||||
public String toShortString() {
|
public String toShortString() {
|
||||||
return "%s%+d".formatted(getMemberNumberTagged(), shareCount);
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.HsOfficeDebitorInsertResource;
|
||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorPatchResource;
|
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.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 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.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
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 org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
|
||||||
|
|
||||||
import jakarta.persistence.EntityManager;
|
import jakarta.persistence.EntityManager;
|
||||||
|
import jakarta.persistence.EntityNotFoundException;
|
||||||
import jakarta.persistence.PersistenceContext;
|
import jakarta.persistence.PersistenceContext;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
|
||||||
public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
||||||
@ -30,6 +37,9 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private HsOfficeDebitorRepository debitorRepo;
|
private HsOfficeDebitorRepository debitorRepo;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private HsOfficeRelationRepository relRepo;
|
||||||
|
|
||||||
@PersistenceContext
|
@PersistenceContext
|
||||||
private EntityManager em;
|
private EntityManager em;
|
||||||
|
|
||||||
@ -53,22 +63,44 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
|||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public ResponseEntity<HsOfficeDebitorResource> addDebitor(
|
public ResponseEntity<HsOfficeDebitorResource> addDebitor(
|
||||||
final String currentUser,
|
String currentUser,
|
||||||
final String assumedRoles,
|
String assumedRoles,
|
||||||
final HsOfficeDebitorInsertResource body) {
|
HsOfficeDebitorInsertResource body) {
|
||||||
|
|
||||||
context.define(currentUser, assumedRoles);
|
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 =
|
final var uri =
|
||||||
MvcUriComponentsBuilder.fromController(getClass())
|
MvcUriComponentsBuilder.fromController(getClass())
|
||||||
.path("/api/hs/office/debitors/{id}")
|
.path("/api/hs/office/debitors/{id}")
|
||||||
.buildAndExpand(saved.getUuid())
|
.buildAndExpand(savedEntity.getUuid())
|
||||||
.toUri();
|
.toUri();
|
||||||
final var mapped = mapper.map(saved, HsOfficeDebitorResource.class);
|
final var mapped = mapper.map(savedEntity, HsOfficeDebitorResource.class);
|
||||||
return ResponseEntity.created(uri).body(mapped);
|
return ResponseEntity.created(uri).body(mapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,6 +151,7 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
|||||||
new HsOfficeDebitorEntityPatcher(em, current).apply(body);
|
new HsOfficeDebitorEntityPatcher(em, current).apply(body);
|
||||||
|
|
||||||
final var saved = debitorRepo.save(current);
|
final var saved = debitorRepo.save(current);
|
||||||
|
Hibernate.initialize(saved);
|
||||||
final var mapped = mapper.map(saved, HsOfficeDebitorResource.class);
|
final var mapped = mapper.map(saved, HsOfficeDebitorResource.class);
|
||||||
return ResponseEntity.ok(mapped);
|
return ResponseEntity.ok(mapped);
|
||||||
}
|
}
|
||||||
|
@ -3,40 +3,56 @@ package net.hostsharing.hsadminng.hs.office.debitor;
|
|||||||
import lombok.*;
|
import lombok.*;
|
||||||
import net.hostsharing.hsadminng.errors.DisplayName;
|
import net.hostsharing.hsadminng.errors.DisplayName;
|
||||||
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
|
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
|
||||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
|
|
||||||
import net.hostsharing.hsadminng.persistence.HasUuid;
|
|
||||||
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
|
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.Stringify;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||||
import org.hibernate.annotations.GenericGenerator;
|
import org.hibernate.annotations.GenericGenerator;
|
||||||
|
import org.hibernate.annotations.JoinFormula;
|
||||||
|
import org.hibernate.annotations.NotFound;
|
||||||
|
import org.hibernate.annotations.NotFoundAction;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import java.util.Optional;
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.UUID;
|
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;
|
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "hs_office_debitor_rv")
|
@Table(name = "hs_office_debitor_rv")
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@Builder
|
@Builder(toBuilder = true)
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@DisplayName("Debitor")
|
@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 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<HsOfficeDebitorEntity> stringify =
|
private static Stringify<HsOfficeDebitorEntity> stringify =
|
||||||
stringify(HsOfficeDebitorEntity.class, "debitor")
|
stringify(HsOfficeDebitorEntity.class, "debitor")
|
||||||
.withProp(e -> DEBITOR_NUMBER_TAG + e.getDebitorNumber())
|
.withIdProp(HsOfficeDebitorEntity::toShortString)
|
||||||
.withProp(HsOfficeDebitorEntity::getPartner)
|
.withProp(e -> ofNullable(e.getDebitorRel()).map(HsOfficeRelationEntity::toShortString).orElse(null))
|
||||||
.withProp(HsOfficeDebitorEntity::getDefaultPrefix)
|
.withProp(HsOfficeDebitorEntity::getDefaultPrefix)
|
||||||
.withSeparator(": ")
|
|
||||||
.quotedValues(false);
|
.quotedValues(false);
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@ -45,15 +61,29 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable {
|
|||||||
private UUID uuid;
|
private UUID uuid;
|
||||||
|
|
||||||
@ManyToOne
|
@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;
|
private HsOfficePartnerEntity partner;
|
||||||
|
|
||||||
@Column(name = "debitornumbersuffix", columnDefinition = "numeric(2)")
|
@Column(name = "debitornumbersuffix", length = 2)
|
||||||
private Byte debitorNumberSuffix; // TODO maybe rather as a formatted String?
|
@Pattern(regexp = TWO_DECIMAL_DIGITS)
|
||||||
|
private String debitorNumberSuffix;
|
||||||
|
|
||||||
@ManyToOne
|
@ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = false)
|
||||||
@JoinColumn(name = "billingcontactuuid")
|
@JoinColumn(name = "debitorreluuid", nullable = false)
|
||||||
private HsOfficeContactEntity billingContact; // TODO: migrate to billingPerson
|
private HsOfficeRelationEntity debitorRel;
|
||||||
|
|
||||||
@Column(name = "billable", nullable = false)
|
@Column(name = "billable", nullable = false)
|
||||||
private Boolean billable; // not a primitive because otherwise the default would be 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 defaultPrefix;
|
||||||
|
|
||||||
private String getDebitorNumberString() {
|
private String getDebitorNumberString() {
|
||||||
if (partner == null || partner.getPartnerNumber() == null || debitorNumberSuffix == null ) {
|
return ofNullable(partner)
|
||||||
return null;
|
.filter(partner -> debitorNumberSuffix != null)
|
||||||
}
|
.map(HsOfficePartnerEntity::getPartnerNumber)
|
||||||
return partner.getPartnerNumber() + String.format("%02d", debitorNumberSuffix);
|
.map(Object::toString)
|
||||||
|
.map(partnerNumber -> partnerNumber + debitorNumberSuffix)
|
||||||
|
.orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Integer getDebitorNumber() {
|
public Integer getDebitorNumber() {
|
||||||
return Optional.ofNullable(getDebitorNumberString()).map(Integer::parseInt).orElse(null);
|
return ofNullable(getDebitorNumberString()).map(Integer::parseInt).orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -97,4 +129,68 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable {
|
|||||||
public String toShortString() {
|
public String toShortString() {
|
||||||
return DEBITOR_NUMBER_TAG + getDebitorNumberString();
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
package net.hostsharing.hsadminng.hs.office.debitor;
|
package net.hostsharing.hsadminng.hs.office.debitor;
|
||||||
|
|
||||||
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
|
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.generated.api.v1.model.HsOfficeDebitorPatchResource;
|
||||||
|
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity;
|
||||||
import net.hostsharing.hsadminng.mapper.EntityPatcher;
|
import net.hostsharing.hsadminng.mapper.EntityPatcher;
|
||||||
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
|
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
|
||||||
|
|
||||||
@ -23,9 +23,9 @@ class HsOfficeDebitorEntityPatcher implements EntityPatcher<HsOfficeDebitorPatch
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void apply(final HsOfficeDebitorPatchResource resource) {
|
public void apply(final HsOfficeDebitorPatchResource resource) {
|
||||||
OptionalFromJson.of(resource.getBillingContactUuid()).ifPresent(newValue -> {
|
OptionalFromJson.of(resource.getDebitorRelUuid()).ifPresent(newValue -> {
|
||||||
verifyNotNull(newValue, "billingContact");
|
verifyNotNull(newValue, "debitorRel");
|
||||||
entity.setBillingContact(em.getReference(HsOfficeContactEntity.class, newValue));
|
entity.setDebitorRel(em.getReference(HsOfficeRelationEntity.class, newValue));
|
||||||
});
|
});
|
||||||
Optional.ofNullable(resource.getBillable()).ifPresent(entity::setBillable);
|
Optional.ofNullable(resource.getBillable()).ifPresent(entity::setBillable);
|
||||||
OptionalFromJson.of(resource.getVatId()).ifPresent(entity::setVatId);
|
OptionalFromJson.of(resource.getVatId()).ifPresent(entity::setVatId);
|
||||||
|
@ -13,7 +13,10 @@ public interface HsOfficeDebitorRepository extends Repository<HsOfficeDebitorEnt
|
|||||||
|
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT debitor FROM HsOfficeDebitorEntity debitor
|
SELECT debitor FROM HsOfficeDebitorEntity debitor
|
||||||
WHERE cast(debitor.partner.partnerNumber as integer) = :partnerNumber
|
JOIN HsOfficePartnerEntity partner
|
||||||
|
ON partner.partnerRel.holder = debitor.debitorRel.anchor
|
||||||
|
AND partner.partnerRel.type = 'PARTNER' AND debitor.debitorRel.type = 'DEBITOR'
|
||||||
|
WHERE cast(partner.partnerNumber as integer) = :partnerNumber
|
||||||
AND cast(debitor.debitorNumberSuffix as integer) = :debitorNumberSuffix
|
AND cast(debitor.debitorNumberSuffix as integer) = :debitorNumberSuffix
|
||||||
""")
|
""")
|
||||||
List<HsOfficeDebitorEntity> findDebitorByDebitorNumber(int partnerNumber, byte debitorNumberSuffix);
|
List<HsOfficeDebitorEntity> findDebitorByDebitorNumber(int partnerNumber, byte debitorNumberSuffix);
|
||||||
@ -24,9 +27,15 @@ public interface HsOfficeDebitorRepository extends Repository<HsOfficeDebitorEnt
|
|||||||
|
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT debitor FROM HsOfficeDebitorEntity debitor
|
SELECT debitor FROM HsOfficeDebitorEntity debitor
|
||||||
JOIN HsOfficePartnerEntity partner ON partner.uuid = debitor.partner.uuid
|
JOIN HsOfficePartnerEntity partner
|
||||||
JOIN HsOfficePersonEntity person ON person.uuid = partner.person.uuid
|
ON partner.partnerRel.holder = debitor.debitorRel.anchor
|
||||||
JOIN HsOfficeContactEntity contact ON contact.uuid = debitor.billingContact.uuid
|
AND partner.partnerRel.type = 'PARTNER' AND debitor.debitorRel.type = 'DEBITOR'
|
||||||
|
JOIN HsOfficePersonEntity person
|
||||||
|
ON person.uuid = partner.partnerRel.holder.uuid
|
||||||
|
OR person.uuid = debitor.debitorRel.holder.uuid
|
||||||
|
JOIN HsOfficeContactEntity contact
|
||||||
|
ON contact.uuid = debitor.debitorRel.contact.uuid
|
||||||
|
OR contact.uuid = partner.partnerRel.contact.uuid
|
||||||
WHERE :name is null
|
WHERE :name is null
|
||||||
OR partner.details.birthName like concat(cast(:name as text), '%')
|
OR partner.details.birthName like concat(cast(:name as text), '%')
|
||||||
OR person.tradeName like concat(cast(:name as text), '%')
|
OR person.tradeName like concat(cast(:name as text), '%')
|
||||||
|
@ -12,9 +12,6 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
|
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
|
||||||
|
|
||||||
import jakarta.persistence.EntityManager;
|
|
||||||
import jakarta.persistence.PersistenceContext;
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
@ -32,9 +29,6 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private HsOfficeMembershipRepository membershipRepo;
|
private HsOfficeMembershipRepository membershipRepo;
|
||||||
|
|
||||||
@PersistenceContext
|
|
||||||
private EntityManager em;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public ResponseEntity<List<HsOfficeMembershipResource>> listMemberships(
|
public ResponseEntity<List<HsOfficeMembershipResource>> listMemberships(
|
||||||
@ -58,7 +52,7 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
|
|||||||
public ResponseEntity<HsOfficeMembershipResource> addMembership(
|
public ResponseEntity<HsOfficeMembershipResource> addMembership(
|
||||||
final String currentUser,
|
final String currentUser,
|
||||||
final String assumedRoles,
|
final String assumedRoles,
|
||||||
@Valid final HsOfficeMembershipInsertResource body) {
|
final HsOfficeMembershipInsertResource body) {
|
||||||
|
|
||||||
context.define(currentUser, assumedRoles);
|
context.define(currentUser, assumedRoles);
|
||||||
|
|
||||||
@ -121,7 +115,7 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
|
|||||||
|
|
||||||
final var current = membershipRepo.findByUuid(membershipUuid).orElseThrow();
|
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 saved = membershipRepo.save(current);
|
||||||
final var mapped = mapper.map(saved, HsOfficeMembershipResource.class, SEPA_MANDATE_ENTITY_TO_RESOURCE_POSTMAPPER);
|
final var mapped = mapper.map(saved, HsOfficeMembershipResource.class, SEPA_MANDATE_ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||||
|
@ -1,23 +1,38 @@
|
|||||||
package net.hostsharing.hsadminng.hs.office.membership;
|
package net.hostsharing.hsadminng.hs.office.membership;
|
||||||
|
|
||||||
import com.vladmihalcea.hibernate.type.range.PostgreSQLRangeType;
|
import io.hypersistence.utils.hibernate.type.range.PostgreSQLRangeType;
|
||||||
import com.vladmihalcea.hibernate.type.range.Range;
|
import io.hypersistence.utils.hibernate.type.range.Range;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
import net.hostsharing.hsadminng.errors.DisplayName;
|
import net.hostsharing.hsadminng.errors.DisplayName;
|
||||||
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
|
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity;
|
||||||
import net.hostsharing.hsadminng.persistence.HasUuid;
|
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
|
||||||
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
|
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.Stringify;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||||
import org.hibernate.annotations.Fetch;
|
|
||||||
import org.hibernate.annotations.FetchMode;
|
|
||||||
import org.hibernate.annotations.Type;
|
import org.hibernate.annotations.Type;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import java.io.IOException;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.*;
|
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.*;
|
||||||
|
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
|
||||||
|
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.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;
|
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@ -28,17 +43,16 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
|||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@DisplayName("Membership")
|
@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 MEMBER_NUMBER_TAG = "M-";
|
||||||
|
public static final String TWO_DECIMAL_DIGITS = "^([0-9]{2})$";
|
||||||
|
|
||||||
private static Stringify<HsOfficeMembershipEntity> stringify = stringify(HsOfficeMembershipEntity.class)
|
private static Stringify<HsOfficeMembershipEntity> stringify = stringify(HsOfficeMembershipEntity.class)
|
||||||
.withProp(e -> MEMBER_NUMBER_TAG + e.getMemberNumber())
|
.withProp(e -> MEMBER_NUMBER_TAG + e.getMemberNumber())
|
||||||
.withProp(e -> e.getPartner().toShortString())
|
.withProp(e -> e.getPartner().toShortString())
|
||||||
.withProp(e -> e.getMainDebitor().toShortString())
|
|
||||||
.withProp(e -> e.getValidity().asString())
|
.withProp(e -> e.getValidity().asString())
|
||||||
.withProp(HsOfficeMembershipEntity::getReasonForTermination)
|
.withProp(HsOfficeMembershipEntity::getReasonForTermination)
|
||||||
.withSeparator(", ")
|
|
||||||
.quotedValues(false);
|
.quotedValues(false);
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@ -49,12 +63,8 @@ public class HsOfficeMembershipEntity implements HasUuid, Stringifyable {
|
|||||||
@JoinColumn(name = "partneruuid")
|
@JoinColumn(name = "partneruuid")
|
||||||
private HsOfficePartnerEntity partner;
|
private HsOfficePartnerEntity partner;
|
||||||
|
|
||||||
@ManyToOne
|
|
||||||
@Fetch(FetchMode.JOIN)
|
|
||||||
@JoinColumn(name = "maindebitoruuid")
|
|
||||||
private HsOfficeDebitorEntity mainDebitor;
|
|
||||||
|
|
||||||
@Column(name = "membernumbersuffix", length = 2)
|
@Column(name = "membernumbersuffix", length = 2)
|
||||||
|
@Pattern(regexp = TWO_DECIMAL_DIGITS)
|
||||||
private String memberNumberSuffix;
|
private String memberNumberSuffix;
|
||||||
|
|
||||||
@Column(name = "validity", columnDefinition = "daterange")
|
@Column(name = "validity", columnDefinition = "daterange")
|
||||||
@ -114,4 +124,45 @@ public class HsOfficeMembershipEntity implements HasUuid, Stringifyable {
|
|||||||
setReasonForTermination(HsOfficeReasonForTermination.NONE);
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,37 +1,26 @@
|
|||||||
package net.hostsharing.hsadminng.hs.office.membership;
|
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.hs.office.generated.api.v1.model.HsOfficeMembershipPatchResource;
|
||||||
import net.hostsharing.hsadminng.mapper.EntityPatcher;
|
import net.hostsharing.hsadminng.mapper.EntityPatcher;
|
||||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||||
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
|
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
|
||||||
|
|
||||||
import jakarta.persistence.EntityManager;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
public class HsOfficeMembershipEntityPatcher implements EntityPatcher<HsOfficeMembershipPatchResource> {
|
public class HsOfficeMembershipEntityPatcher implements EntityPatcher<HsOfficeMembershipPatchResource> {
|
||||||
|
|
||||||
private final EntityManager em;
|
|
||||||
private final Mapper mapper;
|
private final Mapper mapper;
|
||||||
private final HsOfficeMembershipEntity entity;
|
private final HsOfficeMembershipEntity entity;
|
||||||
|
|
||||||
public HsOfficeMembershipEntityPatcher(
|
public HsOfficeMembershipEntityPatcher(
|
||||||
final EntityManager em,
|
|
||||||
final Mapper mapper,
|
final Mapper mapper,
|
||||||
final HsOfficeMembershipEntity entity) {
|
final HsOfficeMembershipEntity entity) {
|
||||||
this.em = em;
|
|
||||||
this.mapper = mapper;
|
this.mapper = mapper;
|
||||||
this.entity = entity;
|
this.entity = entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void apply(final HsOfficeMembershipPatchResource resource) {
|
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(
|
OptionalFromJson.of(resource.getValidTo()).ifPresent(
|
||||||
entity::setValidTo);
|
entity::setValidTo);
|
||||||
Optional.ofNullable(resource.getReasonForTermination())
|
Optional.ofNullable(resource.getReasonForTermination())
|
||||||
@ -40,10 +29,4 @@ public class HsOfficeMembershipEntityPatcher implements EntityPatcher<HsOfficeMe
|
|||||||
OptionalFromJson.of(resource.getMembershipFeeBillable()).ifPresent(
|
OptionalFromJson.of(resource.getMembershipFeeBillable()).ifPresent(
|
||||||
entity::setMembershipFeeBillable);
|
entity::setMembershipFeeBillable);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void verifyNotNull(final UUID newValue, final String propertyName) {
|
|
||||||
if (newValue == null) {
|
|
||||||
throw new IllegalArgumentException("property '" + propertyName + "' must not be null");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -7,13 +7,13 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficePartners
|
|||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerInsertResource;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerInsertResource;
|
||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerPatchResource;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerPatchResource;
|
||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerResource;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerResource;
|
||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerRoleInsertResource;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerRelInsertResource;
|
||||||
import net.hostsharing.hsadminng.persistence.HasUuid;
|
|
||||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
|
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
|
||||||
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity;
|
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity;
|
||||||
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipRepository;
|
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRepository;
|
||||||
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipType;
|
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType;
|
||||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||||
|
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@ -40,7 +40,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
|
|||||||
private HsOfficePartnerRepository partnerRepo;
|
private HsOfficePartnerRepository partnerRepo;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsOfficeRelationshipRepository relationshipRepo;
|
private HsOfficeRelationRepository relationRepo;
|
||||||
|
|
||||||
@PersistenceContext
|
@PersistenceContext
|
||||||
private EntityManager em;
|
private EntityManager em;
|
||||||
@ -110,9 +110,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
|
|||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (partnerRepo.deleteByUuid(partnerUuid) != 1 ||
|
if (partnerRepo.deleteByUuid(partnerUuid) != 1) {
|
||||||
// TODO: move to after delete trigger in partner
|
|
||||||
relationshipRepo.deleteByUuid(partnerToDelete.get().getPartnerRole().getUuid()) != 1 ) {
|
|
||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,24 +139,22 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
|
|||||||
private HsOfficePartnerEntity createPartnerEntity(final HsOfficePartnerInsertResource body) {
|
private HsOfficePartnerEntity createPartnerEntity(final HsOfficePartnerInsertResource body) {
|
||||||
final var entityToSave = new HsOfficePartnerEntity();
|
final var entityToSave = new HsOfficePartnerEntity();
|
||||||
entityToSave.setPartnerNumber(body.getPartnerNumber());
|
entityToSave.setPartnerNumber(body.getPartnerNumber());
|
||||||
entityToSave.setPartnerRole(persistPartnerRole(body.getPartnerRole()));
|
entityToSave.setPartnerRel(persistPartnerRel(body.getPartnerRel()));
|
||||||
entityToSave.setContact(ref(HsOfficeContactEntity.class, body.getContactUuid()));
|
|
||||||
entityToSave.setPerson(ref(HsOfficePersonEntity.class, body.getPersonUuid()));
|
|
||||||
entityToSave.setDetails(mapper.map(body.getDetails(), HsOfficePartnerDetailsEntity.class));
|
entityToSave.setDetails(mapper.map(body.getDetails(), HsOfficePartnerDetailsEntity.class));
|
||||||
return entityToSave;
|
return entityToSave;
|
||||||
}
|
}
|
||||||
|
|
||||||
private HsOfficeRelationshipEntity persistPartnerRole(final HsOfficePartnerRoleInsertResource resource) {
|
private HsOfficeRelationEntity persistPartnerRel(final HsOfficePartnerRelInsertResource resource) {
|
||||||
final var entity = new HsOfficeRelationshipEntity();
|
final var entity = new HsOfficeRelationEntity();
|
||||||
entity.setRelType(HsOfficeRelationshipType.PARTNER);
|
entity.setType(HsOfficeRelationType.PARTNER);
|
||||||
entity.setRelAnchor(ref(HsOfficePersonEntity.class, resource.getRelAnchorUuid()));
|
entity.setAnchor(ref(HsOfficePersonEntity.class, resource.getAnchorUuid()));
|
||||||
entity.setRelHolder(ref(HsOfficePersonEntity.class, resource.getRelHolderUuid()));
|
entity.setHolder(ref(HsOfficePersonEntity.class, resource.getHolderUuid()));
|
||||||
entity.setContact(ref(HsOfficeContactEntity.class, resource.getContactUuid()));
|
entity.setContact(ref(HsOfficeContactEntity.class, resource.getContactUuid()));
|
||||||
em.persist(entity);
|
em.persist(entity);
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
private <E extends HasUuid> E ref(final Class<E> entityClass, final UUID uuid) {
|
private <E extends RbacObject> E ref(final Class<E> entityClass, final UUID uuid) {
|
||||||
try {
|
try {
|
||||||
return em.getReference(entityClass, uuid);
|
return em.getReference(entityClass, uuid);
|
||||||
} catch (final Throwable exc) {
|
} catch (final Throwable exc) {
|
||||||
|
@ -2,14 +2,20 @@ package net.hostsharing.hsadminng.hs.office.partner;
|
|||||||
|
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
import net.hostsharing.hsadminng.errors.DisplayName;
|
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.Stringify;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
import java.io.IOException;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static net.hostsharing.hsadminng.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;
|
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@ -20,7 +26,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
|||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@DisplayName("PartnerDetails")
|
@DisplayName("PartnerDetails")
|
||||||
public class HsOfficePartnerDetailsEntity implements HasUuid, Stringifyable {
|
public class HsOfficePartnerDetailsEntity implements RbacObject, Stringifyable {
|
||||||
|
|
||||||
private static Stringify<HsOfficePartnerDetailsEntity> stringify = stringify(
|
private static Stringify<HsOfficePartnerDetailsEntity> stringify = stringify(
|
||||||
HsOfficePartnerDetailsEntity.class,
|
HsOfficePartnerDetailsEntity.class,
|
||||||
@ -31,7 +37,6 @@ public class HsOfficePartnerDetailsEntity implements HasUuid, Stringifyable {
|
|||||||
.withProp(HsOfficePartnerDetailsEntity::getBirthday)
|
.withProp(HsOfficePartnerDetailsEntity::getBirthday)
|
||||||
.withProp(HsOfficePartnerDetailsEntity::getBirthName)
|
.withProp(HsOfficePartnerDetailsEntity::getBirthName)
|
||||||
.withProp(HsOfficePartnerDetailsEntity::getDateOfDeath)
|
.withProp(HsOfficePartnerDetailsEntity::getDateOfDeath)
|
||||||
.withSeparator(", ")
|
|
||||||
.quotedValues(false);
|
.quotedValues(false);
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@ -55,6 +60,36 @@ public class HsOfficePartnerDetailsEntity implements HasUuid, Stringifyable {
|
|||||||
return registrationNumber != null ? registrationNumber
|
return registrationNumber != null ? registrationNumber
|
||||||
: birthName != null ? birthName
|
: birthName != null ? birthName
|
||||||
: birthday != null ? birthday.toString()
|
: birthday != null ? birthday.toString()
|
||||||
: dateOfDeath != null ? dateOfDeath.toString() : "<empty details>";
|
: dateOfDeath != null ? dateOfDeath.toString()
|
||||||
|
: "<empty details>";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static RbacView rbac() {
|
||||||
|
return rbacViewFor("partnerDetails", HsOfficePartnerDetailsEntity.class)
|
||||||
|
.withIdentityView(SQL.query("""
|
||||||
|
SELECT 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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,40 @@
|
|||||||
package net.hostsharing.hsadminng.hs.office.partner;
|
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.errors.DisplayName;
|
||||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
|
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.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.Stringify;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||||
import org.hibernate.annotations.NotFound;
|
import org.hibernate.annotations.NotFound;
|
||||||
import org.hibernate.annotations.NotFoundAction;
|
import org.hibernate.annotations.NotFoundAction;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.Column;
|
||||||
import java.util.Optional;
|
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 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;
|
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@ -25,12 +45,20 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
|||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@DisplayName("Partner")
|
@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<HsOfficePartnerEntity> stringify = stringify(HsOfficePartnerEntity.class, "partner")
|
private static Stringify<HsOfficePartnerEntity> stringify = stringify(HsOfficePartnerEntity.class, "partner")
|
||||||
.withProp(HsOfficePartnerEntity::getPerson)
|
.withIdProp(HsOfficePartnerEntity::toShortString)
|
||||||
.withProp(HsOfficePartnerEntity::getContact)
|
.withProp(p -> ofNullable(p.getPartnerRel())
|
||||||
.withSeparator(": ")
|
.map(HsOfficeRelationEntity::getHolder)
|
||||||
|
.map(HsOfficePersonEntity::toShortString)
|
||||||
|
.orElse(null))
|
||||||
|
.withProp(p -> ofNullable(p.getPartnerRel())
|
||||||
|
.map(HsOfficeRelationEntity::getContact)
|
||||||
|
.map(HsOfficeContactEntity::toShortString)
|
||||||
|
.orElse(null))
|
||||||
.quotedValues(false);
|
.quotedValues(false);
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@ -40,25 +68,19 @@ public class HsOfficePartnerEntity implements Stringifyable, HasUuid {
|
|||||||
@Column(name = "partnernumber", columnDefinition = "numeric(5) not null")
|
@Column(name = "partnernumber", columnDefinition = "numeric(5) not null")
|
||||||
private Integer partnerNumber;
|
private Integer partnerNumber;
|
||||||
|
|
||||||
@ManyToOne
|
@ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = false)
|
||||||
@JoinColumn(name = "partnerroleuuid", nullable = false)
|
@JoinColumn(name = "partnerreluuid", nullable = false)
|
||||||
private HsOfficeRelationshipEntity partnerRole;
|
private HsOfficeRelationEntity partnerRel;
|
||||||
|
|
||||||
// TODO: remove, is replaced by partnerRole
|
@ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = true)
|
||||||
@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)
|
|
||||||
@JoinColumn(name = "detailsuuid")
|
@JoinColumn(name = "detailsuuid")
|
||||||
@NotFound(action = NotFoundAction.IGNORE)
|
@NotFound(action = NotFoundAction.IGNORE)
|
||||||
private HsOfficePartnerDetailsEntity details;
|
private HsOfficePartnerDetailsEntity details;
|
||||||
|
|
||||||
|
public String getTaggedPartnerNumber() {
|
||||||
|
return PARTNER_NUMBER_TAG + partnerNumber;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return stringify.apply(this);
|
return stringify.apply(this);
|
||||||
@ -66,6 +88,31 @@ public class HsOfficePartnerEntity implements Stringifyable, HasUuid {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toShortString() {
|
public String toShortString() {
|
||||||
return Optional.ofNullable(person).map(HsOfficePersonEntity::toShortString).orElse("<person=null>");
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
package net.hostsharing.hsadminng.hs.office.partner;
|
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.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.EntityPatcher;
|
||||||
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
|
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
|
||||||
|
|
||||||
import jakarta.persistence.EntityManager;
|
import jakarta.persistence.EntityManager;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
class HsOfficePartnerEntityPatcher implements EntityPatcher<HsOfficePartnerPatchResource> {
|
class HsOfficePartnerEntityPatcher implements EntityPatcher<HsOfficePartnerPatchResource> {
|
||||||
private final EntityManager em;
|
private final EntityManager em;
|
||||||
@ -21,19 +19,15 @@ class HsOfficePartnerEntityPatcher implements EntityPatcher<HsOfficePartnerPatch
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void apply(final HsOfficePartnerPatchResource resource) {
|
public void apply(final HsOfficePartnerPatchResource resource) {
|
||||||
OptionalFromJson.of(resource.getContactUuid()).ifPresent(newValue -> {
|
OptionalFromJson.of(resource.getPartnerRelUuid()).ifPresent(newValue -> {
|
||||||
verifyNotNull(newValue, "contact");
|
verifyNotNull(newValue, "partnerRel");
|
||||||
entity.setContact(em.getReference(HsOfficeContactEntity.class, newValue));
|
entity.setPartnerRel(em.getReference(HsOfficeRelationEntity.class, newValue));
|
||||||
});
|
|
||||||
OptionalFromJson.of(resource.getPersonUuid()).ifPresent(newValue -> {
|
|
||||||
verifyNotNull(newValue, "person");
|
|
||||||
entity.setPerson(em.getReference(HsOfficePersonEntity.class, newValue));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
new HsOfficePartnerDetailsEntityPatcher(em, entity.getDetails()).apply(resource.getDetails());
|
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) {
|
if (newValue == null) {
|
||||||
throw new IllegalArgumentException("property '" + propertyName + "' must not be null");
|
throw new IllegalArgumentException("property '" + propertyName + "' must not be null");
|
||||||
}
|
}
|
||||||
|
@ -11,10 +11,13 @@ public interface HsOfficePartnerRepository extends Repository<HsOfficePartnerEnt
|
|||||||
|
|
||||||
Optional<HsOfficePartnerEntity> findByUuid(UUID id);
|
Optional<HsOfficePartnerEntity> findByUuid(UUID id);
|
||||||
|
|
||||||
|
List<HsOfficePartnerEntity> findAll(); // TODO.impl: move to a repo in test sources
|
||||||
|
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT partner FROM HsOfficePartnerEntity partner
|
SELECT partner FROM HsOfficePartnerEntity partner
|
||||||
JOIN HsOfficeContactEntity contact ON contact.uuid = partner.contact.uuid
|
JOIN HsOfficeRelationEntity rel ON rel.uuid = partner.partnerRel.uuid
|
||||||
JOIN HsOfficePersonEntity person ON person.uuid = partner.person.uuid
|
JOIN HsOfficeContactEntity contact ON contact.uuid = rel.contact.uuid
|
||||||
|
JOIN HsOfficePersonEntity person ON person.uuid = rel.holder.uuid
|
||||||
WHERE :name is null
|
WHERE :name is null
|
||||||
OR partner.details.birthName like concat(cast(:name as text), '%')
|
OR partner.details.birthName like concat(cast(:name as text), '%')
|
||||||
OR contact.label like concat(cast(:name as text), '%')
|
OR contact.label like concat(cast(:name as text), '%')
|
||||||
|
@ -3,14 +3,22 @@ package net.hostsharing.hsadminng.hs.office.person;
|
|||||||
import lombok.*;
|
import lombok.*;
|
||||||
import lombok.experimental.FieldNameConstants;
|
import lombok.experimental.FieldNameConstants;
|
||||||
import net.hostsharing.hsadminng.errors.DisplayName;
|
import net.hostsharing.hsadminng.errors.DisplayName;
|
||||||
import net.hostsharing.hsadminng.persistence.HasUuid;
|
import net.hostsharing.hsadminng.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.Stringify;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.UUID;
|
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;
|
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@ -22,7 +30,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@FieldNameConstants
|
@FieldNameConstants
|
||||||
@DisplayName("Person")
|
@DisplayName("Person")
|
||||||
public class HsOfficePersonEntity implements HasUuid, Stringifyable {
|
public class HsOfficePersonEntity implements RbacObject, Stringifyable {
|
||||||
|
|
||||||
private static Stringify<HsOfficePersonEntity> toString = stringify(HsOfficePersonEntity.class, "person")
|
private static Stringify<HsOfficePersonEntity> toString = stringify(HsOfficePersonEntity.class, "person")
|
||||||
.withProp(Fields.personType, HsOfficePersonEntity::getPersonType)
|
.withProp(Fields.personType, HsOfficePersonEntity::getPersonType)
|
||||||
@ -64,4 +72,28 @@ public class HsOfficePersonEntity implements HasUuid, Stringifyable {
|
|||||||
return personType + " " +
|
return personType + " " +
|
||||||
(!StringUtils.isEmpty(tradeName) ? tradeName : (StringUtils.isEmpty(salutation) ? "" : salutation + " ") + (familyName + ", " + givenName));
|
(!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", "title", "salutation", "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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.context.Context;
|
||||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRepository;
|
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.generated.api.v1.model.*;
|
||||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository;
|
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository;
|
||||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||||
@ -22,7 +22,7 @@ import java.util.function.BiConsumer;
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
|
||||||
public class HsOfficeRelationshipController implements HsOfficeRelationshipsApi {
|
public class HsOfficeRelationController implements HsOfficeRelationsApi {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Context context;
|
private Context context;
|
||||||
@ -31,10 +31,10 @@ public class HsOfficeRelationshipController implements HsOfficeRelationshipsApi
|
|||||||
private Mapper mapper;
|
private Mapper mapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsOfficeRelationshipRepository relationshipRepo;
|
private HsOfficeRelationRepository relationRepo;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsOfficePersonRepository relHolderRepo;
|
private HsOfficePersonRepository holderRepo;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsOfficeContactRepository contactRepo;
|
private HsOfficeContactRepository contactRepo;
|
||||||
@ -44,79 +44,80 @@ public class HsOfficeRelationshipController implements HsOfficeRelationshipsApi
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public ResponseEntity<List<HsOfficeRelationshipResource>> listRelationships(
|
public ResponseEntity<List<HsOfficeRelationResource>> listRelations(
|
||||||
final String currentUser,
|
final String currentUser,
|
||||||
final String assumedRoles,
|
final String assumedRoles,
|
||||||
final UUID personUuid,
|
final UUID personUuid,
|
||||||
final HsOfficeRelationshipTypeResource relationshipType) {
|
final HsOfficeRelationTypeResource relationType) {
|
||||||
context.define(currentUser, assumedRoles);
|
context.define(currentUser, assumedRoles);
|
||||||
|
|
||||||
final var entities = relationshipRepo.findRelationshipRelatedToPersonUuidAndRelationshipType(personUuid,
|
final var entities = relationRepo.findRelationRelatedToPersonUuidAndRelationType(personUuid,
|
||||||
mapper.map(relationshipType, HsOfficeRelationshipType.class));
|
mapper.map(relationType, HsOfficeRelationType.class));
|
||||||
|
|
||||||
final var resources = mapper.mapList(entities, HsOfficeRelationshipResource.class,
|
final var resources = mapper.mapList(entities, HsOfficeRelationResource.class,
|
||||||
RELATIONSHIP_ENTITY_TO_RESOURCE_POSTMAPPER);
|
RELATION_ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||||
return ResponseEntity.ok(resources);
|
return ResponseEntity.ok(resources);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public ResponseEntity<HsOfficeRelationshipResource> addRelationship(
|
public ResponseEntity<HsOfficeRelationResource> addRelation(
|
||||||
final String currentUser,
|
final String currentUser,
|
||||||
final String assumedRoles,
|
final String assumedRoles,
|
||||||
final HsOfficeRelationshipInsertResource body) {
|
final HsOfficeRelationInsertResource body) {
|
||||||
|
|
||||||
context.define(currentUser, assumedRoles);
|
context.define(currentUser, assumedRoles);
|
||||||
|
|
||||||
final var entityToSave = new HsOfficeRelationshipEntity();
|
final var entityToSave = new HsOfficeRelationEntity();
|
||||||
entityToSave.setRelType(HsOfficeRelationshipType.valueOf(body.getRelType()));
|
entityToSave.setType(HsOfficeRelationType.valueOf(body.getType()));
|
||||||
entityToSave.setRelAnchor(relHolderRepo.findByUuid(body.getRelAnchorUuid()).orElseThrow(
|
entityToSave.setMark(body.getMark());
|
||||||
() -> new NoSuchElementException("cannot find relAnchorUuid " + body.getRelAnchorUuid())
|
entityToSave.setAnchor(holderRepo.findByUuid(body.getAnchorUuid()).orElseThrow(
|
||||||
|
() -> new NoSuchElementException("cannot find anchorUuid " + body.getAnchorUuid())
|
||||||
));
|
));
|
||||||
entityToSave.setRelHolder(relHolderRepo.findByUuid(body.getRelHolderUuid()).orElseThrow(
|
entityToSave.setHolder(holderRepo.findByUuid(body.getHolderUuid()).orElseThrow(
|
||||||
() -> new NoSuchElementException("cannot find relHolderUuid " + body.getRelHolderUuid())
|
() -> new NoSuchElementException("cannot find holderUuid " + body.getHolderUuid())
|
||||||
));
|
));
|
||||||
entityToSave.setContact(contactRepo.findByUuid(body.getContactUuid()).orElseThrow(
|
entityToSave.setContact(contactRepo.findByUuid(body.getContactUuid()).orElseThrow(
|
||||||
() -> new NoSuchElementException("cannot find contactUuid " + body.getContactUuid())
|
() -> new NoSuchElementException("cannot find contactUuid " + body.getContactUuid())
|
||||||
));
|
));
|
||||||
|
|
||||||
final var saved = relationshipRepo.save(entityToSave);
|
final var saved = relationRepo.save(entityToSave);
|
||||||
|
|
||||||
final var uri =
|
final var uri =
|
||||||
MvcUriComponentsBuilder.fromController(getClass())
|
MvcUriComponentsBuilder.fromController(getClass())
|
||||||
.path("/api/hs/office/relationships/{id}")
|
.path("/api/hs/office/relations/{id}")
|
||||||
.buildAndExpand(saved.getUuid())
|
.buildAndExpand(saved.getUuid())
|
||||||
.toUri();
|
.toUri();
|
||||||
final var mapped = mapper.map(saved, HsOfficeRelationshipResource.class,
|
final var mapped = mapper.map(saved, HsOfficeRelationResource.class,
|
||||||
RELATIONSHIP_ENTITY_TO_RESOURCE_POSTMAPPER);
|
RELATION_ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||||
return ResponseEntity.created(uri).body(mapped);
|
return ResponseEntity.created(uri).body(mapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public ResponseEntity<HsOfficeRelationshipResource> getRelationshipByUuid(
|
public ResponseEntity<HsOfficeRelationResource> getRelationByUuid(
|
||||||
final String currentUser,
|
final String currentUser,
|
||||||
final String assumedRoles,
|
final String assumedRoles,
|
||||||
final UUID relationshipUuid) {
|
final UUID relationUuid) {
|
||||||
|
|
||||||
context.define(currentUser, assumedRoles);
|
context.define(currentUser, assumedRoles);
|
||||||
|
|
||||||
final var result = relationshipRepo.findByUuid(relationshipUuid);
|
final var result = relationRepo.findByUuid(relationUuid);
|
||||||
if (result.isEmpty()) {
|
if (result.isEmpty()) {
|
||||||
return ResponseEntity.notFound().build();
|
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
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public ResponseEntity<Void> deleteRelationshipByUuid(
|
public ResponseEntity<Void> deleteRelationByUuid(
|
||||||
final String currentUser,
|
final String currentUser,
|
||||||
final String assumedRoles,
|
final String assumedRoles,
|
||||||
final UUID relationshipUuid) {
|
final UUID relationUuid) {
|
||||||
context.define(currentUser, assumedRoles);
|
context.define(currentUser, assumedRoles);
|
||||||
|
|
||||||
final var result = relationshipRepo.deleteByUuid(relationshipUuid);
|
final var result = relationRepo.deleteByUuid(relationUuid);
|
||||||
if (result == 0) {
|
if (result == 0) {
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
}
|
}
|
||||||
@ -126,27 +127,27 @@ public class HsOfficeRelationshipController implements HsOfficeRelationshipsApi
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public ResponseEntity<HsOfficeRelationshipResource> patchRelationship(
|
public ResponseEntity<HsOfficeRelationResource> patchRelation(
|
||||||
final String currentUser,
|
final String currentUser,
|
||||||
final String assumedRoles,
|
final String assumedRoles,
|
||||||
final UUID relationshipUuid,
|
final UUID relationUuid,
|
||||||
final HsOfficeRelationshipPatchResource body) {
|
final HsOfficeRelationPatchResource body) {
|
||||||
|
|
||||||
context.define(currentUser, assumedRoles);
|
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 saved = relationRepo.save(current);
|
||||||
final var mapped = mapper.map(saved, HsOfficeRelationshipResource.class);
|
final var mapped = mapper.map(saved, HsOfficeRelationResource.class);
|
||||||
return ResponseEntity.ok(mapped);
|
return ResponseEntity.ok(mapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
final BiConsumer<HsOfficeRelationshipEntity, HsOfficeRelationshipResource> RELATIONSHIP_ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
final BiConsumer<HsOfficeRelationEntity, HsOfficeRelationResource> RELATION_ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
||||||
resource.setRelAnchor(mapper.map(entity.getRelAnchor(), HsOfficePersonResource.class));
|
resource.setAnchor(mapper.map(entity.getAnchor(), HsOfficePersonResource.class));
|
||||||
resource.setRelHolder(mapper.map(entity.getRelHolder(), HsOfficePersonResource.class));
|
resource.setHolder(mapper.map(entity.getHolder(), HsOfficePersonResource.class));
|
||||||
resource.setContact(mapper.map(entity.getContact(), HsOfficeContactResource.class));
|
resource.setContact(mapper.map(entity.getContact(), HsOfficeContactResource.class));
|
||||||
};
|
};
|
||||||
}
|
}
|
@ -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<HsOfficeRelationEntity> 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<HsOfficeRelationEntity> 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");
|
||||||
|
}
|
||||||
|
}
|
@ -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.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.EntityPatcher;
|
||||||
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
|
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
|
||||||
|
|
||||||
import jakarta.persistence.EntityManager;
|
import jakarta.persistence.EntityManager;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
class HsOfficeRelationshipEntityPatcher implements EntityPatcher<HsOfficeRelationshipPatchResource> {
|
class HsOfficeRelationEntityPatcher implements EntityPatcher<HsOfficeRelationPatchResource> {
|
||||||
|
|
||||||
private final EntityManager em;
|
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.em = em;
|
||||||
this.entity = entity;
|
this.entity = entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void apply(final HsOfficeRelationshipPatchResource resource) {
|
public void apply(final HsOfficeRelationPatchResource resource) {
|
||||||
OptionalFromJson.of(resource.getContactUuid()).ifPresent(newValue -> {
|
OptionalFromJson.of(resource.getContactUuid()).ifPresent(newValue -> {
|
||||||
verifyNotNull(newValue, "contact");
|
verifyNotNull(newValue, "contact");
|
||||||
entity.setContact(em.getReference(HsOfficeContactEntity.class, newValue));
|
entity.setContact(em.getReference(HsOfficeContactEntity.class, newValue));
|
@ -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<HsOfficeRelationEntity, UUID> {
|
||||||
|
|
||||||
|
Optional<HsOfficeRelationEntity> findByUuid(UUID id);
|
||||||
|
|
||||||
|
default List<HsOfficeRelationEntity> 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<HsOfficeRelationEntity> 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<HsOfficeRelationEntity> findRelationRelatedToPersonUuidAndRelationTypeString(@NotNull UUID personUuid, String relationType);
|
||||||
|
|
||||||
|
HsOfficeRelationEntity save(final HsOfficeRelationEntity entity);
|
||||||
|
|
||||||
|
long count();
|
||||||
|
|
||||||
|
int deleteByUuid(UUID uuid);
|
||||||
|
}
|
@ -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,
|
UNKNOWN,
|
||||||
PARTNER,
|
PARTNER,
|
||||||
EX_PARTNER,
|
EX_PARTNER,
|
||||||
REPRESENTATIVE,
|
REPRESENTATIVE,
|
||||||
VIP_CONTACT,
|
VIP_CONTACT,
|
||||||
ACCOUNTING,
|
DEBITOR,
|
||||||
OPERATIONS,
|
OPERATIONS,
|
||||||
SUBSCRIBER
|
SUBSCRIBER
|
||||||
}
|
}
|
@ -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<HsOfficeRelationshipEntity> 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<HsOfficeRelationshipEntity> 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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<HsOfficeRelationshipEntity, UUID> {
|
|
||||||
|
|
||||||
Optional<HsOfficeRelationshipEntity> findByUuid(UUID id);
|
|
||||||
|
|
||||||
default List<HsOfficeRelationshipEntity> 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<HsOfficeRelationshipEntity> 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<HsOfficeRelationshipEntity> findRelationshipRelatedToPersonUuidAndRelationshipTypeString(@NotNull UUID personUuid, String relationshipType);
|
|
||||||
|
|
||||||
HsOfficeRelationshipEntity save(final HsOfficeRelationshipEntity entity);
|
|
||||||
|
|
||||||
long count();
|
|
||||||
|
|
||||||
int deleteByUuid(UUID uuid);
|
|
||||||
}
|
|
@ -14,7 +14,6 @@ import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBui
|
|||||||
|
|
||||||
import jakarta.persistence.EntityManager;
|
import jakarta.persistence.EntityManager;
|
||||||
import jakarta.persistence.PersistenceContext;
|
import jakarta.persistence.PersistenceContext;
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
@ -57,7 +56,7 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
|
|||||||
public ResponseEntity<HsOfficeSepaMandateResource> addSepaMandate(
|
public ResponseEntity<HsOfficeSepaMandateResource> addSepaMandate(
|
||||||
final String currentUser,
|
final String currentUser,
|
||||||
final String assumedRoles,
|
final String assumedRoles,
|
||||||
@Valid final HsOfficeSepaMandateInsertResource body) {
|
final HsOfficeSepaMandateInsertResource body) {
|
||||||
|
|
||||||
context.define(currentUser, assumedRoles);
|
context.define(currentUser, assumedRoles);
|
||||||
|
|
||||||
@ -132,6 +131,7 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
|
|||||||
if (entity.getValidity().hasUpperBound()) {
|
if (entity.getValidity().hasUpperBound()) {
|
||||||
resource.setValidTo(entity.getValidity().upper().minusDays(1));
|
resource.setValidTo(entity.getValidity().upper().minusDays(1));
|
||||||
}
|
}
|
||||||
|
resource.getDebitor().setDebitorNumber(entity.getDebitor().getDebitorNumber());
|
||||||
};
|
};
|
||||||
|
|
||||||
final BiConsumer<HsOfficeSepaMandateInsertResource, HsOfficeSepaMandateEntity> SEPA_MANDATE_RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
|
final BiConsumer<HsOfficeSepaMandateInsertResource, HsOfficeSepaMandateEntity> SEPA_MANDATE_RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
|
||||||
|
@ -1,21 +1,32 @@
|
|||||||
package net.hostsharing.hsadminng.hs.office.sepamandate;
|
package net.hostsharing.hsadminng.hs.office.sepamandate;
|
||||||
|
|
||||||
import com.vladmihalcea.hibernate.type.range.PostgreSQLRangeType;
|
import io.hypersistence.utils.hibernate.type.range.PostgreSQLRangeType;
|
||||||
import com.vladmihalcea.hibernate.type.range.Range;
|
import io.hypersistence.utils.hibernate.type.range.Range;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
import net.hostsharing.hsadminng.errors.DisplayName;
|
import net.hostsharing.hsadminng.errors.DisplayName;
|
||||||
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
|
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
|
||||||
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
|
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
|
||||||
import net.hostsharing.hsadminng.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.Stringify;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||||
import org.hibernate.annotations.Type;
|
import org.hibernate.annotations.Type;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
import java.io.IOException;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.*;
|
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.*;
|
||||||
|
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
|
||||||
|
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL;
|
||||||
|
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.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;
|
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@ -26,14 +37,13 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
|||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@DisplayName("SEPA-Mandate")
|
@DisplayName("SEPA-Mandate")
|
||||||
public class HsOfficeSepaMandateEntity implements Stringifyable, HasUuid {
|
public class HsOfficeSepaMandateEntity implements Stringifyable, RbacObject {
|
||||||
|
|
||||||
private static Stringify<HsOfficeSepaMandateEntity> stringify = stringify(HsOfficeSepaMandateEntity.class)
|
private static Stringify<HsOfficeSepaMandateEntity> stringify = stringify(HsOfficeSepaMandateEntity.class)
|
||||||
.withProp(e -> e.getBankAccount().getIban())
|
.withProp(e -> e.getBankAccount().getIban())
|
||||||
.withProp(HsOfficeSepaMandateEntity::getReference)
|
.withProp(HsOfficeSepaMandateEntity::getReference)
|
||||||
.withProp(HsOfficeSepaMandateEntity::getAgreement)
|
.withProp(HsOfficeSepaMandateEntity::getAgreement)
|
||||||
.withProp(e -> e.getValidity().asString())
|
.withProp(e -> e.getValidity().asString())
|
||||||
.withSeparator(", ")
|
|
||||||
.quotedValues(false);
|
.quotedValues(false);
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@ -84,4 +94,53 @@ public class HsOfficeSepaMandateEntity implements Stringifyable, HasUuid {
|
|||||||
return reference;
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.
|
|
||||||
*
|
|
||||||
* <p>This example code worked with Hibernate 5 (Spring Boot 3.0.x):
|
|
||||||
* <pre><code>
|
|
||||||
* return (UUID[]) em.createNativeQuery("select currentSubjectsUuids() as uuids", UUID[].class).getSingleResult();
|
|
||||||
* </code></pre>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* <p>With Hibernate 6 (Spring Boot 3.1.x), this utility method can be used like such:
|
|
||||||
* <pre><code>
|
|
||||||
* final byte[] result = (byte[]) em.createNativeQuery("select * from currentSubjectsUuids() as uuids", UUID[].class)
|
|
||||||
* .getSingleResult();
|
|
||||||
* return fromPostgresArray(result, UUID.class, UUID::fromString);
|
|
||||||
* </code></pre>
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @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 <T> type of a single element of the Java array
|
|
||||||
*/
|
|
||||||
public static <T> T[] fromPostgresArray(final byte[] pgArray, final Class<T> elementClass, final Function<String, T> 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> T[] newGenericArray(final Class<T> elementClass, final int length) {
|
|
||||||
return (T[]) Array.newInstance(elementClass, length);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,6 +1,6 @@
|
|||||||
package net.hostsharing.hsadminng.mapper;
|
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 lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
package net.hostsharing.hsadminng.persistence;
|
|
||||||
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
public interface HasUuid {
|
|
||||||
UUID getUuid();
|
|
||||||
}
|
|
@ -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<RbacView.RbacGrantDefinition> getInsertGrants() {
|
||||||
|
return rbacDef.getGrantDefs().stream()
|
||||||
|
.filter(g -> g.grantType() == PERM_TO_ROLE)
|
||||||
|
.filter(g -> g.getPermDef().toCreate && g.getPermDef().getPermission() == INSERT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<RbacView.RbacGrantDefinition> getOptionalInsertGrant() {
|
||||||
|
return getInsertGrants()
|
||||||
|
.reduce(singleton());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<RbacView.RbacRoleDefinition> getOptionalInsertSuperRole() {
|
||||||
|
return getInsertGrants()
|
||||||
|
.map(RbacView.RbacGrantDefinition::getSuperRoleDef)
|
||||||
|
.reduce(singleton());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> BinaryOperator<T> singleton() {
|
||||||
|
return (x, y) -> {
|
||||||
|
throw new IllegalStateException("only a single INSERT permission grant allowed");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String toVar(final RbacView.RbacRoleDefinition roleDef) {
|
||||||
|
return uncapitalize(roleDef.getEntityAlias().simpleName()) + capitalize(roleDef.getRole().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 + ")";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
package net.hostsharing.hsadminng.rbac.rbacdef;
|
||||||
|
|
||||||
|
public enum PostgresTriggerReference {
|
||||||
|
NEW, OLD
|
||||||
|
}
|
@ -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("--//");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
package net.hostsharing.hsadminng.rbac.rbacdef;
|
||||||
|
|
||||||
|
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with;
|
||||||
|
|
||||||
|
public class RbacObjectGenerator {
|
||||||
|
|
||||||
|
private final String liquibaseTagPrefix;
|
||||||
|
private final String rawTableName;
|
||||||
|
|
||||||
|
public RbacObjectGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) {
|
||||||
|
this.liquibaseTagPrefix = liquibaseTagPrefix;
|
||||||
|
this.rawTableName = rbacDef.getRootEntityAlias().getRawTableName();
|
||||||
|
}
|
||||||
|
|
||||||
|
void generateTo(final StringWriter plPgSql) {
|
||||||
|
plPgSql.writeLn("""
|
||||||
|
-- ============================================================================
|
||||||
|
--changeset ${liquibaseTagPrefix}-rbac-OBJECT:1 endDelimiter:--//
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
|
call generateRelatedRbacObject('${rawTableName}');
|
||||||
|
--//
|
||||||
|
|
||||||
|
""",
|
||||||
|
with("liquibaseTagPrefix", liquibaseTagPrefix),
|
||||||
|
with("rawTableName", rawTableName));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
package net.hostsharing.hsadminng.rbac.rbacdef;
|
||||||
|
|
||||||
|
|
||||||
|
import static java.util.stream.Collectors.joining;
|
||||||
|
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.indented;
|
||||||
|
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with;
|
||||||
|
|
||||||
|
public class RbacRestrictedViewGenerator {
|
||||||
|
private final RbacView rbacDef;
|
||||||
|
private final String liquibaseTagPrefix;
|
||||||
|
private final String 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));
|
||||||
|
}
|
||||||
|
}
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
1087
src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java
Normal file
1087
src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java
Normal file
File diff suppressed because it is too large
Load Diff
@ -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());
|
||||||
|
}
|
||||||
|
}
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
@ -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<RbacView.RbacGrantDefinition> rbacGrants = new HashSet<>();
|
||||||
|
private final String liquibaseTagPrefix;
|
||||||
|
private final String simpleEntityName;
|
||||||
|
private final String simpleEntityVarName;
|
||||||
|
private final String rawTableName;
|
||||||
|
|
||||||
|
RolesGrantsAndPermissionsGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) {
|
||||||
|
this.rbacDef = rbacDef;
|
||||||
|
this.rbacGrants.addAll(rbacDef.getGrantDefs().stream()
|
||||||
|
.filter(RbacView.RbacGrantDefinition::isToCreate)
|
||||||
|
.collect(toSet()));
|
||||||
|
this.liquibaseTagPrefix = liquibaseTagPrefix;
|
||||||
|
|
||||||
|
simpleEntityVarName = rbacDef.getRootEntityAlias().simpleName();
|
||||||
|
simpleEntityName = capitalize(simpleEntityVarName);
|
||||||
|
rawTableName = rbacDef.getRootEntityAlias().getRawTableName();
|
||||||
|
}
|
||||||
|
|
||||||
|
void generateTo(final StringWriter plPgSql) {
|
||||||
|
generateInsertTrigger(plPgSql);
|
||||||
|
if (hasAnyUpdatableEntityAliases()) {
|
||||||
|
generateUpdateTrigger(plPgSql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateHeader(final StringWriter plPgSql, final String triggerType) {
|
||||||
|
plPgSql.writeLn("""
|
||||||
|
-- ============================================================================
|
||||||
|
--changeset ${liquibaseTagPrefix}-rbac-${triggerType}-trigger:1 endDelimiter:--//
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
|
""",
|
||||||
|
with("liquibaseTagPrefix", liquibaseTagPrefix),
|
||||||
|
with("triggerType", triggerType));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateInsertTriggerFunction(final StringWriter plPgSql) {
|
||||||
|
plPgSql.writeLn("""
|
||||||
|
/*
|
||||||
|
Creates the roles, grants and permission for the AFTER INSERT TRIGGER.
|
||||||
|
*/
|
||||||
|
|
||||||
|
create or replace procedure buildRbacSystemFor${simpleEntityName}(
|
||||||
|
NEW ${rawTableName}
|
||||||
|
)
|
||||||
|
language plpgsql as $$
|
||||||
|
|
||||||
|
declare
|
||||||
|
"""
|
||||||
|
.replace("${simpleEntityName}", simpleEntityName)
|
||||||
|
.replace("${rawTableName}", rawTableName));
|
||||||
|
|
||||||
|
plPgSql.chopEmptyLines();
|
||||||
|
plPgSql.indented(() -> {
|
||||||
|
referencedEntityAliases()
|
||||||
|
.forEach((ea) -> plPgSql.writeLn(entityRefVar(NEW, ea) + " " + ea.getRawTableName() + ";"));
|
||||||
|
});
|
||||||
|
|
||||||
|
plPgSql.writeLn();
|
||||||
|
plPgSql.writeLn("begin");
|
||||||
|
plPgSql.indented(() -> {
|
||||||
|
plPgSql.writeLn("call enterTriggerForObjectUuid(NEW.uuid);");
|
||||||
|
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<RbacView.EntityAlias> referencedEntityAliases() {
|
||||||
|
return rbacDef.getEntityAliases().values().stream()
|
||||||
|
.filter(ea -> !rbacDef.isRootEntityAlias(ea))
|
||||||
|
.filter(ea -> ea.dependsOnColum() != null)
|
||||||
|
.filter(ea -> ea.entityClass() != null)
|
||||||
|
.filter(ea -> ea.fetchSql() != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Stream<RbacView.EntityAlias> updatableEntityAliases() {
|
||||||
|
return referencedEntityAliases()
|
||||||
|
.filter(ea -> rbacDef.getUpdatableColumns().contains(ea.dependsOnColum().column));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateUpdateRolesAndGrantsAfterUpdate(final StringWriter plPgSql) {
|
||||||
|
plPgSql.ensureSingleEmptyLine();
|
||||||
|
|
||||||
|
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<String> arrayElements, final int singleLineLimit) {
|
||||||
|
return arrayElements.size() <= singleLineLimit
|
||||||
|
? String.join(", ", arrayElements)
|
||||||
|
: arrayElements.stream().collect(joining(",\n\t", "\n\t", ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<RbacView.RbacGrantDefinition> findPermissionsGrantsForRole(
|
||||||
|
final RbacView.EntityAlias entityAlias,
|
||||||
|
final RbacView.Role role) {
|
||||||
|
final var roleDef = rbacDef.findRbacRole(entityAlias, role);
|
||||||
|
return rbacGrants.stream()
|
||||||
|
.filter(g -> g.grantType() == PERM_TO_ROLE && g.getSuperRoleDef() == roleDef)
|
||||||
|
.collect(toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<RbacView.RbacGrantDefinition> findGrantsToUserForRole(
|
||||||
|
final RbacView.EntityAlias entityAlias,
|
||||||
|
final RbacView.Role role) {
|
||||||
|
final var roleDef = rbacDef.findRbacRole(entityAlias, role);
|
||||||
|
return rbacGrants.stream()
|
||||||
|
.filter(g -> g.grantType() == ROLE_TO_USER && g.getSubRoleDef() == roleDef)
|
||||||
|
.collect(toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<RbacView.RbacGrantDefinition> findIncomingSuperRolesForRole(
|
||||||
|
final RbacView.EntityAlias entityAlias,
|
||||||
|
final RbacView.Role role) {
|
||||||
|
final var roleDef = rbacDef.findRbacRole(entityAlias, role);
|
||||||
|
return rbacGrants.stream()
|
||||||
|
.filter(g -> g.grantType() == ROLE_TO_ROLE && g.getSubRoleDef() == roleDef)
|
||||||
|
.collect(toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<RbacView.RbacGrantDefinition> findOutgoingSuperRolesForRole(
|
||||||
|
final RbacView.EntityAlias entityAlias,
|
||||||
|
final RbacView.Role role) {
|
||||||
|
final var roleDef = rbacDef.findRbacRole(entityAlias, role);
|
||||||
|
return rbacGrants.stream()
|
||||||
|
.filter(g -> g.grantType() == ROLE_TO_ROLE && g.getSuperRoleDef() == roleDef)
|
||||||
|
.filter(g -> g.getSubRoleDef().getEntityAlias() != entityAlias)
|
||||||
|
.collect(toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateInsertTrigger(final StringWriter plPgSql) {
|
||||||
|
|
||||||
|
generateHeader(plPgSql, "insert");
|
||||||
|
generateInsertTriggerFunction(plPgSql);
|
||||||
|
|
||||||
|
plPgSql.writeLn("""
|
||||||
|
/*
|
||||||
|
AFTER INSERT TRIGGER to create the role+grant structure for a new ${rawTableName} row.
|
||||||
|
*/
|
||||||
|
|
||||||
|
create or replace function insertTriggerFor${simpleEntityName}_tf()
|
||||||
|
returns trigger
|
||||||
|
language plpgsql
|
||||||
|
strict as $$
|
||||||
|
begin
|
||||||
|
call buildRbacSystemFor${simpleEntityName}(NEW);
|
||||||
|
return NEW;
|
||||||
|
end; $$;
|
||||||
|
|
||||||
|
create trigger insertTriggerFor${simpleEntityName}_tg
|
||||||
|
after insert on ${rawTableName}
|
||||||
|
for each row
|
||||||
|
execute procedure insertTriggerFor${simpleEntityName}_tf();
|
||||||
|
"""
|
||||||
|
.replace("${simpleEntityName}", simpleEntityName)
|
||||||
|
.replace("${rawTableName}", rawTableName)
|
||||||
|
);
|
||||||
|
|
||||||
|
generateFooter(plPgSql);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateUpdateTrigger(final StringWriter plPgSql) {
|
||||||
|
|
||||||
|
generateHeader(plPgSql, "update");
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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.
|
@ -1,13 +1,13 @@
|
|||||||
package net.hostsharing.hsadminng.rbac.rbacgrant;
|
package net.hostsharing.hsadminng.rbac.rbacgrant;
|
||||||
|
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.springframework.data.annotation.Immutable;
|
import org.springframework.data.annotation.Immutable;
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.Column;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import jakarta.persistence.Id;
|
import jakarta.persistence.Id;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ import java.util.UUID;
|
|||||||
@Immutable
|
@Immutable
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class RawRbacGrantEntity {
|
public class RawRbacGrantEntity implements Comparable {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
private UUID uuid;
|
private UUID uuid;
|
||||||
@ -64,4 +64,9 @@ public class RawRbacGrantEntity {
|
|||||||
// TODO: remove .distinct() once partner.person + partner.contact are removed
|
// TODO: remove .distinct() once partner.person + partner.contact are removed
|
||||||
return roles.stream().map(RawRbacGrantEntity::toDisplay).sorted().distinct().toList();
|
return roles.stream().map(RawRbacGrantEntity::toDisplay).sorted().distinct().toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(final Object o) {
|
||||||
|
return uuid.compareTo(((RawRbacGrantEntity)o).uuid);
|
||||||
|
}
|
||||||
}
|
}
|
@ -8,4 +8,8 @@ import java.util.UUID;
|
|||||||
public interface RawRbacGrantRepository extends Repository<RawRbacGrantEntity, UUID> {
|
public interface RawRbacGrantRepository extends Repository<RawRbacGrantEntity, UUID> {
|
||||||
|
|
||||||
List<RawRbacGrantEntity> findAll();
|
List<RawRbacGrantEntity> findAll();
|
||||||
|
|
||||||
|
List<RawRbacGrantEntity> findByAscendingUuid(UUID ascendingUuid);
|
||||||
|
|
||||||
|
List<RawRbacGrantEntity> findByDescendantUuid(UUID refUuid);
|
||||||
}
|
}
|
@ -94,4 +94,17 @@ public class RbacGrantController implements RbacGrantsApi {
|
|||||||
|
|
||||||
return ResponseEntity.noContent().build();
|
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<String> 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);
|
||||||
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -59,9 +59,9 @@ public class RbacGrantEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String toDisplay() {
|
public String toDisplay() {
|
||||||
return "{ grant role " + grantedRoleIdName +
|
return "{ grant role:" + grantedRoleIdName +
|
||||||
" to user " + granteeUserName +
|
" to user:" + granteeUserName +
|
||||||
" by role " + grantedByRoleIdName +
|
" by role:" + grantedByRoleIdName +
|
||||||
(assumed ? " and assume" : "") +
|
(assumed ? " and assume" : "") +
|
||||||
" }";
|
" }";
|
||||||
}
|
}
|
||||||
|
@ -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<Include> ALL = EnumSet.allOf(Include.class);
|
||||||
|
public static final EnumSet<Include> ALL_TEST_ENTITY_RELATED = EnumSet.of(USERS, DETAILS, NOT_ASSUMED, TEST_ENTITIES, PERMISSIONS);
|
||||||
|
public static final EnumSet<Include> 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<Include> includes) {
|
||||||
|
final var graph = new LimitedHashSet<RawRbacGrantEntity>();
|
||||||
|
for ( UUID subjectUuid: context.currentSubjectsUuids() ) {
|
||||||
|
traverseGrantsTo(graph, subjectUuid, includes);
|
||||||
|
}
|
||||||
|
return toMermaidFlowchart(graph, includes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void traverseGrantsTo(final Set<RawRbacGrantEntity> graph, final UUID refUuid, final EnumSet<Include> includes) {
|
||||||
|
final var grants = rawGrantRepo.findByAscendingUuid(refUuid);
|
||||||
|
grants.forEach(g -> {
|
||||||
|
if (!includes.contains(PERMISSIONS) && g.getDescendantIdName().startsWith("perm:")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ( !g.getDescendantIdName().startsWith("role:global")) {
|
||||||
|
if (!includes.contains(TEST_ENTITIES) && g.getDescendantIdName().contains(":test_")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!includes.contains(NON_TEST_ENTITIES) && !g.getDescendantIdName().contains(":test_")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
graph.add(g);
|
||||||
|
if (includes.contains(NOT_ASSUMED) || g.isAssumed()) {
|
||||||
|
traverseGrantsTo(graph, g.getDescendantUuid(), includes);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public String allGrantsFrom(final UUID targetObject, final String op, final EnumSet<Include> includes) {
|
||||||
|
final var refUuid = (UUID) em.createNativeQuery("SELECT uuid FROM rbacpermission WHERE objectuuid=:targetObject AND op=:op")
|
||||||
|
.setParameter("targetObject", targetObject)
|
||||||
|
.setParameter("op", op)
|
||||||
|
.getSingleResult();
|
||||||
|
final var graph = new LimitedHashSet<RawRbacGrantEntity>();
|
||||||
|
traverseGrantsFrom(graph, refUuid, includes);
|
||||||
|
return toMermaidFlowchart(graph, includes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void traverseGrantsFrom(final Set<RawRbacGrantEntity> graph, final UUID refUuid, final EnumSet<Include> option) {
|
||||||
|
final var grants = rawGrantRepo.findByDescendantUuid(refUuid);
|
||||||
|
grants.forEach(g -> {
|
||||||
|
if (!option.contains(USERS) && g.getAscendantIdName().startsWith("user:")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
graph.add(g);
|
||||||
|
if (option.contains(NOT_ASSUMED) || g.isAssumed()) {
|
||||||
|
traverseGrantsFrom(graph, g.getAscendingUuid(), option);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private String toMermaidFlowchart(final HashSet<RawRbacGrantEntity> graph, final EnumSet<Include> includes) {
|
||||||
|
final var entities =
|
||||||
|
includes.contains(DETAILS)
|
||||||
|
? graph.stream()
|
||||||
|
.flatMap(g -> Stream.of(
|
||||||
|
new Node(g.getAscendantIdName(), g.getAscendingUuid()),
|
||||||
|
new Node(g.getDescendantIdName(), g.getDescendantUuid()))
|
||||||
|
)
|
||||||
|
.collect(groupingBy(RbacGrantsDiagramService::renderEntityIdName))
|
||||||
|
.entrySet().stream()
|
||||||
|
.map(entity -> "subgraph " + 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<T> extends HashSet<T> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean add(final T t) {
|
||||||
|
if (size() < GRANT_LIMIT ) {
|
||||||
|
return super.add(t);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
record Node(String idName, UUID uuid) {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package net.hostsharing.hsadminng.rbac.rbacobject;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface RbacObject {
|
||||||
|
UUID getUuid();
|
||||||
|
}
|
@ -34,6 +34,6 @@ public class RbacRoleEntity {
|
|||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
private RbacRoleType roleType;
|
private RbacRoleType roleType;
|
||||||
|
|
||||||
@Formula("objectTable||'#'||objectIdName||'.'||roleType")
|
@Formula("objectTable||'#'||objectIdName||':'||roleType")
|
||||||
private String roleName;
|
private String roleName;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
package net.hostsharing.hsadminng.rbac.rbacrole;
|
package net.hostsharing.hsadminng.rbac.rbacrole;
|
||||||
|
|
||||||
public enum RbacRoleType {
|
public enum RbacRoleType {
|
||||||
owner, admin, agent, tenant, guest
|
OWNER, ADMIN, AGENT, TENANT, GUEST, REFERRER
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,8 @@ public interface RbacUserPermission {
|
|||||||
String getRoleName();
|
String getRoleName();
|
||||||
UUID getPermissionUuid();
|
UUID getPermissionUuid();
|
||||||
String getOp();
|
String getOp();
|
||||||
|
String getOpTableName();
|
||||||
String getObjectTable();
|
String getObjectTable();
|
||||||
String getObjectIdName();
|
String getObjectIdName();
|
||||||
UUID getObjectUuid();
|
UUID getObjectUuid();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ public final class Stringify<B> {
|
|||||||
|
|
||||||
private final Class<B> clazz;
|
private final Class<B> clazz;
|
||||||
private final String name;
|
private final String name;
|
||||||
|
private Function<B, ?> idProp;
|
||||||
private final List<Property<B>> props = new ArrayList<>();
|
private final List<Property<B>> props = new ArrayList<>();
|
||||||
private String separator = ", ";
|
private String separator = ", ";
|
||||||
private Boolean quotedValues = null;
|
private Boolean quotedValues = null;
|
||||||
@ -42,6 +43,11 @@ public final class Stringify<B> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Stringify<B> withIdProp(final Function<B, ?> getter) {
|
||||||
|
idProp = getter;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Stringify<B> withProp(final String propName, final Function<B, ?> getter) {
|
public Stringify<B> withProp(final String propName, final Function<B, ?> getter) {
|
||||||
props.add(new Property<>(propName, getter));
|
props.add(new Property<>(propName, getter));
|
||||||
return this;
|
return this;
|
||||||
@ -64,7 +70,9 @@ public final class Stringify<B> {
|
|||||||
})
|
})
|
||||||
.map(propVal -> propName(propVal, "=") + optionallyQuoted(propVal))
|
.map(propVal -> propName(propVal, "=") + optionallyQuoted(propVal))
|
||||||
.collect(Collectors.joining(separator));
|
.collect(Collectors.joining(separator));
|
||||||
return name + "(" + propValues + ")";
|
return idProp != null
|
||||||
|
? name + "(" + idProp.apply(object) + ": " + propValues + ")"
|
||||||
|
: name + "(" + propValues + ")";
|
||||||
}
|
}
|
||||||
|
|
||||||
public Stringify<B> withSeparator(final String separator) {
|
public Stringify<B> withSeparator(final String separator) {
|
||||||
|
@ -10,6 +10,8 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
|
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
|
||||||
|
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import jakarta.persistence.PersistenceContext;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@ -24,6 +26,9 @@ public class TestCustomerController implements TestCustomersApi {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private TestCustomerRepository testCustomerRepository;
|
private TestCustomerRepository testCustomerRepository;
|
||||||
|
|
||||||
|
@PersistenceContext
|
||||||
|
EntityManager em;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public ResponseEntity<List<TestCustomerResource>> listCustomers(
|
public ResponseEntity<List<TestCustomerResource>> listCustomers(
|
||||||
@ -48,7 +53,6 @@ public class TestCustomerController implements TestCustomersApi {
|
|||||||
context.define(currentUser, assumedRoles);
|
context.define(currentUser, assumedRoles);
|
||||||
|
|
||||||
final var saved = testCustomerRepository.save(mapper.map(customer, TestCustomerEntity.class));
|
final var saved = testCustomerRepository.save(mapper.map(customer, TestCustomerEntity.class));
|
||||||
|
|
||||||
final var uri =
|
final var uri =
|
||||||
MvcUriComponentsBuilder.fromController(getClass())
|
MvcUriComponentsBuilder.fromController(getClass())
|
||||||
.path("/api/test/customers/{id}")
|
.path("/api/test/customers/{id}")
|
||||||
|
@ -4,17 +4,27 @@ import lombok.AllArgsConstructor;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.Setter;
|
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 jakarta.persistence.*;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.UUID;
|
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
|
@Entity
|
||||||
@Table(name = "test_customer_rv")
|
@Table(name = "test_customer_rv")
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class TestCustomerEntity {
|
public class TestCustomerEntity implements RbacObject {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue
|
@GeneratedValue
|
||||||
@ -25,4 +35,28 @@ public class TestCustomerEntity {
|
|||||||
|
|
||||||
@Column(name = "adminusername")
|
@Column(name = "adminusername")
|
||||||
private String 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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
@ -4,18 +4,29 @@ import lombok.AllArgsConstructor;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.Setter;
|
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 net.hostsharing.hsadminng.test.cust.TestCustomerEntity;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.UUID;
|
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
|
@Entity
|
||||||
@Table(name = "test_package_rv")
|
@Table(name = "test_package_rv")
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class TestPackageEntity {
|
public class TestPackageEntity implements RbacObject {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue
|
@GeneratedValue
|
||||||
@ -31,4 +42,32 @@ public class TestPackageEntity {
|
|||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
private String description;
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ map:
|
|||||||
null: org.openapitools.jackson.nullable.JsonNullable
|
null: org.openapitools.jackson.nullable.JsonNullable
|
||||||
/api/hs/office/persons/{personUUID}:
|
/api/hs/office/persons/{personUUID}:
|
||||||
null: org.openapitools.jackson.nullable.JsonNullable
|
null: org.openapitools.jackson.nullable.JsonNullable
|
||||||
/api/hs/office/relationships/{relationshipUUID}:
|
/api/hs/office/relations/{relationUUID}:
|
||||||
null: org.openapitools.jackson.nullable.JsonNullable
|
null: org.openapitools.jackson.nullable.JsonNullable
|
||||||
/api/hs/office/bankaccounts/{bankAccountUUID}:
|
/api/hs/office/bankaccounts/{bankAccountUUID}:
|
||||||
null: org.openapitools.jackson.nullable.JsonNullable
|
null: org.openapitools.jackson.nullable.JsonNullable
|
||||||
|
@ -9,6 +9,8 @@ components:
|
|||||||
uuid:
|
uuid:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
|
debitorRel:
|
||||||
|
$ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelation'
|
||||||
debitorNumber:
|
debitorNumber:
|
||||||
type: integer
|
type: integer
|
||||||
format: int32
|
format: int32
|
||||||
@ -21,8 +23,6 @@ components:
|
|||||||
maximum: 99
|
maximum: 99
|
||||||
partner:
|
partner:
|
||||||
$ref: './hs-office-partner-schemas.yaml#/components/schemas/HsOfficePartner'
|
$ref: './hs-office-partner-schemas.yaml#/components/schemas/HsOfficePartner'
|
||||||
billingContact:
|
|
||||||
$ref: './hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContact'
|
|
||||||
billable:
|
billable:
|
||||||
type: boolean
|
type: boolean
|
||||||
vatId:
|
vatId:
|
||||||
@ -43,7 +43,7 @@ components:
|
|||||||
HsOfficeDebitorPatch:
|
HsOfficeDebitorPatch:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
billingContactUuid:
|
debitorRelUuid:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
@ -75,14 +75,11 @@ components:
|
|||||||
HsOfficeDebitorInsert:
|
HsOfficeDebitorInsert:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
partnerUuid:
|
debitorRel:
|
||||||
|
$ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationInsert'
|
||||||
|
debitorRelUuid:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
nullable: false
|
|
||||||
billingContactUuid:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
nullable: false
|
|
||||||
debitorNumberSuffix:
|
debitorNumberSuffix:
|
||||||
type: integer
|
type: integer
|
||||||
format: int8
|
format: int8
|
||||||
@ -105,9 +102,7 @@ components:
|
|||||||
defaultPrefix:
|
defaultPrefix:
|
||||||
type: string
|
type: string
|
||||||
pattern: '^[a-z]{3}$'
|
pattern: '^[a-z]{3}$'
|
||||||
|
|
||||||
required:
|
required:
|
||||||
- partnerUuid
|
- debitorNumberSuffix
|
||||||
- billingContactUuid
|
|
||||||
- defaultPrefix
|
- defaultPrefix
|
||||||
- billable
|
- billable
|
||||||
|
@ -46,10 +46,6 @@ components:
|
|||||||
HsOfficeMembershipPatch:
|
HsOfficeMembershipPatch:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
mainDebitorUuid:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
nullable: true
|
|
||||||
validTo:
|
validTo:
|
||||||
type: string
|
type: string
|
||||||
format: date
|
format: date
|
||||||
@ -69,10 +65,6 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
nullable: false
|
nullable: false
|
||||||
mainDebitorUuid:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
nullable: false
|
|
||||||
memberNumberSuffix:
|
memberNumberSuffix:
|
||||||
type: string
|
type: string
|
||||||
minLength: 2
|
minLength: 2
|
||||||
@ -95,7 +87,6 @@ components:
|
|||||||
required:
|
required:
|
||||||
- partnerUuid
|
- partnerUuid
|
||||||
- memberNumberSuffix
|
- memberNumberSuffix
|
||||||
- mainDebitorUuid
|
|
||||||
- validFrom
|
- validFrom
|
||||||
- membershipFeeBillable
|
- membershipFeeBillable
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
@ -14,10 +14,8 @@ components:
|
|||||||
format: int8
|
format: int8
|
||||||
minimum: 10000
|
minimum: 10000
|
||||||
maximum: 99999
|
maximum: 99999
|
||||||
person:
|
partnerRel:
|
||||||
$ref: './hs-office-person-schemas.yaml#/components/schemas/HsOfficePerson'
|
$ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelation'
|
||||||
contact:
|
|
||||||
$ref: './hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContact'
|
|
||||||
details:
|
details:
|
||||||
$ref: '#/components/schemas/HsOfficePartnerDetails'
|
$ref: '#/components/schemas/HsOfficePartnerDetails'
|
||||||
|
|
||||||
@ -52,11 +50,7 @@ components:
|
|||||||
HsOfficePartnerPatch:
|
HsOfficePartnerPatch:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
personUuid:
|
partnerRelUuid:
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
nullable: true
|
|
||||||
contactUuid:
|
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
@ -96,38 +90,31 @@ components:
|
|||||||
format: int8
|
format: int8
|
||||||
minimum: 10000
|
minimum: 10000
|
||||||
maximum: 99999
|
maximum: 99999
|
||||||
partnerRole:
|
partnerRel:
|
||||||
$ref: '#/components/schemas/HsOfficePartnerRoleInsert'
|
$ref: '#/components/schemas/HsOfficePartnerRelInsert'
|
||||||
personUuid:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
contactUuid:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
details:
|
details:
|
||||||
$ref: '#/components/schemas/HsOfficePartnerDetailsInsert'
|
$ref: '#/components/schemas/HsOfficePartnerDetailsInsert'
|
||||||
required:
|
required:
|
||||||
- partnerNumber
|
- partnerNumber
|
||||||
- personUuid
|
- partnerRel
|
||||||
- contactUuid
|
|
||||||
- details
|
- details
|
||||||
|
|
||||||
HsOfficePartnerRoleInsert:
|
HsOfficePartnerRelInsert:
|
||||||
type: object
|
type: object
|
||||||
nullable: false
|
nullable: false
|
||||||
properties:
|
properties:
|
||||||
relAnchorUuid:
|
anchorUuid:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
relHolderUuid:
|
holderUuid:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
contactUuid:
|
contactUuid:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
required:
|
required:
|
||||||
- relAnchorUuid
|
- anchorUuid
|
||||||
- relHolderUuid
|
- holderUuid
|
||||||
- relContactUuid
|
- relContactUuid
|
||||||
|
|
||||||
HsOfficePartnerDetailsInsert:
|
HsOfficePartnerDetailsInsert:
|
||||||
|
@ -3,37 +3,37 @@ components:
|
|||||||
|
|
||||||
schemas:
|
schemas:
|
||||||
|
|
||||||
HsOfficeRelationshipType:
|
HsOfficeRelationType:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
- UNKNOWN
|
- UNKNOWN
|
||||||
- PARTNER
|
- PARTNER
|
||||||
- EX_PARTNER
|
- EX_PARTNER
|
||||||
- REPRESENTATIVE,
|
- DEBITOR
|
||||||
|
- REPRESENTATIVE
|
||||||
- VIP_CONTACT
|
- VIP_CONTACT
|
||||||
- ACCOUNTING,
|
|
||||||
- OPERATIONS
|
- OPERATIONS
|
||||||
- SUBSCRIBER
|
- SUBSCRIBER
|
||||||
|
|
||||||
HsOfficeRelationship:
|
HsOfficeRelation:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
uuid:
|
uuid:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
relAnchor:
|
anchor:
|
||||||
$ref: './hs-office-person-schemas.yaml#/components/schemas/HsOfficePerson'
|
$ref: './hs-office-person-schemas.yaml#/components/schemas/HsOfficePerson'
|
||||||
relHolder:
|
holder:
|
||||||
$ref: './hs-office-person-schemas.yaml#/components/schemas/HsOfficePerson'
|
$ref: './hs-office-person-schemas.yaml#/components/schemas/HsOfficePerson'
|
||||||
relType:
|
type:
|
||||||
type: string
|
type: string
|
||||||
relMark:
|
mark:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
contact:
|
contact:
|
||||||
$ref: './hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContact'
|
$ref: './hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContact'
|
||||||
|
|
||||||
HsOfficeRelationshipPatch:
|
HsOfficeRelationPatch:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
contactUuid:
|
contactUuid:
|
||||||
@ -41,25 +41,26 @@ components:
|
|||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
|
|
||||||
HsOfficeRelationshipInsert:
|
HsOfficeRelationInsert:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
relAnchorUuid:
|
anchorUuid:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
relHolderUuid:
|
holderUuid:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
relType:
|
type:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
relMark:
|
mark:
|
||||||
type: string
|
type: string
|
||||||
|
nullable: true
|
||||||
contactUuid:
|
contactUuid:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
required:
|
required:
|
||||||
- relAnchorUuid
|
- anchorUuid
|
||||||
- relHolderUuid
|
- holderUuid
|
||||||
- relType
|
- type
|
||||||
- relContactUuid
|
- contactUuid
|
@ -1,25 +1,25 @@
|
|||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- hs-office-relationships
|
- hs-office-relations
|
||||||
description: 'Fetch a single person relationship by its uuid, if visible for the current subject.'
|
description: 'Fetch a single person relation by its uuid, if visible for the current subject.'
|
||||||
operationId: getRelationshipByUuid
|
operationId: getRelationByUuid
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: './auth.yaml#/components/parameters/currentUser'
|
- $ref: './auth.yaml#/components/parameters/currentUser'
|
||||||
- $ref: './auth.yaml#/components/parameters/assumedRoles'
|
- $ref: './auth.yaml#/components/parameters/assumedRoles'
|
||||||
- name: relationshipUUID
|
- name: relationUUID
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
description: UUID of the relationship to fetch.
|
description: UUID of the relation to fetch.
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
content:
|
content:
|
||||||
'application/json':
|
'application/json':
|
||||||
schema:
|
schema:
|
||||||
$ref: './hs-office-relationship-schemas.yaml#/components/schemas/HsOfficeRelationship'
|
$ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelation'
|
||||||
|
|
||||||
"401":
|
"401":
|
||||||
$ref: './error-responses.yaml#/components/responses/Unauthorized'
|
$ref: './error-responses.yaml#/components/responses/Unauthorized'
|
||||||
@ -28,13 +28,13 @@ get:
|
|||||||
|
|
||||||
patch:
|
patch:
|
||||||
tags:
|
tags:
|
||||||
- hs-office-relationships
|
- hs-office-relations
|
||||||
description: 'Updates a single person relationship by its uuid, if permitted for the current subject.'
|
description: 'Updates a single person relation by its uuid, if permitted for the current subject.'
|
||||||
operationId: patchRelationship
|
operationId: patchRelation
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: './auth.yaml#/components/parameters/currentUser'
|
- $ref: './auth.yaml#/components/parameters/currentUser'
|
||||||
- $ref: './auth.yaml#/components/parameters/assumedRoles'
|
- $ref: './auth.yaml#/components/parameters/assumedRoles'
|
||||||
- name: relationshipUUID
|
- name: relationUUID
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
@ -44,14 +44,14 @@ patch:
|
|||||||
content:
|
content:
|
||||||
'application/json':
|
'application/json':
|
||||||
schema:
|
schema:
|
||||||
$ref: './hs-office-relationship-schemas.yaml#/components/schemas/HsOfficeRelationshipPatch'
|
$ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationPatch'
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
content:
|
content:
|
||||||
'application/json':
|
'application/json':
|
||||||
schema:
|
schema:
|
||||||
$ref: './hs-office-relationship-schemas.yaml#/components/schemas/HsOfficeRelationship'
|
$ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelation'
|
||||||
"401":
|
"401":
|
||||||
$ref: './error-responses.yaml#/components/responses/Unauthorized'
|
$ref: './error-responses.yaml#/components/responses/Unauthorized'
|
||||||
"403":
|
"403":
|
||||||
@ -59,19 +59,19 @@ patch:
|
|||||||
|
|
||||||
delete:
|
delete:
|
||||||
tags:
|
tags:
|
||||||
- hs-office-relationships
|
- hs-office-relations
|
||||||
description: 'Delete a single person relationship by its uuid, if permitted for the current subject.'
|
description: 'Delete a single person relation by its uuid, if permitted for the current subject.'
|
||||||
operationId: deleteRelationshipByUuid
|
operationId: deleteRelationByUuid
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: './auth.yaml#/components/parameters/currentUser'
|
- $ref: './auth.yaml#/components/parameters/currentUser'
|
||||||
- $ref: './auth.yaml#/components/parameters/assumedRoles'
|
- $ref: './auth.yaml#/components/parameters/assumedRoles'
|
||||||
- name: relationshipUUID
|
- name: relationUUID
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
description: UUID of the relationship to delete.
|
description: UUID of the relation to delete.
|
||||||
responses:
|
responses:
|
||||||
"204":
|
"204":
|
||||||
description: No Content
|
description: No Content
|
@ -1,9 +1,9 @@
|
|||||||
get:
|
get:
|
||||||
summary: Returns a list of (optionally filtered) person relationships for a given person.
|
summary: Returns a list of (optionally filtered) person relations 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.
|
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:
|
tags:
|
||||||
- hs-office-relationships
|
- hs-office-relations
|
||||||
operationId: listRelationships
|
operationId: listRelations
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: './auth.yaml#/components/parameters/currentUser'
|
- $ref: './auth.yaml#/components/parameters/currentUser'
|
||||||
- $ref: './auth.yaml#/components/parameters/assumedRoles'
|
- $ref: './auth.yaml#/components/parameters/assumedRoles'
|
||||||
@ -13,13 +13,13 @@ get:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
description: Prefix of name properties from relHolder or contact to filter the results.
|
description: Prefix of name properties from holder or contact to filter the results.
|
||||||
- name: relationshipType
|
- name: relationType
|
||||||
in: query
|
in: query
|
||||||
required: false
|
required: false
|
||||||
schema:
|
schema:
|
||||||
$ref: './hs-office-relationship-schemas.yaml#/components/schemas/HsOfficeRelationshipType'
|
$ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationType'
|
||||||
description: Prefix of name properties from relHolder or contact to filter the results.
|
description: Prefix of name properties from holder or contact to filter the results.
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
@ -28,17 +28,17 @@ get:
|
|||||||
schema:
|
schema:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: './hs-office-relationship-schemas.yaml#/components/schemas/HsOfficeRelationship'
|
$ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelation'
|
||||||
"401":
|
"401":
|
||||||
$ref: './error-responses.yaml#/components/responses/Unauthorized'
|
$ref: './error-responses.yaml#/components/responses/Unauthorized'
|
||||||
"403":
|
"403":
|
||||||
$ref: './error-responses.yaml#/components/responses/Forbidden'
|
$ref: './error-responses.yaml#/components/responses/Forbidden'
|
||||||
|
|
||||||
post:
|
post:
|
||||||
summary: Adds a new person relationship.
|
summary: Adds a new person relation.
|
||||||
tags:
|
tags:
|
||||||
- hs-office-relationships
|
- hs-office-relations
|
||||||
operationId: addRelationship
|
operationId: addRelation
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: './auth.yaml#/components/parameters/currentUser'
|
- $ref: './auth.yaml#/components/parameters/currentUser'
|
||||||
- $ref: './auth.yaml#/components/parameters/assumedRoles'
|
- $ref: './auth.yaml#/components/parameters/assumedRoles'
|
||||||
@ -46,7 +46,7 @@ post:
|
|||||||
content:
|
content:
|
||||||
'application/json':
|
'application/json':
|
||||||
schema:
|
schema:
|
||||||
$ref: './hs-office-relationship-schemas.yaml#/components/schemas/HsOfficeRelationshipInsert'
|
$ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationInsert'
|
||||||
required: true
|
required: true
|
||||||
responses:
|
responses:
|
||||||
"201":
|
"201":
|
||||||
@ -54,7 +54,7 @@ post:
|
|||||||
content:
|
content:
|
||||||
'application/json':
|
'application/json':
|
||||||
schema:
|
schema:
|
||||||
$ref: './hs-office-relationship-schemas.yaml#/components/schemas/HsOfficeRelationship'
|
$ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelation'
|
||||||
"401":
|
"401":
|
||||||
$ref: './error-responses.yaml#/components/responses/Unauthorized'
|
$ref: './error-responses.yaml#/components/responses/Unauthorized'
|
||||||
"403":
|
"403":
|
@ -35,13 +35,13 @@ paths:
|
|||||||
$ref: "./hs-office-persons-with-uuid.yaml"
|
$ref: "./hs-office-persons-with-uuid.yaml"
|
||||||
|
|
||||||
|
|
||||||
# Relationships
|
# Relations
|
||||||
|
|
||||||
/api/hs/office/relationships:
|
/api/hs/office/relations:
|
||||||
$ref: "./hs-office-relationships.yaml"
|
$ref: "./hs-office-relations.yaml"
|
||||||
|
|
||||||
/api/hs/office/relationships/{relationshipUUID}:
|
/api/hs/office/relations/{relationUUID}:
|
||||||
$ref: "./hs-office-relationships-with-uuid.yaml"
|
$ref: "./hs-office-relations-with-uuid.yaml"
|
||||||
|
|
||||||
|
|
||||||
# BankAccounts
|
# BankAccounts
|
||||||
|
@ -19,8 +19,11 @@ components:
|
|||||||
roleType:
|
roleType:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
- owner
|
- OWNER
|
||||||
- admin
|
- ADMIN
|
||||||
- tenant
|
- AGENT
|
||||||
|
- TENANT
|
||||||
|
- REFERRER
|
||||||
|
- GUEST
|
||||||
roleName:
|
roleName:
|
||||||
type: string
|
type: string
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
-- NUMERIC-HASH-FUNCTIONS
|
-- NUMERIC-HASH-FUNCTIONS
|
||||||
--changeset hash:1 endDelimiter:--//
|
--changeset numeric-hash-functions:1 endDelimiter:--//
|
||||||
-- ----------------------------------------------------------------------------
|
-- ----------------------------------------------------------------------------
|
||||||
|
|
||||||
create function bigIntHash(text) returns bigint as $$
|
create function bigIntHash(text) returns bigint as $$
|
@ -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; $$
|
||||||
|
--//
|
@ -10,10 +10,10 @@
|
|||||||
This function will be overwritten by later changesets.
|
This function will be overwritten by later changesets.
|
||||||
*/
|
*/
|
||||||
create procedure contextDefined(
|
create procedure contextDefined(
|
||||||
currentTask varchar,
|
currentTask varchar(127),
|
||||||
currentRequest varchar,
|
currentRequest text,
|
||||||
currentUser varchar,
|
currentUser varchar(63),
|
||||||
assumedRoles varchar
|
assumedRoles varchar(1023)
|
||||||
)
|
)
|
||||||
language plpgsql as $$
|
language plpgsql as $$
|
||||||
begin
|
begin
|
||||||
@ -23,22 +23,27 @@ end; $$;
|
|||||||
Defines the transaction context.
|
Defines the transaction context.
|
||||||
*/
|
*/
|
||||||
create or replace procedure defineContext(
|
create or replace procedure defineContext(
|
||||||
currentTask varchar,
|
currentTask varchar(127),
|
||||||
currentRequest varchar = null,
|
currentRequest text = null,
|
||||||
currentUser varchar = null,
|
currentUser varchar(63) = null,
|
||||||
assumedRoles varchar = null
|
assumedRoles varchar(1023) = null
|
||||||
)
|
)
|
||||||
language plpgsql as $$
|
language plpgsql as $$
|
||||||
begin
|
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);
|
execute format('set local hsadminng.currentTask to %L', currentTask);
|
||||||
|
|
||||||
currentRequest := coalesce(currentRequest, '');
|
currentRequest := coalesce(currentRequest, '');
|
||||||
execute format('set local hsadminng.currentRequest to %L', currentRequest);
|
execute format('set local hsadminng.currentRequest to %L', currentRequest);
|
||||||
|
|
||||||
currentUser := coalesce(currentUser, '');
|
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);
|
execute format('set local hsadminng.currentUser to %L', currentUser);
|
||||||
|
|
||||||
assumedRoles := coalesce(assumedRoles, '');
|
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);
|
execute format('set local hsadminng.assumedRoles to %L', assumedRoles);
|
||||||
|
|
||||||
call contextDefined(currentTask, currentRequest, currentUser, assumedRoles);
|
call contextDefined(currentTask, currentRequest, currentUser, assumedRoles);
|
||||||
@ -54,11 +59,11 @@ end; $$;
|
|||||||
Raises exception if not set.
|
Raises exception if not set.
|
||||||
*/
|
*/
|
||||||
create or replace function currentTask()
|
create or replace function currentTask()
|
||||||
returns varchar(96)
|
returns varchar(127)
|
||||||
stable -- leakproof
|
stable -- leakproof
|
||||||
language plpgsql as $$
|
language plpgsql as $$
|
||||||
declare
|
declare
|
||||||
currentTask varchar(96);
|
currentTask varchar(127);
|
||||||
begin
|
begin
|
||||||
begin
|
begin
|
||||||
currentTask := current_setting('hsadminng.currentTask');
|
currentTask := current_setting('hsadminng.currentTask');
|
||||||
@ -82,11 +87,11 @@ end; $$;
|
|||||||
Raises exception if not set.
|
Raises exception if not set.
|
||||||
*/
|
*/
|
||||||
create or replace function currentRequest()
|
create or replace function currentRequest()
|
||||||
returns varchar(512)
|
returns text
|
||||||
stable -- leakproof
|
stable -- leakproof
|
||||||
language plpgsql as $$
|
language plpgsql as $$
|
||||||
declare
|
declare
|
||||||
currentRequest varchar(512);
|
currentRequest text;
|
||||||
begin
|
begin
|
||||||
begin
|
begin
|
||||||
currentRequest := current_setting('hsadminng.currentRequest');
|
currentRequest := current_setting('hsadminng.currentRequest');
|
||||||
@ -130,22 +135,11 @@ end; $$;
|
|||||||
or empty array, if not set.
|
or empty array, if not set.
|
||||||
*/
|
*/
|
||||||
create or replace function assumedRoles()
|
create or replace function assumedRoles()
|
||||||
returns varchar(63)[]
|
returns varchar(1023)[]
|
||||||
stable -- leakproof
|
stable -- leakproof
|
||||||
language plpgsql as $$
|
language plpgsql as $$
|
||||||
declare
|
|
||||||
currentSubject varchar(63);
|
|
||||||
begin
|
begin
|
||||||
begin
|
return string_to_array(current_setting('hsadminng.assumedRoles', true), ';');
|
||||||
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, ';');
|
|
||||||
end; $$;
|
end; $$;
|
||||||
|
|
||||||
create or replace function cleanIdentifier(rawIdentifier varchar)
|
create or replace function cleanIdentifier(rawIdentifier varchar)
|
||||||
@ -155,7 +149,7 @@ create or replace function cleanIdentifier(rawIdentifier varchar)
|
|||||||
declare
|
declare
|
||||||
cleanIdentifier varchar;
|
cleanIdentifier varchar;
|
||||||
begin
|
begin
|
||||||
cleanIdentifier := regexp_replace(rawIdentifier, '[^A-Za-z0-9\-._:]+', '', 'g');
|
cleanIdentifier := regexp_replace(rawIdentifier, '[^A-Za-z0-9\-._]+', '', 'g');
|
||||||
return cleanIdentifier;
|
return cleanIdentifier;
|
||||||
end; $$;
|
end; $$;
|
||||||
|
|
||||||
@ -213,17 +207,17 @@ begin
|
|||||||
end ; $$;
|
end ; $$;
|
||||||
|
|
||||||
create or replace function currentSubjects()
|
create or replace function currentSubjects()
|
||||||
returns varchar(63)[]
|
returns varchar(1023)[]
|
||||||
stable -- leakproof
|
stable -- leakproof
|
||||||
language plpgsql as $$
|
language plpgsql as $$
|
||||||
declare
|
declare
|
||||||
assumedRoles varchar(63)[];
|
assumedRoles varchar(1023)[];
|
||||||
begin
|
begin
|
||||||
assumedRoles := assumedRoles();
|
assumedRoles := assumedRoles();
|
||||||
if array_length(assumedRoles, 1) > 0 then
|
if array_length(assumedRoles, 1) > 0 then
|
||||||
return assumedRoles();
|
return assumedRoles;
|
||||||
else
|
else
|
||||||
return array [currentUser()]::varchar(63)[];
|
return array [currentUser()]::varchar(1023)[];
|
||||||
end if;
|
end if;
|
||||||
end; $$;
|
end; $$;
|
||||||
|
|
@ -27,9 +27,9 @@ create table tx_context
|
|||||||
txId bigint not null,
|
txId bigint not null,
|
||||||
txTimestamp timestamp not null,
|
txTimestamp timestamp not null,
|
||||||
currentUser varchar(63) not null, -- not the uuid, because users can be deleted
|
currentUser varchar(63) not null, -- not the uuid, because users can be deleted
|
||||||
assumedRoles varchar not null, -- not the uuids, because roles can be deleted
|
assumedRoles varchar(1023) not null, -- not the uuids, because roles can be deleted
|
||||||
currentTask varchar(96) not null,
|
currentTask varchar(127) not null,
|
||||||
currentRequest varchar(512) not null
|
currentRequest text not null
|
||||||
);
|
);
|
||||||
|
|
||||||
create index on tx_context using brin (txTimestamp);
|
create index on tx_context using brin (txTimestamp);
|
@ -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; $$;
|
|
||||||
--//
|
|
||||||
|
|
@ -86,29 +86,6 @@ create or replace function findRbacUserId(userName varchar)
|
|||||||
language sql as $$
|
language sql as $$
|
||||||
select uuid from RbacUser where name = userName
|
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
|
create table RbacRole
|
||||||
(
|
(
|
||||||
@ -203,15 +180,33 @@ create type RbacRoleDescriptor as
|
|||||||
(
|
(
|
||||||
objectTable varchar(63), -- for human readability and easier debugging
|
objectTable varchar(63), -- for human readability and easier debugging
|
||||||
objectUuid uuid,
|
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 RbacRoleDescriptor
|
||||||
returns null on null input
|
returns null on null input
|
||||||
stable -- leakproof
|
stable -- leakproof
|
||||||
language sql as $$
|
language sql as $$
|
||||||
select objectTable, objectUuid, roleType::RbacRoleType;
|
select objectTable, objectUuid, roleType::RbacRoleType, assumed;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
create or replace function createRole(roleDescriptor RbacRoleDescriptor)
|
create or replace function createRole(roleDescriptor RbacRoleDescriptor)
|
||||||
@ -254,7 +249,7 @@ declare
|
|||||||
roleUuid uuid;
|
roleUuid uuid;
|
||||||
begin
|
begin
|
||||||
-- TODO.refact: extract function toRbacRoleDescriptor(roleIdName varchar) + find other occurrences
|
-- 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);
|
objectTableFromRoleIdName = split_part(roleParts, '#', 1);
|
||||||
objectNameFromRoleIdName = split_part(roleParts, '#', 2);
|
objectNameFromRoleIdName = split_part(roleParts, '#', 2);
|
||||||
roleTypeFromRoleIdName = split_part(roleParts, '#', 3);
|
roleTypeFromRoleIdName = split_part(roleParts, '#', 3);
|
||||||
@ -275,22 +270,18 @@ create or replace function findRoleId(roleDescriptor RbacRoleDescriptor)
|
|||||||
select uuid from RbacRole where objectUuid = roleDescriptor.objectUuid and roleType = roleDescriptor.roleType;
|
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 uuid
|
||||||
returns null on null input
|
|
||||||
language plpgsql as $$
|
language plpgsql as $$
|
||||||
declare
|
declare
|
||||||
roleUuid uuid;
|
roleUuid uuid;
|
||||||
begin
|
begin
|
||||||
roleUuid = findRoleId(roleDescriptor);
|
assert roleDescriptor is not null, 'roleDescriptor must not be null';
|
||||||
|
|
||||||
|
roleUuid := findRoleId(roleDescriptor);
|
||||||
if (roleUuid is null) then
|
if (roleUuid is null) then
|
||||||
if (whenNotExists = 'fail') then
|
|
||||||
raise exception 'RbacRole "%#%.%" not found', roleDescriptor.objectTable, roleDescriptor.objectUuid, roleDescriptor.roleType;
|
raise exception 'RbacRole "%#%.%" not found', roleDescriptor.objectTable, roleDescriptor.objectUuid, roleDescriptor.roleType;
|
||||||
end if;
|
end if;
|
||||||
if (whenNotExists = 'create') then
|
|
||||||
roleUuid = createRole(roleDescriptor);
|
|
||||||
end if;
|
|
||||||
end if;
|
|
||||||
return roleUuid;
|
return roleUuid;
|
||||||
end;
|
end;
|
||||||
$$;
|
$$;
|
||||||
@ -365,13 +356,14 @@ 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 (
|
check (
|
||||||
VALUE = '*'
|
VALUE = 'DELETE'
|
||||||
or VALUE = 'delete'
|
or VALUE = 'UPDATE'
|
||||||
or VALUE = 'edit'
|
or VALUE = 'SELECT'
|
||||||
or VALUE = 'view'
|
or VALUE = 'INSERT'
|
||||||
or VALUE = 'assume'
|
or VALUE = 'ASSUME'
|
||||||
|
-- TODO: all values below are deprecated, use insert with table
|
||||||
or VALUE ~ '^add-[a-z]+$'
|
or VALUE ~ '^add-[a-z]+$'
|
||||||
or VALUE ~ '^new-[a-z-]+$'
|
or VALUE ~ '^new-[a-z-]+$'
|
||||||
);
|
);
|
||||||
@ -381,22 +373,51 @@ create table RbacPermission
|
|||||||
uuid uuid primary key references RbacReference (uuid) on delete cascade,
|
uuid uuid primary key references RbacReference (uuid) on delete cascade,
|
||||||
objectUuid uuid not null references RbacObject,
|
objectUuid uuid not null references RbacObject,
|
||||||
op RbacOp not null,
|
op RbacOp not null,
|
||||||
unique (objectUuid, op)
|
opTableName varchar(60)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ALTER TABLE RbacPermission
|
||||||
|
ADD CONSTRAINT RbacPermission_uc UNIQUE NULLS NOT DISTINCT (objectUuid, op, opTableName);
|
||||||
|
|
||||||
call create_journal('RbacPermission');
|
call create_journal('RbacPermission');
|
||||||
|
|
||||||
create or replace function permissionExists(forObjectUuid uuid, forOp RbacOp)
|
create or replace function createPermission(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null)
|
||||||
returns bool
|
returns uuid
|
||||||
language sql as $$
|
language plpgsql as $$
|
||||||
select exists(
|
declare
|
||||||
select op
|
permissionUuid uuid;
|
||||||
from RbacPermission p
|
begin
|
||||||
where p.objectUuid = forObjectUuid
|
if (forObjectUuid is null) then
|
||||||
and p.op in ('*', forOp)
|
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[])
|
create or replace function createPermissions(forObjectUuid uuid, permitOps RbacOp[])
|
||||||
returns uuid[]
|
returns uuid[]
|
||||||
language plpgsql as $$
|
language plpgsql as $$
|
||||||
@ -407,9 +428,6 @@ begin
|
|||||||
if (forObjectUuid is null) then
|
if (forObjectUuid is null) then
|
||||||
raise exception 'forObjectUuid must not be null';
|
raise exception 'forObjectUuid must not be null';
|
||||||
end if;
|
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)
|
for i in array_lower(permitOps, 1)..array_upper(permitOps, 1)
|
||||||
loop
|
loop
|
||||||
@ -430,7 +448,19 @@ begin
|
|||||||
end;
|
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 uuid
|
||||||
returns null on null input
|
returns null on null input
|
||||||
stable -- leakproof
|
stable -- leakproof
|
||||||
@ -439,25 +469,46 @@ select uuid
|
|||||||
from RbacPermission p
|
from RbacPermission p
|
||||||
where p.objectUuid = forObjectUuid
|
where p.objectUuid = forObjectUuid
|
||||||
and p.op = forOp
|
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 uuid
|
||||||
returns null on null input
|
|
||||||
stable -- leakproof
|
stable -- leakproof
|
||||||
language plpgsql as $$
|
language plpgsql as $$
|
||||||
declare
|
declare
|
||||||
permissionId uuid;
|
permissionUuid uuid;
|
||||||
begin
|
begin
|
||||||
permissionId := findPermissionId(forObjectUuid, forOp);
|
select uuid into permissionUuid
|
||||||
if permissionId is null and forOp <> '*' then
|
from RbacPermission p
|
||||||
permissionId := findPermissionId(forObjectUuid, '*');
|
where p.objectUuid = forObjectUuid
|
||||||
end if;
|
and p.op = forOp
|
||||||
return permissionId;
|
and forOpTableName is null or p.opTableName = forOpTableName;
|
||||||
end $$;
|
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:--//
|
--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)
|
create or replace function hasGlobalRoleGranted(userUuid uuid)
|
||||||
returns bool
|
returns bool
|
||||||
stable -- leakproof
|
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[])
|
create or replace procedure grantPermissionsToRole(roleUuid uuid, permissionIds uuid[])
|
||||||
language plpgsql as $$
|
language plpgsql as $$
|
||||||
begin
|
begin
|
||||||
@ -591,7 +675,7 @@ begin
|
|||||||
perform assertReferenceType('subRoleId (descendant)', subRoleId, 'RbacRole');
|
perform assertReferenceType('subRoleId (descendant)', subRoleId, 'RbacRole');
|
||||||
|
|
||||||
if isGranted(subRoleId, superRoleId) then
|
if isGranted(subRoleId, superRoleId) then
|
||||||
raise exception '[400] Cyclic role grant detected between % and %', subRoleId, superRoleId;
|
call raiseDuplicateRoleGrantException(subRoleId, superRoleId);
|
||||||
end if;
|
end if;
|
||||||
|
|
||||||
insert
|
insert
|
||||||
@ -607,6 +691,11 @@ declare
|
|||||||
superRoleId uuid;
|
superRoleId uuid;
|
||||||
subRoleId uuid;
|
subRoleId uuid;
|
||||||
begin
|
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);
|
superRoleId := findRoleId(superRole);
|
||||||
subRoleId := findRoleId(subRole);
|
subRoleId := findRoleId(subRole);
|
||||||
|
|
||||||
@ -614,7 +703,7 @@ begin
|
|||||||
perform assertReferenceType('subRoleId (descendant)', subRoleId, 'RbacRole');
|
perform assertReferenceType('subRoleId (descendant)', subRoleId, 'RbacRole');
|
||||||
|
|
||||||
if isGranted(subRoleId, superRoleId) then
|
if isGranted(subRoleId, superRoleId) then
|
||||||
raise exception '[400] Cyclic role grant detected between % and %', subRoleId, superRoleId;
|
call raiseDuplicateRoleGrantException(subRoleId, superRoleId);
|
||||||
end if;
|
end if;
|
||||||
|
|
||||||
insert
|
insert
|
||||||
@ -629,6 +718,7 @@ declare
|
|||||||
superRoleId uuid;
|
superRoleId uuid;
|
||||||
subRoleId uuid;
|
subRoleId uuid;
|
||||||
begin
|
begin
|
||||||
|
if ( superRoleId is null ) then return; end if;
|
||||||
superRoleId := findRoleId(superRole);
|
superRoleId := findRoleId(superRole);
|
||||||
if ( subRoleId is null ) then return; end if;
|
if ( subRoleId is null ) then return; end if;
|
||||||
subRoleId := findRoleId(subRole);
|
subRoleId := findRoleId(subRole);
|
||||||
@ -637,7 +727,7 @@ begin
|
|||||||
perform assertReferenceType('subRoleId (descendant)', subRoleId, 'RbacRole');
|
perform assertReferenceType('subRoleId (descendant)', subRoleId, 'RbacRole');
|
||||||
|
|
||||||
if isGranted(subRoleId, superRoleId) then
|
if isGranted(subRoleId, superRoleId) then
|
||||||
raise exception '[400] Cyclic role grant detected between % and %', subRoleId, superRoleId;
|
call raiseDuplicateRoleGrantException(subRoleId, superRoleId);
|
||||||
end if;
|
end if;
|
||||||
|
|
||||||
insert
|
insert
|
||||||
@ -661,11 +751,39 @@ begin
|
|||||||
if (isGranted(superRoleId, subRoleId)) then
|
if (isGranted(superRoleId, subRoleId)) then
|
||||||
delete from RbacGrants where ascendantUuid = superRoleId and descendantUuid = subRoleId;
|
delete from RbacGrants where ascendantUuid = superRoleId and descendantUuid = subRoleId;
|
||||||
else
|
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;
|
subRole, subRoleId, superRole, superRoleId;
|
||||||
end if;
|
end if;
|
||||||
end; $$;
|
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:--//
|
--changeset rbac-base-QUERY-ACCESSIBLE-OBJECT-UUIDS:1 endDelimiter:--//
|
||||||
-- ----------------------------------------------------------------------------
|
-- ----------------------------------------------------------------------------
|
||||||
@ -697,7 +815,7 @@ begin
|
|||||||
select descendantUuid
|
select descendantUuid
|
||||||
from grants) as granted
|
from grants) as granted
|
||||||
join RbacPermission perm
|
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
|
join RbacObject obj on obj.uuid = perm.objectUuid and obj.objectTable = forObjectTable
|
||||||
limit maxObjects + 1;
|
limit maxObjects + 1;
|
||||||
|
|
||||||
@ -789,6 +907,5 @@ do $$
|
|||||||
create role restricted;
|
create role restricted;
|
||||||
grant all privileges on all tables in schema public to restricted;
|
grant all privileges on all tables in schema public to restricted;
|
||||||
end if;
|
end if;
|
||||||
end $$
|
end $$;
|
||||||
--//
|
--//
|
||||||
|
|
@ -30,24 +30,35 @@ begin
|
|||||||
insert
|
insert
|
||||||
into RbacGrants (grantedByRoleUuid, ascendantUuid, descendantUuid, assumed)
|
into RbacGrants (grantedByRoleUuid, ascendantUuid, descendantUuid, assumed)
|
||||||
values (grantedByRoleUuid, userUuid, roleUuid, doAssume);
|
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?
|
-- Most powerful or latest grant wins? What about managed?
|
||||||
-- on conflict do nothing; -- allow granting multiple times
|
-- on conflict do nothing; -- allow granting multiple times
|
||||||
end; $$;
|
end; $$;
|
||||||
|
|
||||||
create or replace procedure grantRoleToUser(grantedByRoleUuid uuid, grantedRoleUuid uuid, userUuid uuid, doAssume boolean = true)
|
create or replace procedure grantRoleToUser(grantedByRoleUuid uuid, grantedRoleUuid uuid, userUuid uuid, doAssume boolean = true)
|
||||||
language plpgsql as $$
|
language plpgsql as $$
|
||||||
|
declare
|
||||||
|
grantedByRoleIdName text;
|
||||||
|
grantedRoleIdName text;
|
||||||
begin
|
begin
|
||||||
perform assertReferenceType('grantingRoleUuid', grantedByRoleUuid, 'RbacRole');
|
perform assertReferenceType('grantingRoleUuid', grantedByRoleUuid, 'RbacRole');
|
||||||
perform assertReferenceType('grantedRoleUuid (descendant)', grantedRoleUuid, 'RbacRole');
|
perform assertReferenceType('grantedRoleUuid (descendant)', grantedRoleUuid, 'RbacRole');
|
||||||
perform assertReferenceType('userUuid (ascendant)', userUuid, 'RbacUser');
|
perform assertReferenceType('userUuid (ascendant)', userUuid, 'RbacUser');
|
||||||
|
|
||||||
if NOT isGranted(currentSubjectsUuids(), grantedByRoleUuid) then
|
assert grantedByRoleUuid is not null, 'grantedByRoleUuid must not be null';
|
||||||
raise exception '[403] Access to granted-by-role % forbidden for %', grantedByRoleUuid, currentSubjects();
|
assert grantedRoleUuid is not null, 'grantedRoleUuid must not be null';
|
||||||
end if;
|
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
|
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;
|
end if;
|
||||||
|
|
||||||
insert
|
insert
|
||||||
@ -99,4 +110,17 @@ begin
|
|||||||
where g.ascendantUuid = userUuid and g.descendantUuid = grantedRoleUuid
|
where g.ascendantUuid = userUuid and g.descendantUuid = grantedRoleUuid
|
||||||
and g.grantedByRoleUuid = revokeRoleFromUser.grantedByRoleUuid;
|
and g.grantedByRoleUuid = revokeRoleFromUser.grantedByRoleUuid;
|
||||||
end; $$;
|
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; $$;
|
||||||
|
--//
|
@ -50,20 +50,23 @@ begin
|
|||||||
|
|
||||||
foreach roleName in array string_to_array(assumedRoles, ';')
|
foreach roleName in array string_to_array(assumedRoles, ';')
|
||||||
loop
|
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);
|
objectTableToAssume = split_part(roleNameParts, '#', 1);
|
||||||
objectNameToAssume = split_part(roleNameParts, '#', 2);
|
objectNameToAssume = split_part(roleNameParts, '#', 2);
|
||||||
roleTypeToAssume = split_part(roleNameParts, '#', 3);
|
roleTypeToAssume = split_part(roleNameParts, '#', 3);
|
||||||
|
|
||||||
objectUuidToAssume = findObjectUuidByIdName(objectTableToAssume, objectNameToAssume);
|
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
|
from RbacRole r
|
||||||
where r.objectUuid = objectUuidToAssume
|
where r.objectUuid = objectUuidToAssume
|
||||||
and r.roleType = roleTypeToAssume
|
and r.roleType = roleTypeToAssume
|
||||||
into roleUuidToAssume;
|
into roleUuidToAssume;
|
||||||
if roleUuidToAssume is null then
|
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;
|
end if;
|
||||||
if not isGranted(currentUserUuid, roleUuidToAssume) then
|
if not isGranted(currentUserUuid, roleUuidToAssume) then
|
||||||
raise exception '[403] user % has no permission to assume role %', currentUser(), roleName;
|
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.
|
This function will be overwritten by later changesets.
|
||||||
*/
|
*/
|
||||||
create or replace procedure contextDefined(
|
create or replace procedure contextDefined(
|
||||||
currentTask varchar,
|
currentTask varchar(127),
|
||||||
currentRequest varchar,
|
currentRequest text,
|
||||||
currentUser varchar,
|
currentUser varchar(63),
|
||||||
assumedRoles varchar
|
assumedRoles varchar(1023)
|
||||||
)
|
)
|
||||||
language plpgsql as $$
|
language plpgsql as $$
|
||||||
declare
|
declare
|
@ -9,7 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
drop view if exists rbacrole_ev;
|
drop view if exists rbacrole_ev;
|
||||||
create or replace view rbacrole_ev as
|
create or replace view rbacrole_ev as
|
||||||
select (objectTable || '#' || objectIdName || '.' || roleType) as roleIdName, *
|
select (objectTable || '#' || objectIdName || ':' || roleType) as roleIdName, *
|
||||||
-- @formatter:off
|
-- @formatter:off
|
||||||
from (
|
from (
|
||||||
select r.*,
|
select r.*,
|
||||||
@ -40,7 +40,7 @@ select *
|
|||||||
where isGranted(currentSubjectsUuids(), r.uuid)
|
where isGranted(currentSubjectsUuids(), r.uuid)
|
||||||
) as unordered
|
) as unordered
|
||||||
-- @formatter:on
|
-- @formatter:on
|
||||||
order by objectTable || '#' || objectIdName || '.' || roleType;
|
order by objectTable || '#' || objectIdName || ':' || roleType;
|
||||||
grant all privileges on rbacrole_rv to ${HSADMINNG_POSTGRES_RESTRICTED_USERNAME};
|
grant all privileges on rbacrole_rv to ${HSADMINNG_POSTGRES_RESTRICTED_USERNAME};
|
||||||
--//
|
--//
|
||||||
|
|
||||||
@ -57,12 +57,13 @@ create or replace view rbacgrants_ev as
|
|||||||
-- @formatter:off
|
-- @formatter:off
|
||||||
select x.grantUuid as uuid,
|
select x.grantUuid as uuid,
|
||||||
x.grantedByTriggerOf as grantedByTriggerOf,
|
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.ascendingIdName as ascendantIdName,
|
||||||
x.descendingIdName as descendantIdName,
|
x.descendingIdName as descendantIdName,
|
||||||
x.grantedByRoleUuid,
|
x.grantedByRoleUuid,
|
||||||
x.ascendantUuid as ascendantUuid,
|
x.ascendantUuid as ascendantUuid,
|
||||||
x.descendantUuid as descendantUuid,
|
x.descendantUuid as descendantUuid,
|
||||||
|
x.op as permOp, x.optablename as permOpTableName,
|
||||||
x.assumed
|
x.assumed
|
||||||
from (
|
from (
|
||||||
select g.uuid as grantUuid,
|
select g.uuid as grantUuid,
|
||||||
@ -70,16 +71,20 @@ create or replace view rbacgrants_ev as
|
|||||||
g.grantedbyroleuuid, g.ascendantuuid, g.descendantuuid, g.assumed,
|
g.grantedbyroleuuid, g.ascendantuuid, g.descendantuuid, g.assumed,
|
||||||
|
|
||||||
coalesce(
|
coalesce(
|
||||||
'user ' || au.name,
|
'user:' || au.name,
|
||||||
'role ' || aro.objectTable || '#' || findIdNameByObjectUuid(aro.objectTable, aro.uuid) || '.' || ar.roletype
|
'role:' || aro.objectTable || '#' || findIdNameByObjectUuid(aro.objectTable, aro.uuid) || ':' || ar.roletype
|
||||||
) as ascendingIdName,
|
) as ascendingIdName,
|
||||||
aro.objectTable, aro.uuid,
|
aro.objectTable, aro.uuid,
|
||||||
|
( case
|
||||||
coalesce(
|
when dro is not null
|
||||||
'role ' || dro.objectTable || '#' || findIdNameByObjectUuid(dro.objectTable, dro.uuid) || '.' || dr.roletype,
|
then ('role:' || dro.objectTable || '#' || findIdNameByObjectUuid(dro.objectTable, dro.uuid) || ':' || dr.roletype)
|
||||||
'perm ' || dp.op || ' on ' || dpo.objecttable || '#' || findIdNameByObjectUuid(dpo.objectTable, dpo.uuid)
|
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,
|
) as descendingIdName,
|
||||||
dro.objectTable, dro.uuid
|
dro.objectTable, dro.uuid,
|
||||||
|
dp.op, dp.optablename
|
||||||
from rbacgrants as g
|
from rbacgrants as g
|
||||||
|
|
||||||
left outer join rbacrole as ar on ar.uuid = g.ascendantUuid
|
left outer join rbacrole as ar on ar.uuid = g.ascendantUuid
|
||||||
@ -110,8 +115,8 @@ create or replace view rbacgrants_ev as
|
|||||||
drop view if exists rbacgrants_rv;
|
drop view if exists rbacgrants_rv;
|
||||||
create or replace view rbacgrants_rv as
|
create or replace view rbacgrants_rv as
|
||||||
-- @formatter:off
|
-- @formatter:off
|
||||||
select o.objectTable || '#' || findIdNameByObjectUuid(o.objectTable, o.uuid) || '.' || r.roletype as grantedByRoleIdName,
|
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.objectTable || '#' || g.objectIdName || ':' || g.roletype as grantedRoleIdName, g.userName, g.assumed,
|
||||||
g.grantedByRoleUuid, g.descendantUuid as grantedRoleUuid, g.ascendantUuid as userUuid,
|
g.grantedByRoleUuid, g.descendantUuid as grantedRoleUuid, g.ascendantUuid as userUuid,
|
||||||
g.objectTable, g.objectUuid, g.objectIdName, g.roleType as grantedRoleType
|
g.objectTable, g.objectUuid, g.objectIdName, g.roleType as grantedRoleType
|
||||||
from (
|
from (
|
||||||
@ -322,7 +327,7 @@ execute function deleteRbacUser();
|
|||||||
drop view if exists RbacOwnGrantedPermissions_rv;
|
drop view if exists RbacOwnGrantedPermissions_rv;
|
||||||
create or replace view RbacOwnGrantedPermissions_rv as
|
create or replace view RbacOwnGrantedPermissions_rv as
|
||||||
select r.uuid as roleuuid, p.uuid as permissionUuid,
|
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
|
o.objecttable, r.objectidname, o.uuid as objectuuid
|
||||||
from rbacrole_rv r
|
from rbacrole_rv r
|
||||||
join rbacgrants g on g.ascendantuuid = r.uuid
|
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,
|
Returns all permissions granted to the given user,
|
||||||
which are also visible to the current user or assumed roles.
|
which are also visible to the current user or assumed roles.
|
||||||
|
*/
|
||||||
|
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)
|
||||||
create or replace function grantedPermissions(targetUserUuid uuid)
|
|
||||||
returns table(roleUuid uuid, roleName text, permissionUuid uuid, op RbacOp, objectTable varchar, objectIdName varchar, objectUuid uuid)
|
|
||||||
returns null on null input
|
returns null on null input
|
||||||
language plpgsql as $$
|
language plpgsql as $$
|
||||||
declare
|
declare
|
||||||
@ -356,12 +359,14 @@ begin
|
|||||||
|
|
||||||
return query select
|
return query select
|
||||||
xp.roleUuid,
|
xp.roleUuid,
|
||||||
(xp.roleObjectTable || '#' || xp.roleObjectIdName || '.' || xp.roleType) as roleName,
|
(xp.roleObjectTable || '#' || xp.roleObjectIdName || ':' || xp.roleType) as roleName,
|
||||||
xp.permissionUuid, xp.op, xp.permissionObjectTable, xp.permissionObjectIdName, xp.permissionObjectUuid
|
xp.permissionUuid, xp.op, xp.opTableName,
|
||||||
|
xp.permissionObjectTable, xp.permissionObjectIdName, xp.permissionObjectUuid
|
||||||
from (select
|
from (select
|
||||||
r.uuid as roleUuid, r.roletype, ro.objectTable as roleObjectTable,
|
r.uuid as roleUuid, r.roletype, ro.objectTable as roleObjectTable,
|
||||||
findIdNameByObjectUuid(ro.objectTable, ro.uuid) as roleObjectIdName,
|
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,
|
findIdNameByObjectUuid(po.objectTable, po.uuid) as permissionObjectIdName,
|
||||||
po.uuid as permissionObjectUuid
|
po.uuid as permissionObjectUuid
|
||||||
from queryPermissionsGrantedToSubjectId( targetUserUuid) as p
|
from queryPermissionsGrantedToSubjectId( targetUserUuid) as p
|
||||||
@ -373,4 +378,15 @@ begin
|
|||||||
) xp;
|
) xp;
|
||||||
-- @formatter:on
|
-- @formatter:on
|
||||||
end; $$;
|
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;
|
||||||
|
$$;
|
||||||
--//
|
--//
|
@ -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; $$;
|
||||||
|
--//
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user