Compare commits

..

17 Commits

Author SHA1 Message Date
e97b177a92 feature/use-case-acceptance-tests-3 (#119)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #119
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
2024-11-07 10:11:46 +01:00
6191bf16e0 feature/split-up-postalAddress (#118)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #118
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
2024-11-06 12:24:37 +01:00
63af33d003 feature/use-case-acceptance-tests-2 (#117)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #117
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
2024-11-05 13:58:31 +01:00
3b94f117fb feature/use-case-acceptance-tests (#116)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #116
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
2024-10-30 11:40:36 +01:00
c181500a1d feature/add-jenkinsfile (#114)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Co-authored-by: Michael Hönnig <michael@hoennig.de>
Reviewed-on: #114
Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
2024-10-14 10:36:50 +02:00
c26ae77a09 feature/api-for-email-address-search-in-contacts (#113)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Co-authored-by: Michael Hönnig <michael@hoennig.de>
Reviewed-on: #113
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
2024-10-11 17:06:44 +02:00
cb8a5190ce fix allowed licenses, do version upgrades upgrade and improve test coverage (#112)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #112
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
2024-10-10 09:31:43 +02:00
60341bf644 add DomainSetup-HostingAssets for new BookingItem via created-event (#111)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #111
Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
2024-10-08 11:48:31 +02:00
cc2b04472f application-event for booking-item-created with domain-setup-example (#110)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #110
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
2024-09-27 11:19:01 +02:00
4811c0328c fix Error code 500 in Relation.find without type (NullPointerException) (#109)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #109
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
2024-09-27 11:00:58 +02:00
cb4aecb9c8 refactoring for implicit creation of dependend hosting-assets (#108)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #108
Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
2024-09-26 10:51:27 +02:00
d949604d70 unique identifers for hosting assets per type (#107)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #107
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
2024-09-25 10:53:43 +02:00
f33a3a2df7 introduce-separate-database-schemas-hs-booking-and-hosting (#106)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #106
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
2024-09-23 10:52:37 +02:00
23b60641e3 introduce-separate-database-schema-hs-office-and-amend-generators (#105)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #105
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
2024-09-18 10:28:21 +02:00
285e6fbeb5 introduce separate database schema-test and amend RBAC generators for schema-generation (#104)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #104
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
2024-09-17 14:21:43 +02:00
1eed0e9b21 introduce separate database-schemas base+rbac (#103)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Co-authored-by: Michael Hönnig <michael@hoennig.de>
Reviewed-on: #103
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
2024-09-16 15:36:37 +02:00
80d79de5f4 import-legacy-ids-of-booking-items-and-hosting-assets (#101)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #101
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
2024-09-16 10:00:41 +02:00
427 changed files with 13837 additions and 7038 deletions

View File

@ -83,7 +83,7 @@ alias fp='grep -r '@Accepts' src | sed -e 's/^.*@/@/g' | sort -u | wc -l'
alias gw-spotless='./gradlew spotlessApply -x pitest -x test -x :processResources' alias gw-spotless='./gradlew spotlessApply -x pitest -x test -x :processResources'
alias gw-test='. .aliases; ./gradlew test' alias gw-test='. .aliases; ./gradlew test'
alias gw-check='. .aliases; gw test importOfficeData check -x pitest -x :dependencyCheckAnalyze' alias gw-check='. .aliases; gw test check -x pitest'
# etc/docker-compose.yml limits CPUs+MEM and includes a PostgreSQL config for analysing slow queries # etc/docker-compose.yml limits CPUs+MEM and includes a PostgreSQL config for analysing slow queries
alias gw-importOfficeData-in-docker-compose=' alias gw-importOfficeData-in-docker-compose='

84
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,84 @@
pipeline {
agent {
dockerfile {
filename 'etc/jenkinsAgent.Dockerfile'
// additionalBuildArgs ...
args '--network=bridge --user root -v $PWD:$PWD -v /var/run/docker.sock:/var/run/docker.sock --group-add 984'
reuseNode true
}
}
environment {
DOCKER_HOST = 'unix:///var/run/docker.sock'
HSADMINNG_POSTGRES_ADMIN_USERNAME = 'admin'
HSADMINNG_POSTGRES_RESTRICTED_USERNAME = 'restricted'
HSADMINNG_MIGRATION_DATA_PATH = 'migration'
}
triggers {
pollSCM('H/1 * * * *')
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage ('Compile') {
steps {
sh './gradlew clean processSpring compileJava compileTestJava --no-daemon'
}
}
stage ('Tests') {
parallel {
stage('Unit-/Integration/Acceptance-Tests') {
steps {
sh './gradlew check --no-daemon -x pitest -x dependencyCheckAnalyze -x importOfficeData -x importHostingAssets'
}
}
stage('Import-Tests') {
steps {
sh './gradlew importOfficeData importHostingAssets --no-daemon'
}
}
stage ('Scenario-Tests') {
steps {
sh './gradlew scenarioTests --no-daemon'
}
}
}
}
stage ('Check') {
steps {
sh './gradlew check -x pitest -x dependencyCheckAnalyze --no-daemon'
}
}
}
post {
always {
// archive test results
junit 'build/test-results/test/*.xml'
// archive the JaCoCo coverage report in XML and HTML format
jacoco(
execPattern: 'build/jacoco/*.exec',
classPattern: 'build/classes/java/main',
sourcePattern: 'src/main/java'
)
// archive scenario-test reports in HTML format
sh '''
./gradlew convertMarkdownToHtml
'''
archiveArtifacts artifacts: 'doc/scenarios/*.html', allowEmptyArchive: true
// cleanup workspace
cleanWs()
}
}
}

View File

@ -51,12 +51,9 @@ Everything is tested on _Ubuntu Linux 22.04_ and _MacOS Monterey (12.4)_.
To be able to build and run the Java Spring Boot application, you need the following tools: To be able to build and run the Java Spring Boot application, you need the following tools:
- Docker 20.x (on MacOS you also need *Docker Desktop* or similar) or Podman - Docker 20.x (on MacOS you also need *Docker Desktop* or similar) or Podman
- if you use Podman, explicitly fetch the Docker-image for PostgreSQL Server 15.5-bookworm: - optionally: PostgreSQL Server 15.5-bookworm
`podman pull docker.io/postgres:15.5-bookworm`
(see instructions below to install and run in Docker) (see instructions below to install and run in Docker)
- The matching Java JDK at will be automatically installed by Gradle toolchain support to `~/.gradle/jdks/`. - The matching Java JDK at will be automatically installed by Gradle toolchain support to `~/.gradle/jdks/`.
- named-checkzone needs to be installed for some tests and to run fully the application:
`sudo apt install bind9-utils`
- You also might need an IDE (e.g. *IntelliJ IDEA* or *Eclipse* or *VS Code* with *[STS](https://spring.io/tools)* and a GUI Frontend for *PostgreSQL* like *Postbird*. - You also might need an IDE (e.g. *IntelliJ IDEA* or *Eclipse* or *VS Code* with *[STS](https://spring.io/tools)* and a GUI Frontend for *PostgreSQL* like *Postbird*.
If you have at least Docker and the Java JDK installed in appropriate versions and in your `PATH`, then you can start like this: If you have at least Docker and the Java JDK installed in appropriate versions and in your `PATH`, then you can start like this:
@ -66,15 +63,12 @@ If you have at least Docker and the Java JDK installed in appropriate versions a
source .aliases # creates some comfortable bash aliases, e.g. 'gw'='./gradlew' source .aliases # creates some comfortable bash aliases, e.g. 'gw'='./gradlew'
gw # initially downloads the configured Gradle version into the project gw # initially downloads the configured Gradle version into the project
gw test # compiles and runs unit- and integration-tests
# if the container has not been built yet, run this: # if the container has not been built yet, run this:
pg-sql-run # downloads + runs PostgreSQL in a Docker container on localhost:5432 pg-sql-run # downloads + runs PostgreSQL in a Docker container on localhost:5432
# if the container has been built already, run this: # if the container has been built already, run this:
pg-sql-start pg-sql-start
# this step was mostly to test if Docker/Podman wirks (and introduce these aliases), so you can stop PostgreSQL again:
pg-sql-stop
gw-test # compiles and runs unit- and integration-tests
gw bootRun # compiles and runs the application on localhost:8080 gw bootRun # compiles and runs the application on localhost:8080
@ -83,17 +77,17 @@ 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 customers: # the following command should return a JSON array with just all customers:
curl \ curl \
-H 'current-user: superuser-alex@hostsharing.net' \ -H 'current-subject: superuser-alex@hostsharing.net' \
http://localhost:8080/api/test/customers http://localhost:8080/api/test/customers
# 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-subject: superuser-alex@hostsharing.net' -H 'assumed-roles: rbactest.customer#yyy:ADMIN' \
http://localhost:8080/api/test/packages http://localhost:8080/api/test/packages
# add a new customer # add a new customer
curl \ curl \
-H 'current-user: superuser-alex@hostsharing.net' -H "Content-Type: application/json" \ -H 'current-subject: superuser-alex@hostsharing.net' -H "Content-Type: application/json" \
-d '{ "prefix":"ttt", "reference":80001, "adminUserName":"admin@ttt.example.com" }' \ -d '{ "prefix":"ttt", "reference":80001, "adminUserName":"admin@ttt.example.com" }' \
-X POST http://localhost:8080/api/test/customers -X POST http://localhost:8080/api/test/customers
@ -117,7 +111,7 @@ But the easiest way to run PostgreSQL is via Docker.
Initially, pull an image compatible to current PostgreSQL version of Hostsharing: Initially, pull an image compatible to current PostgreSQL version of Hostsharing:
docker pull postgres:15.5-bookworm
<big>**&#9888;**</big> <big>**&#9888;**</big>
If we switch the version, please also amend the documentation as well as the aliases file. Thanks! If we switch the version, please also amend the documentation as well as the aliases file. Thanks!
@ -262,7 +256,7 @@ If not, you need to install some tooling.
##### for IntelliJ IDEA (or derived products) ##### for IntelliJ IDEA (or derived products)
1. Activate the bundled Jetbrains Markdown PlantUML Extension via 1. Activate the bundled Jebrains Markdown PlantUML Extension via
[File | Settings | Languages & Frameworks | Markdown](jetbrains://idea/settings?name=Languages+%26+Frameworks--Markdown) [File | Settings | Languages & Frameworks | Markdown](jetbrains://idea/settings?name=Languages+%26+Frameworks--Markdown)
2. Install the Jetbrains Mermaid plugin: https://plugins.jetbrains.com/plugin/20146-mermaid, it also works embedded in Markdown files. 2. Install the Jetbrains Mermaid plugin: https://plugins.jetbrains.com/plugin/20146-mermaid, it also works embedded in Markdown files.
@ -310,6 +304,15 @@ To increase the amount of test data, increase the number of generated customers
If you already have data, e.g. for customers 0..999 (thus with reference numbers 10000..10999) and want to add another 1000 customers, amend the for loop to 1000...1999 and also uncomment and amend the `CONTINUE WHEN` or `WHERE` conditions in the other test data generators, using the first new customer reference number (in the example that's 11000). If you already have data, e.g. for customers 0..999 (thus with reference numbers 10000..10999) and want to add another 1000 customers, amend the for loop to 1000...1999 and also uncomment and amend the `CONTINUE WHEN` or `WHERE` conditions in the other test data generators, using the first new customer reference number (in the example that's 11000).
### For Historization
The historization is not yet integrated into the *Liquibase*-scripts.
You can explore the prototype as follows:
- start with an empty database
(the example tables are currently not compatible with RBAC),
- then run `historization.sql` in the database,
- finally run `examples.sql` in the database.
## Coding Guidelines ## Coding Guidelines
@ -494,9 +497,19 @@ We'll see if this changes when the project progresses and more validations are a
### OWASP Security Vulnerability Check ### OWASP Security Vulnerability Check
An OWASP security vulnerability is configured and can be utilized by running: An OWASP security vulnerability is configured, but you need an API key.
Fetch it from https://nvd.nist.gov/developers/request-an-api-key.
Then add it to your `~/.gradle/gradle.properties` file:
```
OWASP_API_KEY=........-....-....-....-............
```
Now you can run the dependency vulnerability check:
```shell ```shell
gw dependencyCheckUpdate
gw dependencyCheckAnalyze gw dependencyCheckAnalyze
``` ```
@ -547,12 +560,37 @@ Dependency versions can be automatically upgraded to the latest available versio
gw useLatestVersions gw useLatestVersions
``` ```
Afterwards, `gw check` is automatically started. Afterward, `gw check` is automatically started.
Please only commit+push to master if the check run shows no errors. Please only commit+push to master if the check run shows no errors.
More infos, e.g. on blacklists see on the [project's website](https://github.com/patrikerdes/gradle-use-latest-versions-plugin). More infos, e.g. on blacklists see on the [project's website](https://github.com/patrikerdes/gradle-use-latest-versions-plugin).
## Biggest Flaws in our Architecture
### The RBAC System is too Complicated
Now, where we have a better experience with what we really need from the RBAC system, we have learned
that and creates too many (grant- and role-) rows and too even tables which could be avoided completely.
The basic idea is always to always have a fixed set of ordered role-types which apply for all DB-tables under RBAC,
e.g. OWNER>ADMIN>AGENT\[>PROXY?\]>TENENT>REFERRER.
Grants between these for the same DB-row would be implicit by order comparision.
This way we would get rid of all explicit grants within the same DB-row
and would not need the `rbac.role` table anymore.
We would also reduce the depth of the expensive recursive CTE-query.
This has to be explored further.
For now, we just keep it in mind and
### The Mapper is Error-Prone
Where `org.modelmapper.ModelMapper` reduces bloat-code a lot and has some nice features about recursive data-structure mappings,
it often causes strange errors which are hard to fix.
E.g. the uuid of the target main object is often taken from an uuid of a sub-subject.
(For now, use `StrictMapper` to avoid this, for the case it happens.)
## How To ... ## How To ...
### How to Configure .pgpass for the Default PostgreSQL Database? ### How to Configure .pgpass for the Default PostgreSQL Database?
@ -586,8 +624,7 @@ sudo apt-get -y install podman
It is possible to move the storage directory to /tmp, e.g. to increase performance or to avoid issues with NFS mounted home directories: It is possible to move the storage directory to /tmp, e.g. to increase performance or to avoid issues with NFS mounted home directories:
```shell ```shell
mkdir -p ~/.config/containers/ cat .config/containers/storage.conf
cat ~/.config/containers/storage.conf
[storage] [storage]
driver = "vfs" driver = "vfs"
graphRoot = "/tmp/containers/storage" graphRoot = "/tmp/containers/storage"
@ -609,10 +646,7 @@ These commands are also available in `.aliases` as `podman-start`.
1. In a local shell. in which you want to run the tests, set some environment variables: 1. In a local shell. in which you want to run the tests, set some environment variables:
```shell ```shell
# sudo ln -s $HOME/.docker/run/docker.sock /var/run/docker.sock
# or:
export DOCKER_HOST="unix:///run/user/$UID/podman/podman.sock" export DOCKER_HOST="unix:///run/user/$UID/podman/podman.sock"
# and:
export TESTCONTAINERS_RYUK_DISABLED=true export TESTCONTAINERS_RYUK_DISABLED=true
``` ```

View File

@ -0,0 +1,38 @@
#!/bin/bash
# waits for commits on any branch on origin, checks it out and builds it
. .aliases
while true; do
git fetch origin >/dev/null
branch_with_new_commits=`git fetch origin >/dev/null; git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads | grep '\[behind' | cut -d' ' -f1 | head -n1`
if [ -n "$branch_with_new_commits" ]; then
echo "checking out branch: $branch_with_new_commits"
if git show-ref --quiet --heads "$branch_with_new_commits"; then
echo "Branch $branch_with_new_commits already exists. Checking it out and pulling latest changes."
git checkout "$branch_with_new_commits"
git pull origin "$branch_with_new_commits"
else
echo "Creating and checking out new branch: $branch_with_new_commits"
git checkout -b "$branch_with_new_commits" "origin/$branch_with_new_commits"
fi
echo "building ..."
./gradlew gw clean test check -x pitest
fi
# wait 10s with a little animation
echo -e -n "\r\033[K waiting for changes (/) ..."
sleep 2
echo -e -n "\r\033[K waiting for changes (-) ..."
sleep 2
echo -e -n "\r\033[K waiting for changes (\) ..."
sleep 2
echo -e -n "\r\033[K waiting for changes (|) ..."
sleep 2
echo -e -n "\r\033[K waiting for changes ( ) ... "
sleep 2
echo -e -n "\r\033[K checking for changes"
done

View File

@ -1,10 +1,10 @@
plugins { plugins {
id 'java' id 'java'
id 'org.springframework.boot' version '3.2.4' id 'org.springframework.boot' version '3.3.4'
id 'io.spring.dependency-management' version '1.1.4' id 'io.spring.dependency-management' version '1.1.6'
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.6' id 'com.github.jk1.dependency-license-report' version '2.9'
id "org.owasp.dependencycheck" version "9.0.10" id "org.owasp.dependencycheck" version "10.0.4"
id "com.diffplug.spotless" version "6.25.0" 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'
@ -58,19 +58,20 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.springframework.boot:spring-boot-starter-jdbc'
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.2'
implementation 'org.springdoc:springdoc-openapi:2.4.0' implementation 'org.springdoc:springdoc-openapi:2.6.0'
implementation 'org.postgresql:postgresql:42.7.3' implementation 'org.postgresql:postgresql:42.7.4'
implementation 'org.liquibase:liquibase-core:4.27.0' implementation 'org.liquibase:liquibase-core:4.29.2'
implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.7.3' implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.8.3'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.0' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.0'
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.12.0'
implementation 'net.java.dev.jna:jna:5.8.0' implementation 'net.java.dev.jna:jna:5.15.0'
implementation 'org.modelmapper:modelmapper:3.2.0' implementation 'org.modelmapper:modelmapper:3.2.1'
implementation 'org.iban4j:iban4j:3.2.7-RELEASE' implementation 'org.iban4j:iban4j:3.2.10-RELEASE'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.4.0' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'
implementation 'org.reflections:reflections:0.9.12' implementation 'org.webjars:swagger-ui:5.17.14'
implementation 'org.reflections:reflections:0.10.2'
compileOnly 'org.projectlombok:lombok' compileOnly 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok' testCompileOnly 'org.projectlombok:lombok'
@ -85,9 +86,9 @@ dependencies {
testImplementation 'org.testcontainers:junit-jupiter' testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.junit.jupiter:junit-jupiter' testImplementation 'org.junit.jupiter:junit-jupiter'
testImplementation 'org.testcontainers:postgresql' testImplementation 'org.testcontainers:postgresql'
testImplementation 'com.tngtech.archunit:archunit-junit5:1.2.1' testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
testImplementation 'io.rest-assured:spring-mock-mvc' testImplementation 'io.rest-assured:spring-mock-mvc'
testImplementation 'org.hamcrest:hamcrest-core:2.2' testImplementation 'org.hamcrest:hamcrest-core:3.0'
testImplementation 'org.pitest:pitest-junit5-plugin:1.2.1' testImplementation 'org.pitest:pitest-junit5-plugin:1.2.1'
testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.junit.jupiter:junit-jupiter-api'
} }
@ -254,7 +255,7 @@ test {
'net.hostsharing.hsadminng.**.generated.**', 'net.hostsharing.hsadminng.**.generated.**',
] ]
useJUnitPlatform { useJUnitPlatform {
excludeTags 'import' excludeTags 'importOfficeData', 'importHostingData', 'scenarioTest'
} }
} }
jacocoTestReport { jacocoTestReport {
@ -276,7 +277,7 @@ jacocoTestCoverageVerification {
violationRules { violationRules {
rule { rule {
limit { limit {
minimum = 0.92 minimum = 0.80 // TODO.test: improve instruction coverage
} }
} }
@ -288,15 +289,20 @@ jacocoTestCoverageVerification {
element = 'CLASS' element = 'CLASS'
excludes = [ excludes = [
'net.hostsharing.hsadminng.**.generated.**', 'net.hostsharing.hsadminng.**.generated.**',
'net.hostsharing.hsadminng.rbac.test.dom.TestDomainEntity',
'net.hostsharing.hsadminng.HsadminNgApplication', 'net.hostsharing.hsadminng.HsadminNgApplication',
'net.hostsharing.hsadminng.ping.PingController', 'net.hostsharing.hsadminng.ping.PingController',
'net.hostsharing.hsadminng.rbac.generator.*',
'net.hostsharing.hsadminng.rbac.grant.RbacGrantsDiagramService',
'net.hostsharing.hsadminng.rbac.grant.RbacGrantsDiagramService.Node',
'net.hostsharing.hsadminng.**.*Repository',
'net.hostsharing.hsadminng.mapper.Mapper' 'net.hostsharing.hsadminng.mapper.Mapper'
] ]
limit { limit {
counter = 'LINE' counter = 'LINE'
value = 'COVEREDRATIO' value = 'COVEREDRATIO'
minimum = 0.98 minimum = 0.75 // TODO.test: improve line coverage
} }
} }
rule { rule {
@ -310,7 +316,7 @@ jacocoTestCoverageVerification {
limit { limit {
counter = 'BRANCH' counter = 'BRANCH'
value = 'COVEREDRATIO' value = 'COVEREDRATIO'
minimum = 1.00 minimum = 0.00 // TODO.test: improve branch coverage
} }
} }
} }
@ -338,19 +344,30 @@ tasks.register('importHostingAssets', Test) {
mustRunAfter spotlessJava mustRunAfter spotlessJava
} }
tasks.register('scenarioTests', Test) {
useJUnitPlatform {
includeTags 'scenarioTest'
}
group 'verification'
description 'run the import jobs as tests'
mustRunAfter spotlessJava
}
// pitest mutation testing // pitest mutation testing
pitest { pitest {
targetClasses = ['net.hostsharing.hsadminng.**'] targetClasses = ['net.hostsharing.hsadminng.**']
excludedClasses = [ excludedClasses = [
'net.hostsharing.hsadminng.config.**', 'net.hostsharing.hsadminng.config.**',
'net.hostsharing.hsadminng.**.*Controller', // 'net.hostsharing.hsadminng.**.*Controller',
'net.hostsharing.hsadminng.**.generated.**' 'net.hostsharing.hsadminng.**.generated.**'
] ]
targetTests = ['net.hostsharing.hsadminng.**.*UnitTest', 'net.hostsharing.hsadminng.**.*RestTest'] targetTests = ['net.hostsharing.hsadminng.**.*UnitTest', 'net.hostsharing.hsadminng.**.*RestTest']
excludedTestClasses = ['**AcceptanceTest*', '**IntegrationTest*'] excludedTestClasses = ['**AcceptanceTest*', '**IntegrationTest*']
pitestVersion = '1.15.3' pitestVersion = '1.17.0'
junit5PluginVersion = '1.1.0' junit5PluginVersion = '1.1.0'
threads = 4 threads = 4
@ -385,3 +402,45 @@ tasks.named("dependencyUpdates").configure {
isNonStable(it.candidate.version) isNonStable(it.candidate.version)
} }
} }
// Generate HTML from Markdown scenario-test-reports using Pandoc:
tasks.register('convertMarkdownToHtml') {
description = 'Generates HTML from Markdown scenario-test-reports using Pandoc.'
group = 'Conversion'
// Define the template file and input directory
def templateFile = file('doc/scenarios/template.html')
// Task configuration and execution
doFirst {
// Check if pandoc is installed
try {
exec {
commandLine 'pandoc', '--version'
}
} catch (Exception) {
throw new GradleException("Pandoc is not installed or not found in the system path.")
}
// Check if the template file exists
if (!templateFile.exists()) {
throw new GradleException("Template file 'doc/scenarios/template.html' not found.")
}
}
doLast {
// Gather all Markdown files in the current directory
fileTree(dir: '.', include: 'doc/scenarios/*.md').each { file ->
// Corrected way to create the output file path
def outputFile = new File(file.parent, file.name.replaceAll(/\.md$/, '.html'))
// Execute pandoc for each markdown file
exec {
commandLine 'pandoc', file.absolutePath, '--template', templateFile.absolutePath, '-o', outputFile.absolutePath
}
println "Converted ${file.name} to ${outputFile.name}"
}
}
}

View File

@ -14,9 +14,9 @@ The core problem here is, that in our RBAC system, determining the permissions o
### Technical Background ### Technical Background
The session variable `hsadminng.currentUser` contains the accessing (domain-level) user, which is unrelated to the PostgreSQL user). The session variable `hsadminng.currentSubject` contains the accessing (domain-level) user, which is unrelated to the PostgreSQL user).
Given is a stored function `isPermissionGrantedToSubject` which detects if the accessing user has a given permission (e.g. 'view'). Given is a stored function `isPermissionGrantedToSubject` which detects if the accessing subject has a given permission (e.g. 'view').
Given is also a stored function `queryAllPermissionsOfSubjectId` which returns the flattened view to all permissions assigned to the given accessing user. Given is also a stored function `queryAllPermissionsOfSubjectId` which returns the flattened view to all permissions assigned to the given accessing user.
@ -38,7 +38,7 @@ In this solution, the database ignores row level visibility and returns all rows
Very flexible access, programmatic, rules could be implemented. Very flexible access, programmatic, rules could be implemented.
The role-hierarchy and permissions for currently logged-in users user could be cached in the backend. The role-hierarchy and permissions for current subjects (e.g. logged-in users) could be cached in the backend.
The access logic can be tested in pure Java unit tests. The access logic can be tested in pure Java unit tests.
@ -74,11 +74,11 @@ For restricted DB-users, which are used by the backend, access to rows is filter
FOR SELECT FOR SELECT
TO restricted TO restricted
USING ( USING (
isPermissionGrantedToSubject(findEffectivePermissionId('customer', id, 'view'), currentUserUuid()) rbac.isPermissionGrantedToSubject(rbac.findEffectivePermissionId('customer', id, 'view'), currentSubjectUuid())
); );
SET SESSION AUTHORIZATION restricted; SET SESSION AUTHORIZATION restricted;
SET hsadminng.currentUser TO 'alex@example.com'; SET hsadminng.currentSubject TO 'alex@example.com';
SELECT * from customer; -- will only return visible rows SELECT * from customer; -- will only return visible rows
#### Advantages #### Advantages
@ -101,10 +101,10 @@ We are bound to PostgreSQL, including integration tests and testing the RBAC sys
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('customer', id, 'view'), currentUserUuid()); SELECT * FROM customer WHERE rbac.isPermissionGrantedToSubject(rbac.findEffectivePermissionId('customer', id, 'view'), currentSubjectUuid());
SET SESSION AUTHORIZATION restricted; SET SESSION AUTHORIZATION restricted;
SET hsadminng.currentUser TO 'alex@example.com'; SET hsadminng.currentSubject TO 'alex@example.com';
SELECT * from customer; -- will only return visible rows SELECT * from customer; -- will only return visible rows
#### Advantages #### Advantages
@ -130,12 +130,12 @@ We do not access the tables directly from the backend, but via views which join
CREATE OR REPLACE VIEW cust_view AS CREATE OR REPLACE VIEW cust_view AS
SELECT c.id, c.reference, c.prefix SELECT c.id, c.reference, c.prefix
FROM customer AS c FROM customer AS c
JOIN queryAllPermissionsOfSubjectId(currentUserUuid()) AS p JOIN queryAllPermissionsOfSubjectId(currentSubjectUuid()) AS p
ON p.tableName='customer' AND p.rowId=c.id AND p.op='view'; ON p.tableName='customer' AND p.rowId=c.id AND p.op='view';
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;
SET hsadminng.currentUser TO 'alex@example.com'; SET hsadminng.currentSubject TO 'alex@example.com';
SELECT * from cust_view; -- will only return visible rows SELECT * from cust_view; -- will only return visible rows
Alternatively the JOIN could also be applied in a "ON SELECT DO INSTEAD"-RULE, if there is any advantage for later features. Alternatively the JOIN could also be applied in a "ON SELECT DO INSTEAD"-RULE, if there is any advantage for later features.

View File

@ -0,0 +1,119 @@
# Handling Automatic Creation of Hosting Assets for New Booking Items
**Status:**
- [x] proposed by (Michael Hönnig)
- [ ] accepted by (Participants)
- [ ] rejected by (Participants)
- [ ] superseded by (superseding ADR)
## Context and Problem Statement
When a customer creates a new booking item (e.g., `MANAGED_WEBSPACE`), the system must automatically create the related hosting asset.
This process can sometimes fail or require additional data from the user, e.g. installing a DNS verification key, or a hostmaster, e.g. the target server to use.
The challenge is how to handle this automatic creation process while dealing with missing data, asynchronicity and failures while ensuring system consistency and proper user notification.
### Technical Background
The creation of hosting assets can occur synchronously (in simple cases) or asynchronously (when additional steps like manual verification are needed).
For example, a `DOMAIN_SETUP` hosting asset may require DNS verification from the user, and until this is provided, the related domain cannot be fully set up.
Additionally, not all data needed for creating the hosting asset is stored in the booking item.
It's part of the HTTP request and later stored in the hosting asset, but we also need to store it before the hosting asset can be created asynchronously.
Current system behavior involves returning HTTP 201 upon booking item creation, but the automatic hosting asset creation might fail due to missing information.
The system needs to manage the creation process in a way that ensures valid hosting assets are created and informs the user of any actions required while still returning a 201 HTTP code, not an error code.
## Considered Options
For storing the data needed for the hosting-asset creation:
* STORAGE-1: Store temporary asset data in the `BookingItemEntity`, e.g. a JSON column.
And delete the value of that column, once the hosting assets got successfully created.
* STORAGE-2: Create hosting assets immediately, even if invalid, but mark them as "inactive" until completed and fully validated.
* STORAGE-3: Store the asset data in a kind of event- or job-queue, which get deleted once the hosting-asset got successfully created.
For the user-notification status:
* STATUS-1: Introduce a status field in the booking-items.
* STATUS-2: Store the status in the event-/job-queue entries.
### STORAGE-1: Temporary Data Storage in `BookingItemEntity`
Store asset-related data (e.g., domain name) in a temporary column or JSON field in the `BookingItemEntity` until the hosting assets are successfully created.
Once assets are created, the temporary data is deleted to avoid inconsistencies.
#### Advantages
- Easy to implement.
#### Disadvantages
- Needs either a separate map of properties in the booking-item.
- Or, if stored as a JSON field in the booking-item-resources, these are misused.
- Requires additional cleanup logic to remove stale data.
### STORAGE-2: Inactive Hosting Assets Until Validation
Create the hosting assets immediately upon booking item creation but mark them as "inactive" until all required information (e.g., verification code) is provided and validation is complete.
#### Advantages
- Avoids temporary external data storage for the hosting-assets.
#### Disadvantages
- Validation becomes more complex as some properties need to be validated, others not.
And some properties even need special treatment for new entities, which then becomes vague.
- Inactive assets have to be filtered from operational assets.
- Potential risk of incomplete or inconsistent assets being created, which may require correction.
- Difficult to write tests for all possible combinations of validations.
### STORAGE-3: Event-Based Approach
The hosting asset data required for creation us passed to the API and stored in a `BookingItemCreatedEvent`.
If hosting asset creation cannot happen synchronously, the event is stored and processed asynchronously in batches, retrying failed asset creation as needed.
#### Advantages
- Clean-data-structure (separation of concerns).
- Clear separation between booking item creation and hosting asset creation.
- Only valid assets in the database.
- Can handle complex asynchronous processes (like waiting for external verification) in a clean and structured manner.
- Easier to manage retries and failures in asset creation without complicating the booking item structure.
#### Disadvantages
- At the Spring controller level, the whole JSON is already converted into Java objects,
but for storing the asset data in the even, we need JSON again.
This could is not just a performance-overhead but could also lead to inconsistencies.
### STATUS-1: Store hosting-asset-creation-status in the `BookingItemEntity`
A status field would be added to booking-items to track the creation state of related hosting assets.
The users could check their booking-items for the status of the hosting-asset creation, error messages and further instructions.
#### Advantages
- Easy to implement.
#### Disadvantages
- Adds a field to the booking-item which is makes no sense anymore once the related hosting asset is created.
### Status-2: Store hosting-asset-creation-status in the `BookingItemCreateEvent`
A status field would be added to the booking-item-created event and get updated with the latest messages any time we try to create the hosting-asset.
#### Advantages
- Clean-data-structure (separation of concerns)
#### Disadvantages
- Accessing the status requires querying the event queue.
## Decision Outcome
**Chosen Option: STORAGE-3 with STATUS-2 (Event-Based Approach with `BookingItemCreatedEvent`)**
The event-based approach was selected as the best solution for handling automatic hosting asset creation. This option provides a clear separation between booking item creation and hosting asset creation, ensuring that no invalid or incomplete assets are created. The asynchronous nature of the event system allows for retries and external validation steps (such as user-entered verification codes) without disrupting the overall flow.
By using `BookingItemCreatedEvent` to store the hosting-asset data and the status,
we don't need to misuse other data structures for temporary data
and therefore hava a clean separation of concerns.

View File

@ -199,21 +199,21 @@ Limit (cost=6549.08..6549.35 rows=54 width=16)
Group Key: grants.descendantuuid Group Key: grants.descendantuuid
-> CTE Scan on grants (cost=0.00..22.06 rows=1103 width=16) -> CTE Scan on grants (cost=0.00..22.06 rows=1103 width=16)
-> Index Only Scan using rbacobject_objecttable_uuid_key on rbacobject obj (cost=0.28..0.31 rows=1 width=16) -> Index Only Scan using rbacobject_objecttable_uuid_key on rbacobject obj (cost=0.28..0.31 rows=1 width=16)
Index Cond: ((objecttable = 'hs_hosting_asset'::text) AND (uuid = perm.objectuuid)) Index Cond: ((objecttable = 'hs_hosting.asset'::text) AND (uuid = perm.objectuuid))
``` ```
### Office-Relation-Query ### Office-Relation-Query
```SQL ```SQL
SELECT hore1_0.uuid,a1_0.uuid,a1_0.familyname,a1_0.givenname,a1_0.persontype,a1_0.salutation,a1_0.title,a1_0.tradename,a1_0.version,c1_0.uuid,c1_0.caption,c1_0.emailaddresses,c1_0.phonenumbers,c1_0.postaladdress,c1_0.version,h1_0.uuid,h1_0.familyname,h1_0.givenname,h1_0.persontype,h1_0.salutation,h1_0.title,h1_0.tradename,h1_0.version,hore1_0.mark,hore1_0.type,hore1_0.version SELECT hore1_0.uuid,a1_0.uuid,a1_0.familyname,a1_0.givenname,a1_0.persontype,a1_0.salutation,a1_0.title,a1_0.tradename,a1_0.version,c1_0.uuid,c1_0.caption,c1_0.emailaddresses,c1_0.phonenumbers,c1_0.postaladdress,c1_0.version,h1_0.uuid,h1_0.familyname,h1_0.givenname,h1_0.persontype,h1_0.salutation,h1_0.title,h1_0.tradename,h1_0.version,hore1_0.mark,hore1_0.type,hore1_0.version
FROM hs_office_relation_rv hore1_0 FROM hs_office.relation_rv hore1_0
LEFT JOIN hs_office_person_rv a1_0 ON a1_0.uuid=hore1_0.anchoruuid LEFT JOIN hs_office.person_rv a1_0 ON a1_0.uuid=hore1_0.anchoruuid
LEFT JOIN hs_office_contact_rv c1_0 ON c1_0.uuid=hore1_0.contactuuid LEFT JOIN hs_office.contact_rv c1_0 ON c1_0.uuid=hore1_0.contactuuid
LEFT JOIN hs_office_person_rv h1_0 ON h1_0.uuid=hore1_0.holderuuid LEFT JOIN hs_office.person_rv h1_0 ON h1_0.uuid=hore1_0.holderuuid
WHERE hore1_0.uuid=$1 WHERE hore1_0.uuid=$1
``` ```
That query on the `hs_office_relation_rv`-table joins the three references anchor-person, holder-person and contact. That query on the `hs_office.relation_rv`-table joins the three references anchor-person, holder-person and contact.
### Total-Query-Time > Total-Import-Runtime ### Total-Query-Time > Total-Import-Runtime
@ -239,7 +239,7 @@ This did not improve the performance.
We were suspicious about the sequential scan over all `rbacpermission` rows which was done by PostgreSQL to execute a HashJoin strategy. Turning off that strategy by We were suspicious about the sequential scan over all `rbacpermission` rows which was done by PostgreSQL to execute a HashJoin strategy. Turning off that strategy by
```SQL ```SQL
ALTER FUNCTION queryAccessibleObjectUuidsOfSubjectIds SET enable_hashjoin = off; ALTER FUNCTION rbac.queryAccessibleObjectUuidsOfSubjectIds SET enable_hashjoin = off;
``` ```
did not improve the performance though. The HashJoin was actually still applied, but no full table scan anymore: did not improve the performance though. The HashJoin was actually still applied, but no full table scan anymore:
@ -270,21 +270,21 @@ At this point, the import took 21mins with these statistics:
| query | calls | total_m | mean_ms | | query | calls | total_m | mean_ms |
|-------|-------|---------|---------| |-------|-------|---------|---------|
| select hore1_0.uuid,a1_0.uuid,a1_0.familyname,a1_0.givenname,a1_0.persontype,a1_0.salutation,a1_0.title,a1_0.tradename,a1_0.version,c1_0.uuid,c1_0.caption,c1_0.emailaddresses,c1_0.phonenumbers,c1_0.postaladdress, c1_0.version,h1_0.uuid,h1_0.familyname,h1_0.givenname,h1_0.persontype,h1_0.salutation,h1_0.title,h1_0.tradename,h1_0.version,hore1_0.mark,hore1_0.type,hore1_0.version from public.hs_office_relation_rv hore1_0 left join public.hs_office_person_rv a1_0 on a1_0.uuid=hore1_0.anchoruuid left join public.hs_office_contact_rv c1_0 on c1_0.uuid=hore1_0.contactuuid left join public.hs_office_person_rv h1_0 on h1_0.uuid=hore1_0.holderuuid where hore1_0.uuid=$1 | 517 | 11 | 1282 | | select hore1_0.uuid,a1_0.uuid,a1_0.familyname,a1_0.givenname,a1_0.persontype,a1_0.salutation,a1_0.title,a1_0.tradename,a1_0.version,c1_0.uuid,c1_0.caption,c1_0.emailaddresses,c1_0.phonenumbers,c1_0.postaladdress, c1_0.version,h1_0.uuid,h1_0.familyname,h1_0.givenname,h1_0.persontype,h1_0.salutation,h1_0.title,h1_0.tradename,h1_0.version,hore1_0.mark,hore1_0.type,hore1_0.version from public.hs_office.relation_rv hore1_0 left join public.hs_office.person_rv a1_0 on a1_0.uuid=hore1_0.anchoruuid left join public.hs_office.contact_rv c1_0 on c1_0.uuid=hore1_0.contactuuid left join public.hs_office.person_rv h1_0 on h1_0.uuid=hore1_0.holderuuid where hore1_0.uuid=$1 | 517 | 11 | 1282 |
| select hope1_0.uuid,hope1_0.familyname,hope1_0.givenname,hope1_0.persontype,hope1_0.salutation,hope1_0.title,hope1_0.tradename,hope1_0.version from public.hs_office_person_rv hope1_0 where hope1_0.uuid=$1 | 973 | 4 | 254 | | select hope1_0.uuid,hope1_0.familyname,hope1_0.givenname,hope1_0.persontype,hope1_0.salutation,hope1_0.title,hope1_0.tradename,hope1_0.version from public.hs_office.person_rv hope1_0 where hope1_0.uuid=$1 | 973 | 4 | 254 |
| select hoce1_0.uuid,hoce1_0.caption,hoce1_0.emailaddresses,hoce1_0.phonenumbers,hoce1_0.postaladdress,hoce1_0.version from public.hs_office_contact_rv hoce1_0 where hoce1_0.uuid=$1 | 973 | 4 | 253 | | select hoce1_0.uuid,hoce1_0.caption,hoce1_0.emailaddresses,hoce1_0.phonenumbers,hoce1_0.postaladdress,hoce1_0.version from public.hs_office.contact_rv hoce1_0 where hoce1_0.uuid=$1 | 973 | 4 | 253 |
| call grantRoleToRole(roleUuid, superRoleUuid, superRoleDesc.assumed) | 31316 | 0 | 1 | | call rbac.grantRoleToRole(roleUuid, superRoleUuid, superRoleDesc.assumed) | 31316 | 0 | 1 |
| call buildRbacSystemForHsHostingAsset(NEW) | 2258 | 0 | 7 | | call buildRbacSystemForHsHostingAsset(NEW) | 2258 | 0 | 7 |
| select * from isGranted(array[granteeId], grantedId) | 44613 | 0 | 0 | | select * from rbac.isGranted(array[granteeId], grantedId) | 44613 | 0 | 0 |
| insert into public.hs_hosting_asset_rv (alarmcontactuuid,assignedtoassetuuid,bookingitemuuid,caption,config,identifier,parentassetuuid,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) | 2207 | 0 | 7 | | insert into public.hs_hosting.asset_rv (alarmcontactuuid,assignedtoassetuuid,bookingitemuuid,caption,config,identifier,parentassetuuid,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) | 2207 | 0 | 7 |
| insert into hs_hosting_asset (alarmcontactuuid, version, bookingitemuuid, type, parentassetuuid, assignedtoassetuuid, config, uuid, identifier, caption) values (new.alarmcontactuuid, new. version, new. bookingitemuuid, new. type, new. parentassetuuid, new. assignedtoassetuuid, new. config, new. uuid, new. identifier, new. caption) returning * | 2207 | 0 | 7 | | insert into hs_hosting.asset (alarmcontactuuid, version, bookingitemuuid, type, parentassetuuid, assignedtoassetuuid, config, uuid, identifier, caption) values (new.alarmcontactuuid, new. version, new. bookingitemuuid, new. type, new. parentassetuuid, new. assignedtoassetuuid, new. config, new. uuid, new. identifier, new. caption) returning * | 2207 | 0 | 7 |
| insert into public.hs_office_relation_rv (anchoruuid,contactuuid,holderuuid,mark,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7) | 1261 | 0 | 9 | | insert into public.hs_office.relation_rv (anchoruuid,contactuuid,holderuuid,mark,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7) | 1261 | 0 | 9 |
| insert into hs_office_relation (uuid, version, anchoruuid, holderuuid, contactuuid, type, mark) values (new.uuid, new. version, new. anchoruuid, new. holderuuid, new. contactuuid, new. type, new. mark) returning * | 1261 | 0 | 9 | | insert into hs_office.relation (uuid, version, anchoruuid, holderuuid, contactuuid, type, mark) values (new.uuid, new. version, new. anchoruuid, new. holderuuid, new. contactuuid, new. type, new. mark) returning * | 1261 | 0 | 9 |
| call buildRbacSystemForHsOfficeRelation(NEW) | 1276 | 0 | 8 | | call buildRbacSystemForHsOfficeRelation(NEW) | 1276 | 0 | 8 |
| with recursive grants as ( select descendantUuid, ascendantUuid from RbacGrants where descendantUuid = grantedId union all select ""grant"".descendantUuid, ""grant"".ascendantUuid from RbacGrants ""grant"" inner join grants recur on recur.ascendantUuid = ""grant"".descendantUuid ) select exists ( select $3 from grants where ascendantUuid = any(granteeIds) ) or grantedId = any(granteeIds) | 47540 | 0 | 0 | | with recursive grants as ( select descendantUuid, ascendantUuid from RbacGrants where descendantUuid = grantedId union all select ""grant"".descendantUuid, ""grant"".ascendantUuid from RbacGrants ""grant"" inner join grants recur on recur.ascendantUuid = ""grant"".descendantUuid ) select exists ( select $3 from grants where ascendantUuid = any(granteeIds) ) or grantedId = any(granteeIds) | 47540 | 0 | 0 |
| insert into RbacGrants (grantedByTriggerOf, ascendantuuid, descendantUuid, assumed) values (currentTriggerObjectUuid(), superRoleId, subRoleId, doAssume) on conflict do nothing" | 40472 | 0 | 0 | | insert into RbacGrants (grantedByTriggerOf, ascendantuuid, descendantUuid, assumed) values (currentTriggerObjectUuid(), superRoleId, subRoleId, doAssume) on conflict do nothing" | 40472 | 0 | 0 |
| insert into public.hs_booking_item_rv (caption,parentitemuuid,projectuuid,resources,type,validity,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8) | 926 | 0 | 7 | | insert into public.hs_booking.item_rv (caption,parentitemuuid,projectuuid,resources,type,validity,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8) | 926 | 0 | 7 |
| insert into hs_booking_item (resources, version, projectuuid, type, parentitemuuid, validity, uuid, caption) values (new.resources, new. version, new. projectuuid, new. type, new. parentitemuuid, new. validity, new. uuid, new. caption) returning * | 926 | 0 | 7 | | insert into hs_booking.item (resources, version, projectuuid, type, parentitemuuid, validity, uuid, caption) values (new.resources, new. version, new. projectuuid, new. type, new. parentitemuuid, new. validity, new. uuid, new. caption) returning * | 926 | 0 | 7 |
The slowest query now was fetching Relations joined with Contact, Anchor-Person and Holder-Person, for all tables using the restricted (RBAC) views (_rv). The slowest query now was fetching Relations joined with Contact, Anchor-Person and Holder-Person, for all tables using the restricted (RBAC) views (_rv).
@ -294,20 +294,20 @@ We changed these mappings from `EAGER` (default) to `LAZY` to `@ManyToOne(fetch
:::small :::small
| query | calls | total (min) | mean (ms) | | query | calls | total (min) | mean (ms) |
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------|-------------|----------| |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------|-------------|----------|
| select hope1_0.uuid,hope1_0.familyname,hope1_0.givenname,hope1_0.persontype,hope1_0.salutation,hope1_0.title,hope1_0.tradename,hope1_0.version from public.hs_office_person_rv hope1_0 where hope1_0.uuid=$1 | 1015 | 4 | 238 | | select hope1_0.uuid,hope1_0.familyname,hope1_0.givenname,hope1_0.persontype,hope1_0.salutation,hope1_0.title,hope1_0.tradename,hope1_0.version from public.hs_office.person_rv hope1_0 where hope1_0.uuid=$1 | 1015 | 4 | 238 |
| select hore1_0.uuid,hore1_0.anchoruuid,hore1_0.contactuuid,hore1_0.holderuuid,hore1_0.mark,hore1_0.type,hore1_0.version from public.hs_office_relation_rv hore1_0 where hore1_0.uuid=$1 | 517 | 4 | 439 | | select hore1_0.uuid,hore1_0.anchoruuid,hore1_0.contactuuid,hore1_0.holderuuid,hore1_0.mark,hore1_0.type,hore1_0.version from public.hs_office.relation_rv hore1_0 where hore1_0.uuid=$1 | 517 | 4 | 439 |
| select hoce1_0.uuid,hoce1_0.caption,hoce1_0.emailaddresses,hoce1_0.phonenumbers,hoce1_0.postaladdress,hoce1_0.version from public.hs_office_contact_rv hoce1_0 where hoce1_0.uuid=$1 | 497 | 2 | 213 | | select hoce1_0.uuid,hoce1_0.caption,hoce1_0.emailaddresses,hoce1_0.phonenumbers,hoce1_0.postaladdress,hoce1_0.version from public.hs_office.contact_rv hoce1_0 where hoce1_0.uuid=$1 | 497 | 2 | 213 |
| call grantRoleToRole(roleUuid, superRoleUuid, superRoleDesc.assumed) | 31316 | 0 | 1 | | call rbac.grantRoleToRole(roleUuid, superRoleUuid, superRoleDesc.assumed) | 31316 | 0 | 1 |
| select * from isGranted(array[granteeId], grantedId) | 44613 | 0 | 0 | | select * from rbac.isGranted(array[granteeId], grantedId) | 44613 | 0 | 0 |
| call buildRbacSystemForHsHostingAsset(NEW) | 2258 | 0 | 7 | | call buildRbacSystemForHsHostingAsset(NEW) | 2258 | 0 | 7 |
| insert into public.hs_hosting_asset_rv (alarmcontactuuid,assignedtoassetuuid,bookingitemuuid,caption,config,identifier,parentassetuuid,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) | 2207 | 0 | 7 | | insert into public.hs_hosting.asset_rv (alarmcontactuuid,assignedtoassetuuid,bookingitemuuid,caption,config,identifier,parentassetuuid,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) | 2207 | 0 | 7 |
| insert into hs_hosting_asset (alarmcontactuuid, version, bookingitemuuid, type, parentassetuuid, assignedtoassetuuid, config, uuid, identifier, caption) values (new.alarmcontactuuid, new. version, new. bookingitemuuid, new. type, new. parentassetuuid, new. assignedtoassetuuid, new. config, new. uuid, new. identifier, new. caption) returning * | 2207 | 0 | 7 | | insert into hs_hosting.asset (alarmcontactuuid, version, bookingitemuuid, type, parentassetuuid, assignedtoassetuuid, config, uuid, identifier, caption) values (new.alarmcontactuuid, new. version, new. bookingitemuuid, new. type, new. parentassetuuid, new. assignedtoassetuuid, new. config, new. uuid, new. identifier, new. caption) returning * | 2207 | 0 | 7 |
| with recursive grants as ( select descendantUuid, ascendantUuid from RbacGrants where descendantUuid = grantedId union all select ""grant"".descendantUuid, ""grant"".ascendantUuid from RbacGrants ""grant"" inner join grants recur on recur.ascendantUuid = ""grant"".descendantUuid ) select exists ( select $3 from grants where ascendantUuid = any(granteeIds) ) or grantedId = any(granteeIds) | 47538 | 0 | 0 | | with recursive grants as ( select descendantUuid, ascendantUuid from RbacGrants where descendantUuid = grantedId union all select ""grant"".descendantUuid, ""grant"".ascendantUuid from RbacGrants ""grant"" inner join grants recur on recur.ascendantUuid = ""grant"".descendantUuid ) select exists ( select $3 from grants where ascendantUuid = any(granteeIds) ) or grantedId = any(granteeIds) | 47538 | 0 | 0 |
insert into public.hs_office_relation_rv (anchoruuid,contactuuid,holderuuid,mark,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7) | 1261 | 0 | 8 | insert into public.hs_office.relation_rv (anchoruuid,contactuuid,holderuuid,mark,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7) | 1261 | 0 | 8 |
| insert into hs_office_relation (uuid, version, anchoruuid, holderuuid, contactuuid, type, mark) values (new.uuid, new. version, new. anchoruuid, new. holderuuid, new. contactuuid, new. type, new. mark) returning * | 1261 | 0 | 8 | | insert into hs_office.relation (uuid, version, anchoruuid, holderuuid, contactuuid, type, mark) values (new.uuid, new. version, new. anchoruuid, new. holderuuid, new. contactuuid, new. type, new. mark) returning * | 1261 | 0 | 8 |
| call buildRbacSystemForHsOfficeRelation(NEW) | 1276 | 0 | 7 | | call buildRbacSystemForHsOfficeRelation(NEW) | 1276 | 0 | 7 |
| insert into public.hs_booking_item_rv (caption,parentitemuuid,projectuuid,resources,type,validity,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8) | 926 | 0 | 7 | | insert into public.hs_booking.item_rv (caption,parentitemuuid,projectuuid,resources,type,validity,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8) | 926 | 0 | 7 |
| insert into hs_booking_item (resources, version, projectuuid, type, parentitemuuid, validity, uuid, caption) values (new.resources, new. version, new. projectuuid, new. type, new. parentitemuuid, new. validity, new. uuid, new. caption) returning * | 926 | 0 | 7 | | insert into hs_booking.item (resources, version, projectuuid, type, parentitemuuid, validity, uuid, caption) values (new.resources, new. version, new. projectuuid, new. type, new. parentitemuuid, new. validity, new. uuid, new. caption) returning * | 926 | 0 | 7 |
insert into RbacGrants (grantedByTriggerOf, ascendantuuid, descendantUuid, assumed) values (currentTriggerObjectUuid(), superRoleId, subRoleId, doAssume) on conflict do nothing | 40472 | 0 | 0 | insert into RbacGrants (grantedByTriggerOf, ascendantuuid, descendantUuid, assumed) values (currentTriggerObjectUuid(), superRoleId, subRoleId, doAssume) on conflict do nothing | 40472 | 0 | 0 |
Now, finally, the total runtime of the import was down to 12 minutes. This is repeatable, where originally, the import took about 25mins in most cases and just rarely - and for unknown reasons - 10min. Now, finally, the total runtime of the import was down to 12 minutes. This is repeatable, where originally, the import took about 25mins in most cases and just rarely - and for unknown reasons - 10min.
@ -318,7 +318,7 @@ But once UnixUser and EmailAlias assets got added to the import, the total time
This was not acceptable, especially not, considering that domains, email-addresses and database-assets are almost 10 times that number and thus the import would go up to over 1100min which is 20 hours. This was not acceptable, especially not, considering that domains, email-addresses and database-assets are almost 10 times that number and thus the import would go up to over 1100min which is 20 hours.
In a first step, a `HsHostingAssetRawEntity` was created, mapped to the raw table (hs_hosting_asset) not to the RBAC-view (hs_hosting_asset_rv). Unfortunately we did not keep measurements, but that was only part of the problem anyway. In a first step, a `HsHostingAssetRawEntity` was created, mapped to the raw table (hs_hosting.asset) not to the RBAC-view (hs_hosting.asset_rv). Unfortunately we did not keep measurements, but that was only part of the problem anyway.
The main problem was, that there is something strange with persisting (`EntityManager.persist`) for EmailAlias assets. Where importing UnixUsers was mostly slow due to RBAC SELECT-permission checks, persisting EmailAliases suddenly created about a million (in numbers 1.000.000) SQL UPDATE statements after the INSERT, all with the same data, just increased version number (used for optimistic locking). We were not able to figure out why this happened. The main problem was, that there is something strange with persisting (`EntityManager.persist`) for EmailAlias assets. Where importing UnixUsers was mostly slow due to RBAC SELECT-permission checks, persisting EmailAliases suddenly created about a million (in numbers 1.000.000) SQL UPDATE statements after the INSERT, all with the same data, just increased version number (used for optimistic locking). We were not able to figure out why this happened.
@ -330,22 +330,22 @@ Now, the longest running queries are these:
| No.| calls | total_m | mean_ms | query | | No.| calls | total_m | mean_ms | query |
|---:|---------|--------:|--------:|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |---:|---------|--------:|--------:|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 | 13.093 | 4 | 21 | insert into hs_hosting_asset( uuid, type, bookingitemuuid, parentassetuuid, assignedtoassetuuid, alarmcontactuuid, identifier, caption, config, version) values ( $1, $2, $3, $4, $5, $6, $7, $8, cast($9 as jsonb), $10) | | 1 | 13.093 | 4 | 21 | insert into hs_hosting.asset( uuid, type, bookingitemuuid, parentassetuuid, assignedtoassetuuid, alarmcontactuuid, identifier, caption, config, version) values ( $1, $2, $3, $4, $5, $6, $7, $8, cast($9 as jsonb), $10) |
| 2 | 517 | 4 | 502 | select hore1_0.uuid,hore1_0.anchoruuid,hore1_0.contactuuid,hore1_0.holderuuid,hore1_0.mark,hore1_0.type,hore1_0.version from public.hs_office_relation_rv hore1_0 where hore1_0.uuid=$1 | | 2 | 517 | 4 | 502 | select hore1_0.uuid,hore1_0.anchoruuid,hore1_0.contactuuid,hore1_0.holderuuid,hore1_0.mark,hore1_0.type,hore1_0.version from public.hs_office.relation_rv hore1_0 where hore1_0.uuid=$1 |
| 3 | 13.144 | 4 | 21 | call buildRbacSystemForHsHostingAsset(NEW) | | 3 | 13.144 | 4 | 21 | call buildRbacSystemForHsHostingAsset(NEW) |
| 4 | 96.632 | 3 | 2 | call grantRoleToRole(roleUuid, superRoleUuid, superRoleDesc.assumed) | | 4 | 96.632 | 3 | 2 | call rbac.grantRoleToRole(roleUuid, superRoleUuid, superRoleDesc.assumed) |
| 5 | 120.815 | 3 | 2 | select * from isGranted(array[granteeId], grantedId) | | 5 | 120.815 | 3 | 2 | select * from rbac.isGranted(array[granteeId], grantedId) |
| 6 | 123.740 | 3 | 2 | with recursive grants as ( select descendantUuid, ascendantUuid from RbacGrants where descendantUuid = grantedId union all select "grant".descendantUuid, "grant".ascendantUuid from RbacGrants "grant" inner join grants recur on recur.ascendantUuid = "grant".descendantUuid ) select exists ( select $3 from grants where ascendantUuid = any(granteeIds) ) or grantedId = any(granteeIds) | | 6 | 123.740 | 3 | 2 | with recursive grants as ( select descendantUuid, ascendantUuid from RbacGrants where descendantUuid = grantedId union all select "grant".descendantUuid, "grant".ascendantUuid from RbacGrants "grant" inner join grants recur on recur.ascendantUuid = "grant".descendantUuid ) select exists ( select $3 from grants where ascendantUuid = any(granteeIds) ) or grantedId = any(granteeIds) |
| 7 | 497 | 2 | 259 | select hoce1_0.uuid,hoce1_0.caption,hoce1_0.emailaddresses,hoce1_0.phonenumbers,hoce1_0.postaladdress,hoce1_0.version from public.hs_office_contact_rv hoce1_0 where hoce1_0.uuid=$1 | | 7 | 497 | 2 | 259 | select hoce1_0.uuid,hoce1_0.caption,hoce1_0.emailaddresses,hoce1_0.phonenumbers,hoce1_0.postaladdress,hoce1_0.version from public.hs_office.contact_rv hoce1_0 where hoce1_0.uuid=$1 |
| 8 | 497 | 2 | 255 | select hope1_0.uuid,hope1_0.familyname,hope1_0.givenname,hope1_0.persontype,hope1_0.salutation,hope1_0.title,hope1_0.tradename,hope1_0.version from public.hs_office_person_rv hope1_0 where hope1_0.uuid=$1 | | 8 | 497 | 2 | 255 | select hope1_0.uuid,hope1_0.familyname,hope1_0.givenname,hope1_0.persontype,hope1_0.salutation,hope1_0.title,hope1_0.tradename,hope1_0.version from public.hs_office.person_rv hope1_0 where hope1_0.uuid=$1 |
| 9 | 13.144 | 1 | 8 | SELECT createRoleWithGrants( hsHostingAssetTENANT(NEW), permissions => array[$7], incomingSuperRoles => array[ hsHostingAssetAGENT(NEW), hsOfficeContactADMIN(newAlarmContact)], outgoingSubRoles => array[ hsBookingItemTENANT(newBookingItem), hsHostingAssetTENANT(newParentAsset)] ) | | 9 | 13.144 | 1 | 8 | SELECT createRoleWithGrants( hs_hosting.asset_TENANT(NEW), permissions => array[$7], incomingSuperRoles => array[ hs_hosting.asset_AGENT(NEW), hs_office.contact_ADMIN(newAlarmContact)], outgoingSubRoles => array[ hs_booking.item_TENANT(newBookingItem), hs_hosting.asset_TENANT(newParentAsset)] ) |
| 10 | 13.144 | 1 | 5 | SELECT createRoleWithGrants( hsHostingAssetADMIN(NEW), permissions => array[$7], incomingSuperRoles => array[ hsBookingItemAGENT(newBookingItem), hsHostingAssetAGENT(newParentAsset), hsHostingAssetOWNER(NEW)] ) | | 10 | 13.144 | 1 | 5 | SELECT createRoleWithGrants( hs_hosting.asset_ADMIN(NEW), permissions => array[$7], incomingSuperRoles => array[ hs_booking.item_AGENT(newBookingItem), hs_hosting.asset_AGENT(newParentAsset), hs_hosting.asset_OWNER(NEW)] ) |
That the `INSERT into hs_hosting_asset` (No. 1) takes up the most time, seems to be normal, and 21ms for each call is also fine. That the `INSERT into hs_hosting.asset` (No. 1) takes up the most time, seems to be normal, and 21ms for each call is also fine.
It seems that the trigger effects (eg. No. 3 and No. 4) are included in the measure for the causing INSERT, otherwise summing up the totals would exceed the actual total time of the whole import. And it was to be expected that building the RBAC rules for new business objects takes most of the time. It seems that the trigger effects (eg. No. 3 and No. 4) are included in the measure for the causing INSERT, otherwise summing up the totals would exceed the actual total time of the whole import. And it was to be expected that building the RBAC rules for new business objects takes most of the time.
In production, the `SELECT ... FROM hs_office_relation_rv` (No. 2) with about 0.5 seconds could still be a problem. But once we apply the improvements from the hosting asset area also to the office area, this should not be a problem for the import anymore. In production, the `SELECT ... FROM hs_office.relation_rv` (No. 2) with about 0.5 seconds could still be a problem. But once we apply the improvements from the hosting asset area also to the office area, this should not be a problem for the import anymore.
## Further Options To Explore ## Further Options To Explore
@ -392,9 +392,9 @@ We found some solution approaches:
3. Inverting the recursion of the CTE-query, combined with the type condition. 3. Inverting the recursion of the CTE-query, combined with the type condition.
Instead of starting the recursion with `currentsubjectsuuids()`, Instead of starting the recursion with `currentSubjectOrAssumedRolesUuids()`,
we could start it with the target table name and row-type, we could start it with the target table name and row-type,
then recurse down to the `currentsubjectsuuids()`. then recurse down to the `currentSubjectOrAssumedRolesUuids()`.
In the end, we need the object UUIDs, though. In the end, we need the object UUIDs, though.
But if we start with the join of `rbacObject` with `rbacPermission`, But if we start with the join of `rbacObject` with `rbacPermission`,
@ -408,12 +408,12 @@ We found some solution approaches:
This optimization idea came from Michael Hierweck and was promising. This optimization idea came from Michael Hierweck and was promising.
The idea is to reduce the size of the result of the recursive CTE query and maybe even speed up that query itself. The idea is to reduce the size of the result of the recursive CTE query and maybe even speed up that query itself.
To evaluate this, I added a type column to the `rbacObject` table, initially as an enum hsHostingAssetType. Then I entered the type there for all rows from hs_hosting_asset. This means that 83,886 of 92,545 rows in `rbacobject` have a type set, leaving 8,659 without. To evaluate this, I added a type column to the `rbacObject` table, initially as an enum hsHostingAssetType. Then I entered the type there for all rows from hs_hosting.asset. This means that 83,886 of 92,545 rows in `rbacobject` have a type set, leaving 8,659 without.
If we do this for other types (we currently have 1,271 relations and 927 booking items), it gets more complicated because they are different enum types. As varchar(16), we could lose performance again due to the higher storage space requirements. If we do this for other types (we currently have 1,271 relations and 927 booking items), it gets more complicated because they are different enum types. As varchar(16), we could lose performance again due to the higher storage space requirements.
But the performance gained is not particularly high anyway. But the performance gained is not particularly high anyway.
See the average seconds per recursive CTE select as role 'hs_hosting_asset:<DEBITOR>defaultproject:ADMIN', See the average seconds per recursive CTE select as role 'hs_hosting.asset:<DEBITOR>defaultproject:ADMIN',
joined with business query for all `'EMAIL_ADDRESSES'`: joined with business query for all `'EMAIL_ADDRESSES'`:
| | D-1000000-hsh | D-1000300-mih | | | D-1000000-hsh | D-1000300-mih |

View File

@ -29,7 +29,7 @@ skinparam linetype ortho
package RBAC { package RBAC {
' forward declarations ' forward declarations
entity RbacUser entity RbacSubject
together { together {
@ -37,8 +37,8 @@ package RBAC {
entity RbacPermission entity RbacPermission
RbacUser -[hidden]> RbacRole RbacSubject -[hidden]> RbacRole
RbacRole -[hidden]> RbacUser RbacRole -[hidden]> RbacSubject
} }
together { together {
@ -57,11 +57,11 @@ package RBAC {
RbacGrant o-u-> RbacReference RbacGrant o-u-> RbacReference
enum RbacReferenceType { enum RbacReferenceType {
RbacUser RbacSubject
RbacRole RbacRole
RbacPermission RbacPermission
} }
RbacReferenceType ..> RbacUser RbacReferenceType ..> RbacSubject
RbacReferenceType ..> RbacRole RbacReferenceType ..> RbacRole
RbacReferenceType ..> RbacPermission RbacReferenceType ..> RbacPermission
@ -71,12 +71,12 @@ package RBAC {
type : RbacReferenceType type : RbacReferenceType
} }
RbacReference o--> RbacReferenceType RbacReference o--> RbacReferenceType
entity RbacUser { entity RbacSubject {
*uuid : uuid <<generated>> *uuid : uuid <<generated>>
-- --
name : varchar name : varchar
} }
RbacUser o-- RbacReference RbacSubject o-- RbacReference
entity RbacRole { entity RbacRole {
*uuid : uuid(RbacReference) *uuid : uuid(RbacReference)
@ -143,20 +143,20 @@ The primary key of the *RbacReference* and its referred object is always identic
#### RbacReferenceType #### RbacReferenceType
The enum *RbacReferenceType* describes the type of reference. The enum *RbacReferenceType* describes the type of reference.
It's only needed to make it easier to find the referred object in *RbacUser*, *RbacRole* or *RbacPermission*. It's only needed to make it easier to find the referred object in *RbacSubject*, *RbacRole* or *RbacPermission*.
#### RbacUser #### RbacSubject
An *RbacUser* is a type of RBAC-subject which references a login account outside this system, identified by a name (usually an email-address). An *RbacSubject* is a type of RBAC-subject which references a login account outside this system, identified by a name (usually an email-address).
*RbacUser*s can be assigned to multiple *RbacRole*s, through which they can get permissions to *RbacObject*s. *RbacSubject*s can be assigned to multiple *RbacRole*s, through which they can get permissions to *RbacObject*s.
The primary key of the *RbacUser* is identical to its related *RbacReference*. The primary key of the *RbacSubject* is identical to its related *RbacReference*.
#### RbacRole #### RbacRole
An *RbacRole* represents a collection of directly or indirectly assigned *RbacPermission*s. An *RbacRole* represents a collection of directly or indirectly assigned *RbacPermission*s.
Each *RbacRole* can be assigned to *RbacUser*s or to another *RbacRole*. Each *RbacRole* can be assigned to *RbacSubject*s or to another *RbacRole*.
Both kinds of assignments are represented via *RbacGrant*. Both kinds of assignments are represented via *RbacGrant*.
@ -184,7 +184,7 @@ Only with this rule, the foreign key in *RbacPermission* can be defined as `NOT
#### RbacGrant #### RbacGrant
The *RbacGrant* entities represent the access-rights structure from *RbacUser*s via hierarchical *RbacRoles* down to *RbacPermission*s. The *RbacGrant* entities represent the access-rights structure from *RbacSubject*s via hierarchical *RbacRoles* down to *RbacPermission*s.
The core SQL queries to determine access rights are all recursive queries on the *RbacGrant* table. The core SQL queries to determine access rights are all recursive queries on the *RbacGrant* table.
@ -284,7 +284,7 @@ hide circle
' use right-angled line routing ' use right-angled line routing
' skinparam linetype ortho ' skinparam linetype ortho
package RbacUsers { package RbacSubjects {
object UserMike object UserMike
object UserSuse object UserSuse
object UserPaul object UserPaul
@ -296,7 +296,7 @@ package RbacRoles {
object RoleCustXyz_Admin object RoleCustXyz_Admin
object RolePackXyz00_Owner object RolePackXyz00_Owner
} }
RbacUsers -[hidden]> RbacRoles RbacSubjects -[hidden]> RbacRoles
package RbacPermissions { package RbacPermissions {
object PermCustXyz_SELECT object PermCustXyz_SELECT
@ -364,10 +364,10 @@ This way, each user can only select the data they have 'SELECT'-permission for,
### Current User ### Current User
The current use is taken from the session variable `hsadminng.currentUser` which contains the name of the user as stored in the The current use is taken from the session variable `hsadminng.currentSubject` which contains the name of the user as stored in the
*RbacUser*s table. Example: *RbacSubject*s table. Example:
SET LOCAL hsadminng.currentUser = 'mike@hostsharing.net'; SET LOCAL hsadminng.currentSubject = 'mike@hostsharing.net';
That user is also used for historicization and audit log, but which is a different topic. That user is also used for historicization and audit log, but which is a different topic.
@ -388,7 +388,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.currentSubject = '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"
@ -605,8 +605,8 @@ Find the SQL script here: `28-hs-tests.sql`.
We have tested two variants of the query for the restricted view, We have tested two variants of the query for the restricted view,
both utilizing a PostgreSQL function like this: both utilizing a PostgreSQL function like this:
FUNCTION queryAccessibleObjectUuidsOfSubjectIds( FUNCTION rbac.queryAccessibleObjectUuidsOfSubjectIds(
requiredOp RbacOp, requiredOp rbac.RbacOp,
forObjectTable varchar, forObjectTable varchar,
subjectIds uuid[], subjectIds uuid[],
maxObjects integer = 16000) maxObjects integer = 16000)
@ -623,8 +623,8 @@ Let's have a look at the two view queries:
FROM customer AS target FROM customer AS target
WHERE target.uuid IN ( WHERE target.uuid IN (
SELECT uuid SELECT uuid
FROM queryAccessibleObjectUuidsOfSubjectIds( FROM rbac.queryAccessibleObjectUuidsOfSubjectIds(
'SELECT, 'customer', currentSubjectsUuids())); 'SELECT, 'customer', currentSubjectOrAssumedRolesUuids()));
This view should be automatically updatable. This view should be automatically updatable.
Where, for updates, we actually have to check for 'UPDATE' instead of 'SELECT' 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.
@ -641,8 +641,8 @@ Looks like the query optimizer needed some statistics to find the best path.
CREATE OR REPLACE VIEW customer_rv AS CREATE OR REPLACE VIEW customer_rv AS
SELECT DISTINCT target.* SELECT DISTINCT target.*
FROM customer AS target FROM customer AS target
JOIN queryAccessibleObjectUuidsOfSubjectIds( JOIN rbac.queryAccessibleObjectUuidsOfSubjectIds(
'SELECT, 'customer', currentSubjectsUuids()) AS allowedObjId 'SELECT, 'customer', currentSubjectOrAssumedRolesUuids()) AS allowedObjId
ON target.uuid = allowedObjId; ON target.uuid = allowedObjId;
This view cannot is not updatable automatically, This view cannot is not updatable automatically,
@ -671,9 +671,9 @@ Access Control for business objects checked according to the assigned roles.
But we decided not to create such roles and permissions for the RBAC-Objects itself. But we decided not to create such roles and permissions for the RBAC-Objects itself.
It would have overcomplicated the system and the necessary information can easily be added to the RBAC-Objects itself, mostly the `RbacGrant`s. It would have overcomplicated the system and the necessary information can easily be added to the RBAC-Objects itself, mostly the `RbacGrant`s.
### RbacUser ### RbacSubject
Users can self-register, thus to create a new RbacUser entity, no login is required. Users can self-register, thus to create a new RbacSubject entity, no login is required.
But such a user has no access-rights except viewing itself. But such a user has no access-rights except viewing itself.
Users can view themselves. Users can view themselves.

124
doc/scenarios/template.html Normal file
View File

@ -0,0 +1,124 @@
<!doctype html>
<html $if(lang)$ lang="$lang$" $endif$>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!--[if lt IE 9]>
<script src="http://css3-mediaqueries-js.googlecode.com/svn/trunk/css3-mediaqueries.js"></script>
<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<!-- <link rel="stylesheet" type="text/css" href="template.css" /> -->
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/template.css" />
<link href="https://vjs.zencdn.net/5.4.4/video-js.css" rel="stylesheet" />
<script src="https://code.jquery.com/jquery-2.2.1.min.js"></script>
<!-- <script type='text/javascript' src='menu/js/jquery.cookie.js'></script> -->
<!-- <script type='text/javascript' src='menu/js/jquery.hoverIntent.minified.js'></script> -->
<!-- <script type='text/javascript' src='menu/js/jquery.dcjqaccordion.2.7.min.js'></script> -->
<!-- <link href="menu/css/skins/blue.css" rel="stylesheet" type="text/css" /> -->
<!-- <link href="menu/css/skins/graphite.css" rel="stylesheet" type="text/css" /> -->
<!-- <link href="menu/css/skins/grey.css" rel="stylesheet" type="text/css" /> -->
<!-- <script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script> -->
<!-- <script src="script.js"></script> -->
<!-- <script src="jquery.sticky-kit.js "></script> -->
<script type='text/javascript' src='https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/js/jquery.cookie.js'></script>
<script type='text/javascript' src='https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/js/jquery.hoverIntent.minified.js'></script>
<script type='text/javascript' src='https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/js/jquery.dcjqaccordion.2.7.min.js'></script>
<link href="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/css/skins/blue.css" rel="stylesheet" type="text/css" />
<link href="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/css/skins/graphite.css" rel="stylesheet" type="text/css" />
<link href="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/menu/css/skins/grey.css" rel="stylesheet" type="text/css" />
<link href="https://cdn.jsdelivr.net/gh/ryangrose/easy-pandoc-templates@948e28e5/css/elegant_bootstrap.css" rel="stylesheet" type="text/css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.4/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
<script src="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/script.js"></script>
<script src="https://cdn.jsdelivr.net/gh/diversen/pandoc-bootstrap-adaptive-template@959c3622/jquery.sticky-kit.js"></script>
<meta name="generator" content="pandoc" />
$for(author-meta)$
<meta name="author" content="$author-meta$" />
$endfor$
$if(date-meta)$
<meta name="date" content="$date-meta$" />
$endif$
<title>$if(title-prefix)$$title-prefix$ - $endif$$pagetitle$</title>
<style type="text/css">code{white-space: pre;}</style>
$if(quotes)$
<style type="text/css">q { quotes: "“" "”" "" ""; }</style>
$endif$
$if(highlighting-css)$
<style type="text/css">
$highlighting-css$
</style>
$endif$
$for(css)$
<link rel="stylesheet" href="$css$" $if(html5)$$else$type="text/css" $endif$/>
$endfor$
$if(math)$
$math$
$endif$
$for(header-includes)$
$header-includes$
$endfor$
</head>
<body>
$if(title)$
<div class="navbar navbar-static-top">
<div class="navbar-inner">
<div class="container">
<span class="doc-title">$title$</span>
<ul class="nav pull-right doc-info">
$for(author)$
<li><p class="navbar-text">$author$</p></li>
$endfor$
$if(date)$
<li><p class="navbar-text">$date$</p></li>
$endif$
</ul>
</div>
</div>
</div>
$endif$
<div class="container">
<div class="row">
$if(toc)$
<div id="$idprefix$TOC" class="span3">
<div class="well toc">
$toc$
</div>
</div>
$endif$
<div class="span$if(toc)$9$else$12$endif$">
$if(abstract)$
<H1>$abstract-title$</H1>
$abstract$
$endif$
$for(include-before)$
$include-before$
$endfor$
$body$
$for(include-after)$
$include-after$
$endfor$
</div>
</div>
</div>
<script src="https://vjs.zencdn.net/5.4.4/video.js"></script>
</body>
</html>

View File

@ -90,6 +90,20 @@ Acceptance-tests, are blackbox-tests and do <u>not</u> count into test-code-cove
TODO.test: Complete the Acceptance-Tests test concept. TODO.test: Complete the Acceptance-Tests test concept.
#### Scenario-Tests
Our Scenario-tests are induced by business use-cases.
They test from the REST API all the way down to the database.
Most scenario-tests are positive tests, they test if business scenarios do work.
But few might be negative tests, which test if specific forbidden data gets rejected.
Our scenario tests also generate test-reports which contain the REST-API calls needed for each scenario.
These reports can be used as examples for the API usage from a business perspective.
There is an extra document regarding scenario-test, see [Scenario-Tests README](../src/test/java/net/hostsharing/hsadminng/hs/office/scenarios/README.md).
#### Performance-Tests #### Performance-Tests
Performance-critical scenarios have to be identified and a special performance-test has to be implemented. Performance-critical scenarios have to be identified and a special performance-test has to be implemented.

View File

@ -1,8 +1,10 @@
{ {
"allowedLicenses": [ "allowedLicenses": [
{ "moduleLicense": "Apache 2.0" },
{ "moduleLicense": "Apache 2" }, { "moduleLicense": "Apache 2" },
{ "moduleLicense": "Apache 2.0" },
{ "moduleLicense": "Apache-2.0" },
{ "moduleLicense": "Apache License 2.0" }, { "moduleLicense": "Apache License 2.0" },
{ "moduleLicense": "Apache License v2.0" },
{ "moduleLicense": "Apache License, Version 2.0" }, { "moduleLicense": "Apache License, Version 2.0" },
{ "moduleLicense": "The Apache Software License, Version 2.0" }, { "moduleLicense": "The Apache Software License, Version 2.0" },
@ -11,6 +13,8 @@
{ "moduleLicense": "BSD-3-Clause" }, { "moduleLicense": "BSD-3-Clause" },
{ "moduleLicense": "The BSD License" }, { "moduleLicense": "The BSD License" },
{ "moduleLicense": "The New BSD License" },
{ "moduleLicense": "CDDL 1.1" }, { "moduleLicense": "CDDL 1.1" },
{ "moduleLicense": "CDDL/GPLv2+CE" }, { "moduleLicense": "CDDL/GPLv2+CE" },
{ "moduleLicense": "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0" }, { "moduleLicense": "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0" },
@ -29,11 +33,22 @@
{ "moduleLicense": "GNU General Public License, version 2 with the GNU Classpath Exception" }, { "moduleLicense": "GNU General Public License, version 2 with the GNU Classpath Exception" },
{ "moduleLicense": "GPL2 w/ CPE" }, { "moduleLicense": "GPL2 w/ CPE" },
{ "moduleLicense": "LGPL, version 2.1"},
{ "moduleLicense": "LGPL-2.1-or-later"},
{ "moduleLicense": "MIT License" }, { "moduleLicense": "MIT License" },
{ "moduleLicense": "MIT" }, { "moduleLicense": "MIT" },
{ "moduleLicense": "The MIT License (MIT)" }, { "moduleLicense": "The MIT License (MIT)" },
{ "moduleLicense": "The MIT License" }, { "moduleLicense": "The MIT License" },
{ "moduleName": "org.springdoc:springdoc-openapi" } { "moduleLicense": "WTFPL" },
{
"moduleLicense": null,
"#moduleLicense": "Apache License 2.0, see https://github.com/springdoc/springdoc-openapi/blob/main/LICENSE",
"moduleVersion": "2.4.0",
"moduleName": "org.springdoc:springdoc-openapi"
}
] ]
} }

View File

@ -0,0 +1,6 @@
FROM eclipse-temurin:21-jdk
RUN apt-get update && \
apt-get install -y bind9-utils pandoc && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

View File

@ -1,12 +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[
Cyclic references are not possible if file comes in JSON text format.
]]></notes>
<packageUrl regex="true">^pkg:maven/com\.fasterxml\.jackson\.core/jackson\-databind@.*$</packageUrl>
<cpe>cpe:/a:fasterxml:jackson-databind</cpe>
</suppress>
<suppress> <suppress>
<notes><![CDATA[ <notes><![CDATA[
Internal tooling, not exposed to the Internet. Internal tooling, not exposed to the Internet.
@ -14,4 +7,10 @@
<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[
Malicious HTTP redirect in JAXB on a REST-endpoint is not that dangerous.
]]></notes>
<cve>CVE-2024-9329</cve>
</suppress>
</suppressions> </suppressions>

View File

@ -6,34 +6,34 @@
rollback; rollback;
begin transaction; begin transaction;
call defineContext('historization testing', null, 'superuser-alex@hostsharing.net', call defineContext('historization testing', null, 'superuser-alex@hostsharing.net',
-- 'hs_booking_project#D-1000000-hshdefaultproject:ADMIN'); -- prod+test -- 'hs_booking.project#D-1000000-hshdefaultproject:ADMIN'); -- prod+test
'hs_booking_project#D-1000313-D-1000313defaultproject:ADMIN'); -- prod+test 'hs_booking.project#D-1000313-D-1000313defaultproject:ADMIN'); -- prod+test
-- 'hs_booking_project#D-1000300-mihdefaultproject:ADMIN'); -- prod -- 'hs_booking.project#D-1000300-mihdefaultproject:ADMIN'); -- prod
-- 'hs_booking_project#D-1000300-mimdefaultproject:ADMIN'); -- test -- 'hs_booking.project#D-1000300-mimdefaultproject:ADMIN'); -- test
-- update hs_hosting_asset set caption='lug00 b' where identifier = 'lug00' and type = 'MANAGED_WEBSPACE'; -- prod -- update hs_hosting.asset set caption='lug00 b' where identifier = 'lug00' and type = 'MANAGED_WEBSPACE'; -- prod
-- update hs_hosting_asset set caption='hsh00 A ' || now()::text where identifier = 'hsh00' and type = 'MANAGED_WEBSPACE'; -- test -- update hs_hosting.asset set caption='hsh00 A ' || now()::text where identifier = 'hsh00' and type = 'MANAGED_WEBSPACE'; -- test
-- update hs_hosting_asset set caption='hsh00 B ' || now()::text where identifier = 'hsh00' and type = 'MANAGED_WEBSPACE'; -- test -- update hs_hosting.asset set caption='hsh00 B ' || now()::text where identifier = 'hsh00' and type = 'MANAGED_WEBSPACE'; -- test
-- insert into hs_hosting_asset -- insert into hs_hosting.asset
-- (uuid, bookingitemuuid, type, parentassetuuid, assignedtoassetuuid, identifier, caption, config, alarmcontactuuid) -- (uuid, bookingitemuuid, type, parentassetuuid, assignedtoassetuuid, identifier, caption, config, alarmcontactuuid)
-- values -- values
-- (uuid_generate_v4(), null, 'EMAIL_ADDRESS', 'bbda5895-0569-4e20-bb4c-34f3a38f3f63'::uuid, null, -- (uuid_generate_v4(), null, 'EMAIL_ADDRESS', 'bbda5895-0569-4e20-bb4c-34f3a38f3f63'::uuid, null,
-- 'new@thi.example.org', 'some new E-Mail-Address', '{}'::jsonb, null); -- 'new@thi.example.org', 'some new E-Mail-Address', '{}'::jsonb, null);
delete from hs_hosting_asset where uuid='5aea68d2-3b55-464f-8362-b05c76c5a681'::uuid; delete from hs_hosting.asset where uuid='5aea68d2-3b55-464f-8362-b05c76c5a681'::uuid;
commit; commit;
-- single version at point in time -- single version at point in time
-- set hsadminng.tx_history_txid to (select max(txid) from tx_context where txtimestamp<='2024-08-27 12:13:13.450821'); -- set hsadminng.tx_history_txid to (select max(txid) from base.tx_context where txtimestamp<='2024-08-27 12:13:13.450821');
set hsadminng.tx_history_txid to ''; set hsadminng.tx_history_txid to '';
set hsadminng.tx_history_timestamp to '2024-08-29 12:42'; set hsadminng.tx_history_timestamp to '2024-08-29 12:42';
-- all versions -- all versions
select tx_history_txid(), txc.txtimestamp, txc.currentUser, txc.currentTask, haex.* select base.tx_history_txid(), txc.txtimestamp, txc.currentSubject, txc.currentTask, haex.*
from hs_hosting_asset_ex haex from hs_hosting.asset_ex haex
join tx_context txc on haex.txid=txc.txid join base.tx_context txc on haex.txid=txc.txid
where haex.identifier = 'test@thi.example.org'; where haex.identifier = 'test@thi.example.org';
select uuid, version, type, identifier, caption from hs_hosting_asset_hv p where identifier = 'test@thi.example.org'; select uuid, version, type, identifier, caption from hs_hosting.asset_hv p where identifier = 'test@thi.example.org';
select pg_current_xact_id(); select pg_current_xact_id();

View File

@ -3,28 +3,28 @@
-- -------------------------------------------------------- -- --------------------------------------------------------
select isGranted(findRoleId('administrators'), findRoleId('test_package#aaa00:OWNER')); select rbac.isGranted(rbac.findRoleId('administrators'), rbac.findRoleId('rbactest.package#aaa00:OWNER'));
select isGranted(findRoleId('test_package#aaa00:OWNER'), findRoleId('administrators')); select rbac.isGranted(rbac.findRoleId('rbactest.package#aaa00:OWNER'), rbac.findRoleId('administrators'));
-- call grantRoleToRole(findRoleId('test_package#aaa00:OWNER'), findRoleId('administrators')); -- call rbac.grantRoleToRole(findRoleId('rbactest.package#aaa00:OWNER'), findRoleId('administrators'));
-- call grantRoleToRole(findRoleId('administrators'), findRoleId('test_package#aaa00:OWNER')); -- call rbac.grantRoleToRole(findRoleId('administrators'), findRoleId('rbactest.package#aaa00:OWNER'));
select count(*) select count(*)
FROM queryAllPermissionsOfSubjectIdForObjectUuids(findRbacUser('superuser-fran@hostsharing.net'), FROM rbac.queryAllPermissionsOfSubjectIdForObjectUuids(rbac.findRbacSubject('superuser-fran@hostsharing.net'),
ARRAY(select uuid from customer where reference < 1100000)); ARRAY(select uuid from rbactest.customer where reference < 1100000));
select count(*) select count(*)
FROM queryAllPermissionsOfSubjectId(findRbacUser('superuser-fran@hostsharing.net')); FROM rbac.queryAllPermissionsOfSubjectId(findRbacSubject('superuser-fran@hostsharing.net'));
select * select *
FROM queryAllPermissionsOfSubjectId(findRbacUser('alex@example.com')); FROM rbac.queryAllPermissionsOfSubjectId(findRbacSubject('alex@example.com'));
select * select *
FROM queryAllPermissionsOfSubjectId(findRbacUser('rosa@example.com')); FROM rbac.queryAllPermissionsOfSubjectId(findRbacSubject('rosa@example.com'));
select * select *
FROM queryAllRbacUsersWithPermissionsFor(findEffectivePermissionId('customer', FROM rbac.queryAllRbacSubjectsWithPermissionsFor(rbac.findEffectivePermissionId('customer',
(SELECT uuid FROM RbacObject WHERE objectTable = 'customer' LIMIT 1), (SELECT uuid FROM rbac.RbacObject WHERE objectTable = 'customer' LIMIT 1),
'add-package')); 'add-package'));
select * select *
FROM queryAllRbacUsersWithPermissionsFor(findEffectivePermissionId('package', FROM rbac.queryAllRbacSubjectsWithPermissionsFor(rbac.findEffectivePermissionId('package',
(SELECT uuid FROM RbacObject WHERE objectTable = 'package' LIMIT 1), (SELECT uuid FROM rbac.RbacObject WHERE objectTable = 'package' LIMIT 1),
'DELETE')); 'DELETE'));
DO LANGUAGE plpgsql DO LANGUAGE plpgsql
@ -33,13 +33,13 @@ $$
userId uuid; userId uuid;
result bool; result bool;
BEGIN BEGIN
userId = findRbacUser('superuser-alex@hostsharing.net'); userId = rbac.findRbacSubject('superuser-alex@hostsharing.net');
result = (SELECT * FROM isPermissionGrantedToSubject(findPermissionId('package', 94928, 'add-package'), userId)); result = (SELECT * FROM rbac.isPermissionGrantedToSubject(rbac.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(findPermissionId('package', 94928, 'SELECT'), userId)); result = (SELECT * FROM rbac.isPermissionGrantedToSubject(rbac.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;

View File

@ -20,43 +20,43 @@ CREATE POLICY customer_policy ON customer
TO restricted TO restricted
USING ( USING (
-- id=1000 -- id=1000
isPermissionGrantedToSubject(findEffectivePermissionId('test_customer', id, 'SELECT'), currentUserUuid()) rbac.isPermissionGrantedToSubject(rbac.findEffectivePermissionId('rbactest.customer', id, 'SELECT'), rbac.currentSubjectUuid())
); );
SET SESSION AUTHORIZATION restricted; SET SESSION AUTHORIZATION restricted;
SET hsadminng.currentUser TO 'alex@example.com'; SET hsadminng.currentSubject TO 'alex@example.com';
SELECT * from customer; SELECT * from customer;
-- access control via view-rule and isPermissionGrantedToSubject - way too slow (35 s 580 ms for 1 million rows) -- access control via view-rule and isPermissionGrantedToSubject - way too slow (35 s 580 ms for 1 million rows)
SET SESSION SESSION AUTHORIZATION DEFAULT; SET SESSION SESSION AUTHORIZATION DEFAULT;
DROP VIEW cust_view; DROP VIEW cust_view;
CREATE VIEW cust_view AS CREATE VIEW cust_view AS
SELECT * FROM customer; SELECT * FROM rbactest.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, 'SELECT'), currentUserUuid()); SELECT * FROM rbactest.customer WHERE rbac.isPermissionGrantedToSubject(rbac.findEffectivePermissionId('rbactest.customer', id, 'SELECT'), rbac.currentSubjectUuid());
SELECT * from cust_view LIMIT 10; SELECT * from cust_view LIMIT 10;
select queryAllPermissionsOfSubjectId(findRbacUser('superuser-alex@hostsharing.net')); select rbac.queryAllPermissionsOfSubjectId(findRbacSubject('superuser-alex@hostsharing.net'));
-- access control via view-rule with join to recursive permissions - really fast (38ms for 1 million rows) -- access control via view-rule with join to recursive permissions - really fast (38ms for 1 million rows)
SET SESSION SESSION AUTHORIZATION DEFAULT; SET SESSION SESSION AUTHORIZATION DEFAULT;
ALTER TABLE customer ENABLE ROW LEVEL SECURITY; ALTER TABLE rbactest.customer ENABLE ROW LEVEL SECURITY;
DROP VIEW IF EXISTS cust_view; DROP VIEW IF EXISTS cust_view;
CREATE OR REPLACE VIEW cust_view AS CREATE OR REPLACE VIEW cust_view AS
SELECT * SELECT *
FROM customer; FROM rbactest.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 c.uuid, c.reference, c.prefix FROM customer AS c SELECT c.uuid, c.reference, c.prefix FROM rbactest.customer AS c
JOIN queryAllPermissionsOfSubjectId(currentUserUuid()) AS p JOIN rbac.queryAllPermissionsOfSubjectId(rbac.currentSubjectUuid()) AS p
ON p.objectTable='test_customer' AND p.objectUuid=c.uuid; ON p.objectTable='rbactest.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;
SET hsadminng.currentUser TO 'alex@example.com'; SET hsadminng.currentSubject TO 'alex@example.com';
SELECT * from cust_view; SELECT * from cust_view;
@ -67,23 +67,23 @@ DROP VIEW IF EXISTS cust_view;
CREATE OR REPLACE VIEW cust_view AS 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(rbac.currentSubjectUuid()) AS p
ON p.objectUuid=c.uuid; 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;
-- SET hsadminng.currentUser TO 'alex@example.com'; -- SET hsadminng.currentSubject TO 'alex@example.com';
SET hsadminng.currentUser TO 'superuser-alex@hostsharing.net'; SET hsadminng.currentSubject TO 'superuser-alex@hostsharing.net';
-- SET hsadminng.currentUser TO 'aaaaouq@example.com'; -- SET hsadminng.currentSubject TO 'aaaaouq@example.com';
SELECT * from cust_view where reference=1144150; SELECT * from cust_view where reference=1144150;
select rr.uuid, rr.type from RbacGrants g select rr.uuid, rr.type from rbac.RbacGrants g
join RbacReference RR on g.ascendantUuid = RR.uuid join rbac.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 rbac.queryAllPermissionsOfSubjectId(findRbacSubject('alex@example.com'))
where objectTable='test_customer'); where objectTable='rbactest.customer');
call grantRoleToUser(findRoleId('test_customer#aaa:ADMIN'), findRbacUser('aaaaouq@example.com')); call rbac.grantRoleToUser(rbac.findRoleId('rbactest.customer#aaa:ADMIN'), rbac.findRbacSubject('aaaaouq@example.com'));
select queryAllPermissionsOfSubjectId(findRbacUser('aaaaouq@example.com')); select rbac.queryAllPermissionsOfSubjectId(findRbacSubject('aaaaouq@example.com'));

View File

@ -1,15 +1,15 @@
-- just a permanent playground to explore optimization of the central recursive CTE query for RBAC -- just a permanent playground to explore optimization of the central recursive CTE query for RBAC
select * from hs_statistics_view; select * from hs_statistics_v;
-- ======================================================== -- ========================================================
-- This is the extracted recursive CTE query to determine the visible object UUIDs of a single table -- This is the extracted recursive CTE query to determine the visible object UUIDs of a single table
-- (and optionally the hosting-asset-type) as a separate VIEW. -- (and optionally the hosting-asset-type) as a separate VIEW.
-- In the generated code this is part of the hs_hosting_asset_rv VIEW. -- In the generated code this is part of the hs_hosting.asset_rv VIEW.
drop view if exists hs_hosting_asset_example_gv; drop view if exists hs_hosting.asset_example_gv;
create view hs_hosting_asset_example_gv as create view hs_hosting.asset_example_gv as
with recursive with recursive
recursive_grants as ( recursive_grants as (
select distinct rbacgrants.descendantuuid, select distinct rbacgrants.descendantuuid,
@ -17,7 +17,7 @@ with recursive
1 as level, 1 as level,
true true
from rbacgrants from rbacgrants
where (rbacgrants.ascendantuuid = any (currentsubjectsuuids())) where (rbacgrants.ascendantuuid = any (rbac.currentSubjectOrAssumedRolesUuids()))
and rbacgrants.assumed and rbacgrants.assumed
union all union all
select distinct g.descendantuuid, select distinct g.descendantuuid,
@ -40,7 +40,7 @@ select distinct perm.objectuuid
join rbacpermission perm on recursive_grants.descendantuuid = perm.uuid join rbacpermission perm on recursive_grants.descendantuuid = perm.uuid
join rbacobject obj on obj.uuid = perm.objectuuid join rbacobject obj on obj.uuid = perm.objectuuid
join count_check cc on cc.valid join count_check cc on cc.valid
where obj.objecttable::text = 'hs_hosting_asset'::text where obj.objecttable::text = 'hs_hosting.asset'::text
-- with/without this type condition -- with/without this type condition
-- and obj.type = 'EMAIL_ADDRESS'::hshostingassettype -- and obj.type = 'EMAIL_ADDRESS'::hshostingassettype
and obj.type = 'EMAIL_ADDRESS'::hshostingassettype and obj.type = 'EMAIL_ADDRESS'::hshostingassettype
@ -53,10 +53,10 @@ select distinct perm.objectuuid
rollback transaction; rollback transaction;
begin transaction; begin transaction;
CALL defineContext('performance testing', null, 'superuser-alex@hostsharing.net', CALL defineContext('performance testing', null, 'superuser-alex@hostsharing.net',
'hs_booking_project#D-1000000-hshdefaultproject:ADMIN'); 'hs_booking.project#D-1000000-hshdefaultproject:ADMIN');
-- 'hs_booking_project#D-1000300-mihdefaultproject:ADMIN'); -- 'hs_booking.project#D-1000300-mihdefaultproject:ADMIN');
SET TRANSACTION READ ONLY; SET TRANSACTION READ ONLY;
EXPLAIN ANALYZE select * from hs_hosting_asset_example_gv; EXPLAIN ANALYZE select * from hs_hosting.asset_example_gv;
end transaction ; end transaction ;
-- ======================================================== -- ========================================================
@ -64,15 +64,15 @@ end transaction ;
-- An example for a restricted view (_rv) similar to the one generated by our RBAC system, -- An example for a restricted view (_rv) similar to the one generated by our RBAC system,
-- but using the above separate VIEW to determine the visible objects. -- but using the above separate VIEW to determine the visible objects.
drop view if exists hs_hosting_asset_example_rv; drop view if exists hs_hosting.asset_example_rv;
create view hs_hosting_asset_example_rv as create view hs_hosting.asset_example_rv as
with accessible_hs_hosting_asset_uuids as ( with accessible_hs_hosting.asset_uuids as (
select * from hs_hosting_asset_example_gv select * from hs_hosting.asset_example_gv
) )
select target.* select target.*
from hs_hosting_asset target from hs_hosting.asset target
where (target.uuid in (select accessible_hs_hosting_asset_uuids.objectuuid where (target.uuid in (select accessible_hs_hosting.asset_uuids.objectuuid
from accessible_hs_hosting_asset_uuids)); from accessible_hs_hosting.asset_uuids));
-- ------------------------------------------------------------------------------- -- -------------------------------------------------------------------------------
@ -89,8 +89,8 @@ BEGIN
start_time := clock_timestamp(); start_time := clock_timestamp();
CALL defineContext('performance testing', null, 'superuser-alex@hostsharing.net', CALL defineContext('performance testing', null, 'superuser-alex@hostsharing.net',
'hs_booking_project#D-1000000-hshdefaultproject:ADMIN'); 'hs_booking.project#D-1000000-hshdefaultproject:ADMIN');
-- 'hs_booking_project#D-1000300-mihdefaultproject:ADMIN'); -- 'hs_booking.project#D-1000300-mihdefaultproject:ADMIN');
SET TRANSACTION READ ONLY; SET TRANSACTION READ ONLY;
FOR i IN 0..25 LOOP FOR i IN 0..25 LOOP
@ -99,7 +99,7 @@ BEGIN
-- An example for a business query based on the view: -- An example for a business query based on the view:
select type, uuid, identifier, caption select type, uuid, identifier, caption
from hs_hosting_asset_example_rv from hs_hosting.asset_example_rv
where type = 'EMAIL_ADDRESS' where type = 'EMAIL_ADDRESS'
and identifier like letter || '%' and identifier like letter || '%'
-- end of the business query example. -- end of the business query example.
@ -115,7 +115,7 @@ BEGIN
END; END;
$$; $$;
-- average seconds per recursive CTE select as role 'hs_hosting_asset:<DEBITOR>defaultproject:ADMIN' -- average seconds per recursive CTE select as role 'hs_hosting.asset:<DEBITOR>defaultproject:ADMIN'
-- joined with business query for all 'EMAIL_ADDRESSES': -- joined with business query for all 'EMAIL_ADDRESSES':
-- D-1000000-hsh D-1000300-mih -- D-1000000-hsh D-1000300-mih
-- - without type comparison in rbacobject: ~3.30 - ~3.49 ~0.23 -- - without type comparison in rbacobject: ~3.30 - ~3.49 ~0.23
@ -128,15 +128,15 @@ $$;
rollback transaction; rollback transaction;
begin transaction; begin transaction;
CALL defineContext('performance testing', null, 'superuser-alex@hostsharing.net', CALL defineContext('performance testing', null, 'superuser-alex@hostsharing.net',
'hs_booking_project#D-1000000-hshdefaultproject:ADMIN'); 'hs_booking.project#D-1000000-hshdefaultproject:ADMIN');
-- 'hs_booking_project#D-1000300-mihdefaultproject:ADMIN'); -- 'hs_booking.project#D-1000300-mihdefaultproject:ADMIN');
SET TRANSACTION READ ONLY; SET TRANSACTION READ ONLY;
EXPLAIN SELECT * from ( EXPLAIN SELECT * from (
-- An example for a business query based on the view: -- An example for a business query based on the view:
select type, uuid, identifier, caption select type, uuid, identifier, caption
from hs_hosting_asset_example_rv from hs_hosting.asset_example_rv
where type = 'EMAIL_ADDRESS' where type = 'EMAIL_ADDRESS'
-- and identifier like 'b%' -- and identifier like 'b%'
-- end of the business query example. -- end of the business query example.
@ -151,17 +151,17 @@ end transaction;
alter table rbacobject alter table rbacobject
-- just for performance testing, we would need a joined enum or a varchar(16) which would make it slow -- just for performance testing, we would need a joined enum or a varchar(16) which would make it slow
add column type hshostingassettype; add column type hs_hosting.AssetType;
-- and fill the type column with hs_hosting_asset types: -- and fill the type column with hs_hosting.asset types:
rollback transaction; rollback transaction;
begin transaction; begin transaction;
call defineContext('setting rbacobject.type from hs_hosting_asset.type', null, 'superuser-alex@hostsharing.net'); call defineContext('setting rbacobject.type from hs_hosting.asset.type', null, 'superuser-alex@hostsharing.net');
UPDATE rbacobject UPDATE rbacobject
SET type = hs.type SET type = hs.type
FROM hs_hosting_asset hs FROM hs_hosting.asset hs
WHERE rbacobject.uuid = hs.uuid; WHERE rbacobject.uuid = hs.uuid;
end transaction; end transaction;

View File

@ -17,7 +17,7 @@ public class JsonObjectMapperConfiguration {
public Jackson2ObjectMapperBuilder customObjectMapper() { public Jackson2ObjectMapperBuilder customObjectMapper() {
return new Jackson2ObjectMapperBuilder() return new Jackson2ObjectMapperBuilder()
.modules(new JsonNullableModule(), new JavaTimeModule()) .modules(new JsonNullableModule(), new JavaTimeModule())
.featuresToEnable(JsonParser.Feature.ALLOW_COMMENTS) .featuresToEnable(JsonParser.Feature.ALLOW_COMMENTS, JsonParser.Feature.ALLOW_COMMENTS)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
} }
} }

View File

@ -38,53 +38,53 @@ public class Context {
private HttpServletRequest request; private HttpServletRequest request;
@Transactional(propagation = MANDATORY) @Transactional(propagation = MANDATORY)
public void define(final String currentUser) { public void define(final String currentSubject) {
define(currentUser, null); define(currentSubject, null);
} }
@Transactional(propagation = MANDATORY) @Transactional(propagation = MANDATORY)
public void define(final String currentUser, final String assumedRoles) { public void define(final String currentSubject, final String assumedRoles) {
define(toTask(request), toCurl(request), currentUser, assumedRoles); define(toTask(request), toCurl(request), currentSubject, assumedRoles);
} }
@Transactional(propagation = MANDATORY) @Transactional(propagation = MANDATORY)
public void define( public void define(
final String currentTask, final String currentTask,
final String currentRequest, final String currentRequest,
final String currentUser, final String currentSubject,
final String assumedRoles) { final String assumedRoles) {
final var query = em.createNativeQuery(""" final var query = em.createNativeQuery("""
call defineContext( call base.defineContext(
cast(:currentTask as varchar(127)), cast(:currentTask as varchar(127)),
cast(:currentRequest as text), cast(:currentRequest as text),
cast(:currentUser as varchar(63)), cast(:currentSubject as varchar(63)),
cast(:assumedRoles as varchar(1023))); cast(:assumedRoles as varchar(1023)));
"""); """);
query.setParameter("currentTask", shortenToMaxLength(currentTask, 127)); query.setParameter("currentTask", shortenToMaxLength(currentTask, 127));
query.setParameter("currentRequest", currentRequest); query.setParameter("currentRequest", currentRequest);
query.setParameter("currentUser", currentUser); query.setParameter("currentSubject", currentSubject);
query.setParameter("assumedRoles", assumedRoles != null ? assumedRoles : ""); query.setParameter("assumedRoles", assumedRoles != null ? assumedRoles : "");
query.executeUpdate(); query.executeUpdate();
} }
public String getCurrentTask() { public String fetchCurrentTask() {
return (String) em.createNativeQuery("select current_setting('hsadminng.currentTask');").getSingleResult(); return (String) em.createNativeQuery("select current_setting('hsadminng.currentTask');").getSingleResult();
} }
public String getCurrentUser() { public String fetchCurrentSubject() {
return String.valueOf(em.createNativeQuery("select currentUser()").getSingleResult()); return String.valueOf(em.createNativeQuery("select base.currentSubject()").getSingleResult());
} }
public UUID getCurrentUserUUid() { public UUID fetchCurrentSubjectUuid() {
return (UUID) em.createNativeQuery("select currentUserUUid()", UUID.class).getSingleResult(); return (UUID) em.createNativeQuery("select rbac.currentSubjectUuid()", UUID.class).getSingleResult();
} }
public String[] getAssumedRoles() { public String[] fetchAssumedRoles() {
return (String[]) em.createNativeQuery("select assumedRoles() as roles", String[].class).getSingleResult(); return (String[]) em.createNativeQuery("select base.assumedRoles() as roles", String[].class).getSingleResult();
} }
public UUID[] currentSubjectsUuids() { public UUID[] fetchCurrentSubjectOrAssumedRolesUuids() {
return (UUID[]) em.createNativeQuery("select currentSubjectsUuids() as uuids", UUID[].class).getSingleResult(); return (UUID[]) em.createNativeQuery("select rbac.currentSubjectOrAssumedRolesUuids() as uuids", UUID[].class).getSingleResult();
} }
public static String getCallerMethodNameFromStackFrame(final int skipFrames) { public static String getCallerMethodNameFromStackFrame(final int skipFrames) {

View File

@ -9,15 +9,25 @@ import java.lang.annotation.Target;
@Target(ElementType.TYPE) @Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface DisplayAs { public @interface DisplayAs {
class DisplayName { class DisplayName {
public static String of(final Class<?> clazz) { public static String of(final Class<?> clazz) {
final var displayNameAnnot = clazz.getAnnotation(DisplayAs.class); final var displayNameAnnot = getDisplayNameAnnotation(clazz);
return displayNameAnnot != null ? displayNameAnnot.value() : clazz.getSimpleName(); return displayNameAnnot != null ? displayNameAnnot.value() : clazz.getSimpleName();
} }
public static String of(@NotNull final Object instance) { public static String of(@NotNull final Object instance) {
return of(instance.getClass()); return of(instance.getClass());
} }
private static DisplayAs getDisplayNameAnnotation(final Class<?> clazz) {
if (clazz == null) {
return null;
}
final var annot = clazz.getAnnotation(DisplayAs.class);
return annot != null ? annot : getDisplayNameAnnotation(clazz.getSuperclass());
}
} }
String value() default ""; String value() default "";

View File

@ -27,7 +27,7 @@ public final class HashGenerator {
"abcdefghijklmnopqrstuvwxyz" + "abcdefghijklmnopqrstuvwxyz" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
"0123456789/."; "0123456789/.";
private static boolean couldBeHashEnabled; // TODO.impl: remove after legacy data is migrated private static boolean couldBeHashEnabled; // TODO.legacy: remove after legacy data is migrated
public enum Algorithm { public enum Algorithm {
LINUX_SHA512(LinuxEtcShadowHashGenerator::hash, "6"), LINUX_SHA512(LinuxEtcShadowHashGenerator::hash, "6"),

View File

@ -18,7 +18,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
// a partial HsOfficeDebitorEntity to reduce the number of SQL queries to load the entity // a partial HsOfficeDebitorEntity to reduce the number of SQL queries to load the entity
@Entity @Entity
@Table(name = "hs_booking_debitor_xv") @Table(schema = "hs_booking", name = "debitor_xv")
@Getter @Getter
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor

View File

@ -0,0 +1,20 @@
package net.hostsharing.hsadminng.hs.booking.item;
import lombok.Getter;
import org.springframework.context.ApplicationEvent;
import jakarta.validation.constraints.NotNull;
@Getter
public class BookingItemCreatedAppEvent extends ApplicationEvent {
private BookingItemCreatedEventEntity entity;
public BookingItemCreatedAppEvent(
@NotNull final Object source,
@NotNull final HsBookingItemRealEntity newBookingItem,
final String assetJson) {
super(source);
this.entity = new BookingItemCreatedEventEntity(newBookingItem, assetJson);
}
}

View File

@ -0,0 +1,55 @@
package net.hostsharing.hsadminng.hs.booking.item;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.persistence.BaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.MapsId;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import jakarta.validation.constraints.NotNull;
import java.util.UUID;
@Entity
@Table(schema = "hs_booking", name = "item_created_event")
@SuperBuilder(toBuilder = true)
@Getter
@ToString
@NoArgsConstructor
public class BookingItemCreatedEventEntity implements BaseEntity {
@Id
@Column(name="bookingitemuuid")
private UUID uuid;
@MapsId
@ManyToOne(optional = false)
@JoinColumn(name = "bookingitemuuid", nullable = false)
private HsBookingItemRealEntity bookingItem;
@Version
private int version;
@Column(name = "assetjson")
private String assetJson;
@Setter
@Column(name = "statusmessage")
private String statusMessage;
public BookingItemCreatedEventEntity(
@NotNull final HsBookingItemRealEntity newBookingItem,
final String assetJson) {
this.bookingItem = newBookingItem;
this.assetJson = assetJson;
}
}

View File

@ -0,0 +1,12 @@
package net.hostsharing.hsadminng.hs.booking.item;
import org.springframework.data.repository.Repository;
import java.util.UUID;
public interface BookingItemCreatedEventRepository extends Repository<BookingItemCreatedEventEntity, UUID> {
BookingItemCreatedEventEntity save(HsBookingItemRealEntity current);
BookingItemCreatedEventEntity findByBookingItem(HsBookingItemRealEntity newBookingItem);
}

View File

@ -14,7 +14,7 @@ import net.hostsharing.hsadminng.hs.booking.project.HsBookingProject;
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity; import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity;
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider; import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper; import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
import net.hostsharing.hsadminng.rbac.rbacobject.BaseEntity; import net.hostsharing.hsadminng.persistence.BaseEntity;
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;

View File

@ -1,26 +1,31 @@
package net.hostsharing.hsadminng.hs.booking.item; package net.hostsharing.hsadminng.hs.booking.item;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingItemsApi; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingItemsApi;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemInsertResource; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemInsertResource;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemResource; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemResource;
import net.hostsharing.hsadminng.hs.booking.item.validators.BookingItemEntitySaveProcessor;
import net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidatorRegistry; import net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidatorRegistry;
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity;
import net.hostsharing.hsadminng.mapper.KeyValueMap; import net.hostsharing.hsadminng.mapper.KeyValueMap;
import net.hostsharing.hsadminng.mapper.Mapper; import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional; 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.time.LocalDate; import java.time.LocalDate;
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;
import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
@RestController @RestController
@ -30,74 +35,87 @@ public class HsBookingItemController implements HsBookingItemsApi {
private Context context; private Context context;
@Autowired @Autowired
private Mapper mapper; private StrictMapper mapper;
@Autowired
private ApplicationEventPublisher applicationEventPublisher;
@Autowired @Autowired
private HsBookingItemRbacRepository bookingItemRepo; private HsBookingItemRbacRepository bookingItemRepo;
@PersistenceContext @Autowired
private EntityManager em; private ObjectMapper jsonMapper;
@Autowired
private EntityManagerWrapper em;
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<List<HsBookingItemResource>> listBookingItemsByProjectUuid( public ResponseEntity<List<HsBookingItemResource>> listBookingItemsByProjectUuid(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID projectUuid) { final UUID projectUuid) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var entities = bookingItemRepo.findAllByProjectUuid(projectUuid); final var entities = bookingItemRepo.findAllByProjectUuid(projectUuid);
final var resources = mapper.mapList(entities, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); final var resources = mapper.mapList(entities, HsBookingItemResource.class, RBAC_ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(resources); return ResponseEntity.ok(resources);
} }
@Override @Override
@Transactional @Transactional
public ResponseEntity<HsBookingItemResource> addBookingItem( public ResponseEntity<HsBookingItemResource> addBookingItem(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final HsBookingItemInsertResource body) { final HsBookingItemInsertResource body) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var entityToSave = mapper.map(body, HsBookingItemRbacEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); final var entityToSave = mapper.map(body, HsBookingItemRbacEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
final var saveProcessor = new BookingItemEntitySaveProcessor(em, entityToSave);
final var saved = HsBookingItemEntityValidatorRegistry.validated(em, bookingItemRepo.save(entityToSave)); final var mapped = saveProcessor
.preprocessEntity()
.validateEntity()
.prepareForSave()
.save()
.validateContext()
.mapUsing(e -> mapper.map(e, HsBookingItemResource.class, ITEM_TO_RESOURCE_POSTMAPPER))
.revampProperties();
publishSavedEvent(saveProcessor, body);
final var uri = final var uri =
MvcUriComponentsBuilder.fromController(getClass()) MvcUriComponentsBuilder.fromController(getClass())
.path("/api/hs/booking/items/{id}") .path("/api/hs/booking/items/{id}")
.buildAndExpand(saved.getUuid()) .buildAndExpand(mapped.getUuid())
.toUri(); .toUri();
final var mapped = mapper.map(saved, HsBookingItemResource.class, 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<HsBookingItemResource> getBookingItemByUuid( public ResponseEntity<HsBookingItemResource> getBookingItemByUuid(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID bookingItemUuid) { final UUID bookingItemUuid) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var result = bookingItemRepo.findByUuid(bookingItemUuid); final var result = bookingItemRepo.findByUuid(bookingItemUuid);
result.ifPresent(entity -> em.detach(entity)); // prevent further LAZY-loading result.ifPresent(entity -> em.detach(entity)); // prevent further LAZY-loading
return result return result
.map(bookingItemEntity -> ResponseEntity.ok( .map(bookingItemEntity -> ResponseEntity.ok(
mapper.map(bookingItemEntity, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER))) mapper.map(bookingItemEntity, HsBookingItemResource.class, RBAC_ENTITY_TO_RESOURCE_POSTMAPPER)))
.orElseGet(() -> ResponseEntity.notFound().build()); .orElseGet(() -> ResponseEntity.notFound().build());
} }
@Override @Override
@Transactional @Transactional
public ResponseEntity<Void> deleteBookingIemByUuid( public ResponseEntity<Void> deleteBookingIemByUuid(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID bookingItemUuid) { final UUID bookingItemUuid) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var result = bookingItemRepo.deleteByUuid(bookingItemUuid); final var result = bookingItemRepo.deleteByUuid(bookingItemUuid);
return result == 0 return result == 0
@ -108,30 +126,46 @@ public class HsBookingItemController implements HsBookingItemsApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<HsBookingItemResource> patchBookingItem( public ResponseEntity<HsBookingItemResource> patchBookingItem(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID bookingItemUuid, final UUID bookingItemUuid,
final HsBookingItemPatchResource body) { final HsBookingItemPatchResource body) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var current = bookingItemRepo.findByUuid(bookingItemUuid).orElseThrow(); final var current = bookingItemRepo.findByUuid(bookingItemUuid).orElseThrow();
new HsBookingItemEntityPatcher(current).apply(body); new HsBookingItemEntityPatcher(current).apply(body);
final var saved = bookingItemRepo.save(HsBookingItemEntityValidatorRegistry.validated(em, current)); final var saved = bookingItemRepo.save(HsBookingItemEntityValidatorRegistry.validated(em, current));
final var mapped = mapper.map(saved, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); final var mapped = mapper.map(saved, HsBookingItemResource.class, RBAC_ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(mapped); return ResponseEntity.ok(mapped);
} }
final BiConsumer<HsBookingItemRbacEntity, HsBookingItemResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { private void publishSavedEvent(final BookingItemEntitySaveProcessor saveProcessor, final HsBookingItemInsertResource body) {
try {
final var bookingItemRealEntity = em.getReference(HsBookingItemRealEntity.class, saveProcessor.getEntity().getUuid());
applicationEventPublisher.publishEvent(new BookingItemCreatedAppEvent(
this, bookingItemRealEntity, jsonMapper.writeValueAsString(body.getHostingAsset())));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
final BiConsumer<HsBookingItem, HsBookingItemResource> ITEM_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
resource.setValidFrom(entity.getValidity().lower()); resource.setValidFrom(entity.getValidity().lower());
if (entity.getValidity().hasUpperBound()) { if (entity.getValidity().hasUpperBound()) {
resource.setValidTo(entity.getValidity().upper().minusDays(1)); resource.setValidTo(entity.getValidity().upper().minusDays(1));
} }
}; };
final BiConsumer<HsBookingItemRbacEntity, HsBookingItemResource> RBAC_ENTITY_TO_RESOURCE_POSTMAPPER = ITEM_TO_RESOURCE_POSTMAPPER::accept;
final BiConsumer<HsBookingItemInsertResource, HsBookingItemRbacEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { final BiConsumer<HsBookingItemInsertResource, HsBookingItemRbacEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
entity.setProject(em.find(HsBookingProjectRealEntity.class, resource.getProjectUuid()));
ofNullable(resource.getParentItemUuid())
.map(parentItemUuid -> em.find(HsBookingItemRealEntity.class, parentItemUuid))
.ifPresent(entity::setParentItem);
entity.setValidity(toPostgresDateRange(LocalDate.now(), resource.getValidTo())); entity.setValidity(toPostgresDateRange(LocalDate.now(), resource.getValidTo()));
entity.putResources(KeyValueMap.from(resource.getResources())); entity.putResources(KeyValueMap.from(resource.getResources()));
}; };

View File

@ -4,9 +4,9 @@ import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProject; import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRbacEntity;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
import jakarta.persistence.AttributeOverride; import jakarta.persistence.AttributeOverride;
import jakarta.persistence.AttributeOverrides; import jakarta.persistence.AttributeOverrides;
@ -15,22 +15,23 @@ import jakarta.persistence.Entity;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.io.IOException; import java.io.IOException;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NULLABLE; import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.DELETE; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NULLABLE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.DELETE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.SELECT; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.UPDATE; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.ADMIN; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.UPDATE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.AGENT; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.OWNER; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.AGENT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.TENANT; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.OWNER;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.directlyFetchedByDependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.TENANT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
@Entity @Entity
@Table(name = "hs_booking_item_rv") @Table(schema = "hs_booking", name = "item_rv")
@SuperBuilder(toBuilder = true) @SuperBuilder(toBuilder = true)
@Getter @Getter
@Setter @Setter
@ -45,10 +46,10 @@ public class HsBookingItemRbacEntity extends HsBookingItem {
.withIdentityView(SQL.projection("caption")) .withIdentityView(SQL.projection("caption"))
.withRestrictedViewOrderBy(SQL.expression("validity")) .withRestrictedViewOrderBy(SQL.expression("validity"))
.withUpdatableColumns("version", "caption", "validity", "resources") .withUpdatableColumns("version", "caption", "validity", "resources")
.toRole("global", ADMIN).grantPermission(INSERT) // TODO.impl: Why is this necessary to insert test data? .toRole(GLOBAL, ADMIN).grantPermission(INSERT) // TODO.impl: Why is this necessary to insert test data?
.toRole("global", ADMIN).grantPermission(DELETE) .toRole(GLOBAL, ADMIN).grantPermission(DELETE)
.importEntityAlias("project", HsBookingProject.class, usingDefaultCase(), .importEntityAlias("project", HsBookingProjectRbacEntity.class, usingDefaultCase(),
dependsOnColumn("projectUuid"), dependsOnColumn("projectUuid"),
directlyFetchedByDependsOnColumn(), directlyFetchedByDependsOnColumn(),
NULLABLE) NULLABLE)
@ -74,7 +75,7 @@ public class HsBookingItemRbacEntity extends HsBookingItem {
with.permission(SELECT); with.permission(SELECT);
}) })
.limitDiagramTo("bookingItem", "project", "global"); .limitDiagramTo("bookingItem", "project", "rbac.global");
} }
public static void main(String[] args) throws IOException { public static void main(String[] args) throws IOException {

View File

@ -13,7 +13,7 @@ import jakarta.persistence.Table;
@Entity @Entity
@Table(name = "hs_booking_item") @Table(schema = "hs_booking", name = "item")
@SuperBuilder(toBuilder = true) @SuperBuilder(toBuilder = true)
@Getter @Getter
@Setter @Setter

View File

@ -0,0 +1,136 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import lombok.Getter;
import net.hostsharing.hsadminng.errors.MultiValidationException;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemResource;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import jakarta.persistence.EntityManager;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.regex.Pattern;
// TODO.refa: introduce common base class with HsHostingAssetEntitySaveProcessor
/**
* Wraps the steps of the pararation, validation, mapping and revamp around saving of a HsBookingItem into a readable API.
*/
public class BookingItemEntitySaveProcessor {
private final HsEntityValidator<HsBookingItem> validator;
private String expectedStep = "preprocessEntity";
private final EntityManager em;
@Getter
private HsBookingItem entity;
@Getter
private HsBookingItemResource resource;
public BookingItemEntitySaveProcessor(final EntityManager em, final HsBookingItem entity) {
this.em = em;
this.entity = entity;
this.validator = HsBookingItemEntityValidatorRegistry.forType(entity.getType());
}
/// initial step allowing to set default values before any validations
public BookingItemEntitySaveProcessor preprocessEntity() {
step("preprocessEntity", "validateEntity");
validator.preprocessEntity(entity);
return this;
}
/// validates the entity itself including its properties
public BookingItemEntitySaveProcessor validateEntity() {
step("validateEntity", "prepareForSave");
MultiValidationException.throwIfNotEmpty(validator.validateEntity(entity));
return this;
}
// TODO.legacy: remove once the migration of legacy data is done
/// validates the entity itself including its properties, but ignoring some error messages for import of legacy data
public BookingItemEntitySaveProcessor validateEntityIgnoring(final String... ignoreRegExp) {
step("validateEntity", "prepareForSave");
final var ignoreRegExpPatterns = Arrays.stream(ignoreRegExp).map(Pattern::compile).toList();
MultiValidationException.throwIfNotEmpty(
validator.validateEntity(entity).stream()
.filter(error -> ignoreRegExpPatterns.stream().noneMatch(p -> p.matcher(error).matches() ))
.toList()
);
return this;
}
/// hashing passwords etc.
public BookingItemEntitySaveProcessor prepareForSave() {
step("prepareForSave", "save");
validator.prepareProperties(em, entity);
return this;
}
/**
* Saves the entity using the given `saveFunction`.
*
* <p>`validator.postPersist(em, entity)` is NOT called.
* If any postprocessing is necessary, the saveFunction has to implement this.</p>
* @param saveFunction
* @return this
*/
public BookingItemEntitySaveProcessor saveUsing(final Function<HsBookingItem, HsBookingItem> saveFunction) {
step("save", "validateContext");
entity = saveFunction.apply(entity);
return this;
}
/**
* Saves the using the `EntityManager`, but does NOT ever merge the entity.
*
* <p>`validator.postPersist(em, entity)` is called afterwards with the entity guaranteed to be flushed to the database.</p>
* @return this
*/
public BookingItemEntitySaveProcessor save() {
return saveUsing(e -> {
if (!em.contains(entity)) {
em.persist(entity);
}
em.flush(); // makes RbacEntity available as RealEntity if needed
validator.postPersist(em, entity);
return entity;
});
}
/// validates the entity within it's parent and child hierarchy (e.g. totals validators and other limits)
public BookingItemEntitySaveProcessor validateContext() {
step("validateContext", "mapUsing");
return HsEntityValidator.doWithEntityManager(em, () -> {
MultiValidationException.throwIfNotEmpty(validator.validateContext(entity));
return this;
});
}
/// maps entity to JSON resource representation
public BookingItemEntitySaveProcessor mapUsing(
final Function<HsBookingItem, HsBookingItemResource> mapFunction) {
step("mapUsing", "revampProperties");
resource = mapFunction.apply(entity);
return this;
}
/// removes write-only-properties and ads computed-properties
@SuppressWarnings("unchecked")
public HsBookingItemResource revampProperties() {
step("revampProperties", null);
final var revampedProps = validator.revampProperties(em, entity, (Map<String, Object>) resource.getResources());
resource.setResources(revampedProps);
return resource;
}
// Makes sure that the steps are called in the correct order.
// Could also be implemented using an interface per method, but that seems exaggerated.
private void step(final String current, final String next) {
if (!expectedStep.equals(current)) {
throw new IllegalStateException("expected " + expectedStep + " but got " + current);
}
expectedStep = next;
}
}

View File

@ -48,10 +48,11 @@ public class HsBookingItemEntityValidatorRegistry {
} }
public static List<String> doValidate(final EntityManager em, final HsBookingItem bookingItem) { public static List<String> doValidate(final EntityManager em, final HsBookingItem bookingItem) {
final var bookingItemValidator = HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType());
return HsEntityValidator.doWithEntityManager(em, () -> return HsEntityValidator.doWithEntityManager(em, () ->
HsEntityValidator.sequentiallyValidate( HsEntityValidator.sequentiallyValidate(
() -> HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateEntity(bookingItem), () -> bookingItemValidator.validateEntity(bookingItem),
() -> HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateContext(bookingItem)) () -> bookingItemValidator.validateContext(bookingItem))
); );
} }

View File

@ -1,3 +1,4 @@
package net.hostsharing.hsadminng.hs.booking.item.validators; package net.hostsharing.hsadminng.hs.booking.item.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem;
@ -12,13 +13,16 @@ import static net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns.REGISTRA
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty; import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator { class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator {
public static final String DOMAIN_NAME_PROPERTY_NAME = "domainName";
public static final String FQDN_REGEX = "^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,12}"; public static final String FQDN_REGEX = "^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,12}";
public static final String DOMAIN_NAME_PROPERTY_NAME = "domainName"; public static final String WEBSPACE_NAME_REGEX = "[a-z][a-z0-9]{2}[0-9]{2}";
public static final String TARGET_UNIX_USER_NAME_REGEX = "^"+WEBSPACE_NAME_REGEX+"$|^"+WEBSPACE_NAME_REGEX+"-[a-z0-9\\._-]+$";
public static final String VERIFICATION_CODE_PROPERTY_NAME = "verificationCode"; public static final String VERIFICATION_CODE_PROPERTY_NAME = "verificationCode";
HsDomainSetupBookingItemValidator() { HsDomainSetupBookingItemValidator() {
super( super(
// TODO.spec: feels wrong
stringProperty(DOMAIN_NAME_PROPERTY_NAME).writeOnce() stringProperty(DOMAIN_NAME_PROPERTY_NAME).writeOnce()
.maxLength(253) .maxLength(253)
.matchesRegEx(FQDN_REGEX).describedAs("is not a (non-top-level) fully qualified domain name") .matchesRegEx(FQDN_REGEX).describedAs("is not a (non-top-level) fully qualified domain name")
@ -45,6 +49,11 @@ class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator {
} }
private static String generateVerificationCode(final EntityManager em, final PropertiesProvider propertiesProvider) { private static String generateVerificationCode(final EntityManager em, final PropertiesProvider propertiesProvider) {
final var userDefinedVerificationCode = propertiesProvider.getDirectValue(VERIFICATION_CODE_PROPERTY_NAME, String.class);
if (userDefinedVerificationCode != null) {
return userDefinedVerificationCode;
}
final var alphaNumeric = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; final var alphaNumeric = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
final var secureRandom = new SecureRandom(); final var secureRandom = new SecureRandom();
final var sb = new StringBuilder(); final var sb = new StringBuilder();

View File

@ -3,29 +3,14 @@ package net.hostsharing.hsadminng.hs.booking.project;
import lombok.*; import lombok.*;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity; import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
import net.hostsharing.hsadminng.rbac.rbacobject.BaseEntity;
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.util.UUID; import java.util.UUID;
import static java.util.Optional.ofNullable; import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase;
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.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;
@MappedSuperclass @MappedSuperclass
@ -65,50 +50,4 @@ public abstract class HsBookingProject implements Stringifyable, BaseEntity<HsBo
return ofNullable(debitor).map(HsBookingDebitorEntity::toShortString).orElse("D-???????") + return ofNullable(debitor).map(HsBookingDebitorEntity::toShortString).orElse("D-???????") +
":" + caption; ":" + caption;
} }
public static RbacView rbac() {
return rbacViewFor("project", HsBookingProject.class)
.withIdentityView(SQL.query("""
SELECT bookingProject.uuid as uuid, debitorIV.idName || '-' || cleanIdentifier(bookingProject.caption) as idName
FROM hs_booking_project bookingProject
JOIN hs_office_debitor_iv debitorIV ON debitorIV.uuid = bookingProject.debitorUuid
"""))
.withRestrictedViewOrderBy(SQL.expression("caption"))
.withUpdatableColumns("version", "caption")
.importEntityAlias("debitor", HsOfficeDebitorEntity.class, usingDefaultCase(),
dependsOnColumn("debitorUuid"),
directlyFetchedByDependsOnColumn(),
NOT_NULL)
.importEntityAlias("debitorRel", HsOfficeRelationRbacEntity.class, usingCase(DEBITOR),
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)
.toRole("debitorRel", ADMIN).grantPermission(INSERT)
.toRole("global", ADMIN).grantPermission(DELETE)
.createRole(OWNER, (with) -> {
with.incomingSuperRole("debitorRel", AGENT).unassumed();
})
.createSubRole(ADMIN, (with) -> {
with.permission(UPDATE);
})
.createSubRole(AGENT)
.createSubRole(TENANT, (with) -> {
with.outgoingSubRole("debitorRel", TENANT);
with.permission(SELECT);
})
.limitDiagramTo("project", "debitorRel", "global");
}
public static void main(String[] args) throws IOException {
rbac().generateWithBaseFileName("6-hs-booking/620-booking-project/6203-hs-booking-project-rbac");
}
} }

View File

@ -6,7 +6,7 @@ import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingProjec
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectInsertResource; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectInsertResource;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectPatchResource; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectPatchResource;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectResource; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectResource;
import net.hostsharing.hsadminng.mapper.Mapper; import net.hostsharing.hsadminng.mapper.StandardMapper;
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;
@ -25,7 +25,7 @@ public class HsBookingProjectController implements HsBookingProjectsApi {
private Context context; private Context context;
@Autowired @Autowired
private Mapper mapper; private StandardMapper mapper;
@Autowired @Autowired
private HsBookingProjectRbacRepository bookingProjectRepo; private HsBookingProjectRbacRepository bookingProjectRepo;
@ -36,10 +36,10 @@ public class HsBookingProjectController implements HsBookingProjectsApi {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<List<HsBookingProjectResource>> listBookingProjectsByDebitorUuid( public ResponseEntity<List<HsBookingProjectResource>> listBookingProjectsByDebitorUuid(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID debitorUuid) { final UUID debitorUuid) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var entities = bookingProjectRepo.findAllByDebitorUuid(debitorUuid); final var entities = bookingProjectRepo.findAllByDebitorUuid(debitorUuid);
@ -50,11 +50,11 @@ public class HsBookingProjectController implements HsBookingProjectsApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<HsBookingProjectResource> addBookingProject( public ResponseEntity<HsBookingProjectResource> addBookingProject(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final HsBookingProjectInsertResource body) { final HsBookingProjectInsertResource body) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var entityToSave = mapper.map(body, HsBookingProjectRbacEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); final var entityToSave = mapper.map(body, HsBookingProjectRbacEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
@ -72,11 +72,11 @@ public class HsBookingProjectController implements HsBookingProjectsApi {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<HsBookingProjectResource> getBookingProjectByUuid( public ResponseEntity<HsBookingProjectResource> getBookingProjectByUuid(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID bookingProjectUuid) { final UUID bookingProjectUuid) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var result = bookingProjectRepo.findByUuid(bookingProjectUuid); final var result = bookingProjectRepo.findByUuid(bookingProjectUuid);
return result return result
@ -88,10 +88,10 @@ public class HsBookingProjectController implements HsBookingProjectsApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<Void> deleteBookingIemByUuid( public ResponseEntity<Void> deleteBookingIemByUuid(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID bookingProjectUuid) { final UUID bookingProjectUuid) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var result = bookingProjectRepo.deleteByUuid(bookingProjectUuid); final var result = bookingProjectRepo.deleteByUuid(bookingProjectUuid);
return result == 0 return result == 0
@ -102,12 +102,12 @@ public class HsBookingProjectController implements HsBookingProjectsApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<HsBookingProjectResource> patchBookingProject( public ResponseEntity<HsBookingProjectResource> patchBookingProject(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID bookingProjectUuid, final UUID bookingProjectUuid,
final HsBookingProjectPatchResource body) { final HsBookingProjectPatchResource body) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var current = bookingProjectRepo.findByUuid(bookingProjectUuid).orElseThrow(); final var current = bookingProjectRepo.findByUuid(bookingProjectUuid).orElseThrow();

View File

@ -6,32 +6,33 @@ import lombok.Setter;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.io.IOException; import java.io.IOException;
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR; import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingCase; import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL; import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.DELETE; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.DELETE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.SELECT; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.UPDATE; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.ADMIN; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.UPDATE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.AGENT; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.OWNER; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.AGENT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.TENANT; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.OWNER;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.directlyFetchedByDependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.TENANT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.fetchedBySql; import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.fetchedBySql;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
@Entity @Entity
@Table(name = "hs_booking_project_rv") @Table(schema = "hs_booking", name = "project_rv")
@SuperBuilder(toBuilder = true) @SuperBuilder(toBuilder = true)
@Getter @Getter
@Setter @Setter
@ -41,9 +42,9 @@ public class HsBookingProjectRbacEntity extends HsBookingProject {
public static RbacView rbac() { public static RbacView rbac() {
return rbacViewFor("project", HsBookingProjectRbacEntity.class) return rbacViewFor("project", HsBookingProjectRbacEntity.class)
.withIdentityView(SQL.query(""" .withIdentityView(SQL.query("""
SELECT bookingProject.uuid as uuid, debitorIV.idName || '-' || cleanIdentifier(bookingProject.caption) as idName SELECT bookingProject.uuid as uuid, debitorIV.idName || '-' || base.cleanIdentifier(bookingProject.caption) as idName
FROM hs_booking_project bookingProject FROM hs_booking.project bookingProject
JOIN hs_office_debitor_iv debitorIV ON debitorIV.uuid = bookingProject.debitorUuid JOIN hs_office.debitor_iv debitorIV ON debitorIV.uuid = bookingProject.debitorUuid
""")) """))
.withRestrictedViewOrderBy(SQL.expression("caption")) .withRestrictedViewOrderBy(SQL.expression("caption"))
.withUpdatableColumns("version", "caption") .withUpdatableColumns("version", "caption")
@ -57,13 +58,13 @@ public class HsBookingProjectRbacEntity extends HsBookingProject {
dependsOnColumn("debitorUuid"), dependsOnColumn("debitorUuid"),
fetchedBySql(""" fetchedBySql("""
SELECT ${columns} SELECT ${columns}
FROM hs_office_relation debitorRel FROM hs_office.relation debitorRel
JOIN hs_office_debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid JOIN hs_office.debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid
WHERE debitor.uuid = ${REF}.debitorUuid WHERE debitor.uuid = ${REF}.debitorUuid
"""), """),
NOT_NULL) NOT_NULL)
.toRole("debitorRel", ADMIN).grantPermission(INSERT) .toRole("debitorRel", ADMIN).grantPermission(INSERT)
.toRole("global", ADMIN).grantPermission(DELETE) .toRole(GLOBAL, ADMIN).grantPermission(DELETE)
.createRole(OWNER, (with) -> { .createRole(OWNER, (with) -> {
with.incomingSuperRole("debitorRel", AGENT).unassumed(); with.incomingSuperRole("debitorRel", AGENT).unassumed();
@ -77,7 +78,7 @@ public class HsBookingProjectRbacEntity extends HsBookingProject {
with.permission(SELECT); with.permission(SELECT);
}) })
.limitDiagramTo("project", "debitorRel", "global"); .limitDiagramTo("project", "debitorRel", "rbac.global");
} }
public static void main(String[] args) throws IOException { public static void main(String[] args) throws IOException {

View File

@ -10,7 +10,7 @@ import jakarta.persistence.Table;
@Entity @Entity
@Table(name = "hs_booking_project") @Table(schema = "hs_booking", name = "project")
@SuperBuilder(toBuilder = true) @SuperBuilder(toBuilder = true)
@Getter @Getter
@Setter @Setter

View File

@ -14,7 +14,7 @@ import net.hostsharing.hsadminng.hs.booking.project.HsBookingProject;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider; import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper; import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
import net.hostsharing.hsadminng.rbac.rbacobject.BaseEntity; import net.hostsharing.hsadminng.persistence.BaseEntity;
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;
@ -89,10 +89,9 @@ public abstract class HsHostingAsset implements Stringifyable, BaseEntity<HsHost
@JoinColumn(name = "alarmcontactuuid") @JoinColumn(name = "alarmcontactuuid")
private HsOfficeContactRealEntity alarmContact; private HsOfficeContactRealEntity alarmContact;
@Builder.Default
@OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true, fetch = FetchType.LAZY) @OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true, fetch = FetchType.LAZY)
@JoinColumn(name = "parentassetuuid", referencedColumnName = "uuid") @JoinColumn(name = "parentassetuuid", referencedColumnName = "uuid")
private List<HsHostingAssetRealEntity> subHostingAssets = new ArrayList<>(); private List<HsHostingAssetRealEntity> subHostingAssets;
@Column(name = "identifier") @Column(name = "identifier")
private String identifier; // e.g. vm1234, xyz00, example.org, xyz00_abc private String identifier; // e.g. vm1234, xyz00, example.org, xyz00_abc
@ -125,6 +124,13 @@ public abstract class HsHostingAsset implements Stringifyable, BaseEntity<HsHost
PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper;}, config).assign(newConfig); PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper;}, config).assign(newConfig);
} }
public List<HsHostingAssetRealEntity> getSubHostingAssets() {
if (subHostingAssets == null) {
subHostingAssets = new ArrayList<>();
}
return subHostingAssets;
}
@Override @Override
public PatchableMapWrapper<Object> directProps() { public PatchableMapWrapper<Object> directProps() {
return getConfig(); return getConfig();

View File

@ -11,7 +11,7 @@ import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAsse
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetResource; import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetResource;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource; import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource;
import net.hostsharing.hsadminng.mapper.KeyValueMap; import net.hostsharing.hsadminng.mapper.KeyValueMap;
import net.hostsharing.hsadminng.mapper.Mapper; import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@ -35,7 +35,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
private Context context; private Context context;
@Autowired @Autowired
private Mapper mapper; private StandardMapper mapper;
@Autowired @Autowired
private HsHostingAssetRbacRepository rbacAssetRepo; private HsHostingAssetRbacRepository rbacAssetRepo;
@ -49,12 +49,12 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<List<HsHostingAssetResource>> listAssets( public ResponseEntity<List<HsHostingAssetResource>> listAssets(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID debitorUuid, final UUID debitorUuid,
final UUID parentAssetUuid, final UUID parentAssetUuid,
final HsHostingAssetTypeResource type) { final HsHostingAssetTypeResource type) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var entities = rbacAssetRepo.findAllByCriteria(debitorUuid, parentAssetUuid, HsHostingAssetType.of(type)); final var entities = rbacAssetRepo.findAllByCriteria(debitorUuid, parentAssetUuid, HsHostingAssetType.of(type));
@ -66,11 +66,11 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<HsHostingAssetResource> addAsset( public ResponseEntity<HsHostingAssetResource> addAsset(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final HsHostingAssetInsertResource body) { final HsHostingAssetInsertResource body) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var entity = mapper.map(body, HsHostingAssetRbacEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); final var entity = mapper.map(body, HsHostingAssetRbacEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
@ -94,11 +94,11 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<HsHostingAssetResource> getAssetByUuid( public ResponseEntity<HsHostingAssetResource> getAssetByUuid(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID assetUuid) { final UUID assetUuid) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var result = rbacAssetRepo.findByUuid(assetUuid); final var result = rbacAssetRepo.findByUuid(assetUuid);
return result return result
@ -110,10 +110,10 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<Void> deleteAssetUuid( public ResponseEntity<Void> deleteAssetUuid(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID assetUuid) { final UUID assetUuid) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var result = rbacAssetRepo.deleteByUuid(assetUuid); final var result = rbacAssetRepo.deleteByUuid(assetUuid);
return result == 0 return result == 0
@ -124,12 +124,12 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<HsHostingAssetResource> patchAsset( public ResponseEntity<HsHostingAssetResource> patchAsset(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID assetUuid, final UUID assetUuid,
final HsHostingAssetPatchResource body) { final HsHostingAssetPatchResource body) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var entity = rbacAssetRepo.findByUuid(assetUuid).orElseThrow(); final var entity = rbacAssetRepo.findByUuid(assetUuid).orElseThrow();

View File

@ -4,36 +4,36 @@ import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRbacEntity;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRbacEntity; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRbacEntity;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.io.IOException; import java.io.IOException;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.CaseDef.inCaseOf; import static net.hostsharing.hsadminng.rbac.generator.RbacView.CaseDef.inCaseOf;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL; import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NULLABLE; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NULLABLE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.DELETE; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.DELETE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.SELECT; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.UPDATE; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.UPDATE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR; import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.ADMIN; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.AGENT; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.AGENT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.GUEST; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.GUEST;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.OWNER; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.OWNER;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.REFERRER; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.REFERRER;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.TENANT; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.TENANT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.directlyFetchedByDependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
@Entity @Entity
@Table(name = "hs_hosting_asset_rv") @Table(schema = "hs_hosting", name = "asset_rv")
@SuperBuilder(toBuilder = true) @SuperBuilder(toBuilder = true)
@Getter @Getter
@Setter @Setter
@ -47,7 +47,7 @@ public class HsHostingAssetRbacEntity extends HsHostingAsset {
.withUpdatableColumns("version", "caption", "config", "assignedToAssetUuid", "alarmContactUuid") .withUpdatableColumns("version", "caption", "config", "assignedToAssetUuid", "alarmContactUuid")
.toRole(GLOBAL, ADMIN).grantPermission(INSERT) // TODO.impl: Why is this necessary to insert test data? .toRole(GLOBAL, ADMIN).grantPermission(INSERT) // TODO.impl: Why is this necessary to insert test data?
.importEntityAlias("bookingItem", HsBookingItem.class, usingDefaultCase(), .importEntityAlias("bookingItem", HsBookingItemRbacEntity.class, usingDefaultCase(),
dependsOnColumn("bookingItemUuid"), dependsOnColumn("bookingItemUuid"),
directlyFetchedByDependsOnColumn(), directlyFetchedByDependsOnColumn(),
NULLABLE) NULLABLE)
@ -106,7 +106,7 @@ public class HsHostingAssetRbacEntity extends HsHostingAsset {
"parentAsset", "parentAsset",
"assignedToAsset", "assignedToAsset",
"alarmContact", "alarmContact",
"global"); "rbac.global");
} }
public static void main(String[] args) throws IOException { public static void main(String[] args) throws IOException {

View File

@ -25,15 +25,15 @@ public interface HsHostingAssetRbacRepository extends HsHostingAssetRepository<H
ha.parentassetuuid, ha.parentassetuuid,
ha.type, ha.type,
ha.version ha.version
from hs_hosting_asset_rv ha from hs_hosting.asset_rv ha
left join hs_booking_item bi on bi.uuid = ha.bookingitemuuid left join hs_booking.item bi on bi.uuid = ha.bookingitemuuid
left join hs_hosting_asset pha on pha.uuid = ha.parentassetuuid left join hs_hosting.asset pha on pha.uuid = ha.parentassetuuid
where (:projectUuid is null or bi.projectuuid=:projectUuid) where (:projectUuid is null or bi.projectuuid=:projectUuid)
and (:parentAssetUuid is null or pha.uuid=:parentAssetUuid) and (:parentAssetUuid is null or pha.uuid=:parentAssetUuid)
and (:type is null or :type=cast(ha.type as text)) and (:type is null or :type=cast(ha.type as text))
""", nativeQuery = true) """, nativeQuery = true)
// The JPQL query did not generate "left join" but just "join". // The JPQL query did not generate "left join" but just "join".
// I also optimized the query by not using the _rv for hs_booking_item and hs_hosting_asset, only for hs_hosting_asset_rv. // I also optimized the query by not using the _rv for hs_booking.item and hs_hosting.asset, only for hs_hosting.asset_rv.
List<HsHostingAssetRbacEntity> findAllByCriteriaImpl(UUID projectUuid, UUID parentAssetUuid, String type); List<HsHostingAssetRbacEntity> findAllByCriteriaImpl(UUID projectUuid, UUID parentAssetUuid, String type);
default List<HsHostingAssetRbacEntity> findAllByCriteria(final UUID projectUuid, final UUID parentAssetUuid, final HsHostingAssetType type) { default List<HsHostingAssetRbacEntity> findAllByCriteria(final UUID projectUuid, final UUID parentAssetUuid, final HsHostingAssetType type) {
return findAllByCriteriaImpl(projectUuid, parentAssetUuid, HsHostingAssetType.asString(type)); return findAllByCriteriaImpl(projectUuid, parentAssetUuid, HsHostingAssetType.asString(type));

View File

@ -9,7 +9,7 @@ import jakarta.persistence.Entity;
import jakarta.persistence.Table; import jakarta.persistence.Table;
@Entity @Entity
@Table(name = "hs_hosting_asset") @Table(schema = "hs_hosting", name = "asset")
@SuperBuilder(builderMethodName = "genericBuilder", toBuilder = true) @SuperBuilder(builderMethodName = "genericBuilder", toBuilder = true)
@Getter @Getter
@Setter @Setter

View File

@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.hosting.asset;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository; import org.springframework.data.repository.Repository;
import jakarta.validation.constraints.NotNull;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@ -13,6 +14,18 @@ public interface HsHostingAssetRealRepository extends HsHostingAssetRepository<H
List<HsHostingAssetRealEntity> findByIdentifier(String assetIdentifier); List<HsHostingAssetRealEntity> findByIdentifier(String assetIdentifier);
default List<HsHostingAssetRealEntity> findByTypeAndIdentifier(@NotNull HsHostingAssetType type, @NotNull String identifier) {
return findByTypeAndIdentifierImpl(type.name(), identifier);
}
@Query("""
select ha
from HsHostingAssetRealEntity ha
where cast(ha.type as String) = :type
and ha.identifier = :identifier
""")
List<HsHostingAssetRealEntity> findByTypeAndIdentifierImpl(@NotNull String type, @NotNull String identifier);
@Query(value = """ @Query(value = """
select ha.uuid, select ha.uuid,
ha.alarmcontactuuid, ha.alarmcontactuuid,
@ -24,15 +37,15 @@ public interface HsHostingAssetRealRepository extends HsHostingAssetRepository<H
ha.parentassetuuid, ha.parentassetuuid,
ha.type, ha.type,
ha.version ha.version
from hs_hosting_asset_rv ha from hs_hosting.asset_rv ha
left join hs_booking_item bi on bi.uuid = ha.bookingitemuuid left join hs_booking.item bi on bi.uuid = ha.bookingitemuuid
left join hs_hosting_asset pha on pha.uuid = ha.parentassetuuid left join hs_hosting.asset pha on pha.uuid = ha.parentassetuuid
where (:projectUuid is null or bi.projectuuid=:projectUuid) where (:projectUuid is null or bi.projectuuid=:projectUuid)
and (:parentAssetUuid is null or pha.uuid=:parentAssetUuid) and (:parentAssetUuid is null or pha.uuid=:parentAssetUuid)
and (:type is null or :type=cast(ha.type as text)) and (:type is null or :type=cast(ha.type as text))
""", nativeQuery = true) """, nativeQuery = true)
// The JPQL query did not generate "left join" but just "join". // The JPQL query did not generate "left join" but just "join".
// I also optimized the query by not using the _rv for hs_booking_item and hs_hosting_asset, only for hs_hosting_asset_rv. // I also optimized the query by not using the _rv for hs_booking.item and hs_hosting.asset, only for hs_hosting.asset_rv.
List<HsHostingAssetRealEntity> findAllByCriteriaImpl(UUID projectUuid, UUID parentAssetUuid, String type); List<HsHostingAssetRealEntity> findAllByCriteriaImpl(UUID projectUuid, UUID parentAssetUuid, String type);
default List<HsHostingAssetRealEntity> findAllByCriteria(final UUID projectUuid, final UUID parentAssetUuid, final HsHostingAssetType type) { default List<HsHostingAssetRealEntity> findAllByCriteria(final UUID projectUuid, final UUID parentAssetUuid, final HsHostingAssetType type) {
return findAllByCriteriaImpl(projectUuid, parentAssetUuid, HsHostingAssetType.asString(type)); return findAllByCriteriaImpl(projectUuid, parentAssetUuid, HsHostingAssetType.asString(type));

View File

@ -0,0 +1,159 @@
package net.hostsharing.hsadminng.hs.hosting.asset.factories;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetSubInsertResource;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetTypeResource;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
import net.hostsharing.hsadminng.lambda.Reducer;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.mapper.ToStringConverter;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import jakarta.validation.ValidationException;
import java.net.IDN;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Function;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_DNS_SETUP;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_HTTP_SETUP;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_MBOX_SETUP;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SMTP_SETUP;
public class DomainSetupHostingAssetFactory extends HostingAssetFactory {
public DomainSetupHostingAssetFactory(
final EntityManagerWrapper emw,
final HsBookingItemRealEntity newBookingItemRealEntity,
final HsHostingAssetAutoInsertResource asset,
final StandardMapper standardMapper) {
super(emw, newBookingItemRealEntity, asset, standardMapper);
}
@Override
protected HsHostingAsset create() {
final var domainSetupAsset = createDomainSetupAsset(getDomainName());
final var subHostingAssets = domainSetupAsset.getSubHostingAssets();
// TODO.legacy: as long as we need to be compatible, we always do all technical domain-setups
final var domainHttpSetupAssetResource = findSubHostingAssetResource(HsHostingAssetTypeResource.DOMAIN_HTTP_SETUP);
final var assignedToUnixUserAssetEntity = domainHttpSetupAssetResource
.map(HsHostingAssetSubInsertResource::getAssignedToAssetUuid)
.map(uuid -> emw.find(HsHostingAssetRealEntity.class, uuid))
.orElseThrow(() -> new ValidationException("DOMAIN_HTTP_SETUP subAsset with assignedToAssetUuid required in compatibility mode"));
subHostingAssets.add(
createDomainSubSetupAssetEntity(
domainSetupAsset,
DOMAIN_HTTP_SETUP,
builder -> builder
.assignedToAsset(assignedToUnixUserAssetEntity)
.identifier(getDomainName() + "|HTTP")
.caption("HTTP-Setup für " + IDN.toUnicode(getDomainName())))
);
// Do not add to subHostingAssets in compatibility mode, in this case, DNS setup works via file system.
// The entity is created just for validation purposes.
createDomainSubSetupAssetEntity(
domainSetupAsset,
DOMAIN_DNS_SETUP,
builder -> builder
.assignedToAsset(assignedToUnixUserAssetEntity.getParentAsset())
.identifier(getDomainName() + "|DNS")
.caption("DNS-Setup für " + IDN.toUnicode(getDomainName())));
subHostingAssets.add(
createDomainSubSetupAssetEntity(
domainSetupAsset,
DOMAIN_MBOX_SETUP,
builder -> builder
.assignedToAsset(assignedToUnixUserAssetEntity.getParentAsset())
.identifier(getDomainName() + "|MBOX")
.caption("MBOX-Setup für " + IDN.toUnicode(getDomainName())))
);
subHostingAssets.add(
createDomainSubSetupAssetEntity(
domainSetupAsset,
DOMAIN_SMTP_SETUP,
builder -> builder
.assignedToAsset(assignedToUnixUserAssetEntity.getParentAsset())
.identifier(getDomainName() + "|SMTP")
.caption("SMTP-Setup für " + IDN.toUnicode(getDomainName())))
);
return domainSetupAsset;
}
private HsHostingAssetRealEntity createDomainSetupAsset(final String domainName) {
return HsHostingAssetRealEntity.builder()
.bookingItem(fromBookingItem)
.type(HsHostingAssetType.DOMAIN_SETUP)
.identifier(domainName)
.caption(asset.getCaption() != null ? asset.getCaption() : domainName)
.alarmContact(ref(HsOfficeContactRealEntity.class, asset.getAlarmContactUuid()))
// the sub-hosting-assets get added later
.build();
}
private HsHostingAssetRealEntity createDomainSubSetupAssetEntity(
final HsHostingAssetRealEntity domainSetupAsset,
final HsHostingAssetType subAssetType,
final Function<HsHostingAssetRealEntity.HsHostingAssetRealEntityBuilder<?, ?>, HsHostingAssetRealEntity.HsHostingAssetRealEntityBuilder<?, ?>> builderTransformer) {
final var resourceType = HsHostingAssetTypeResource.valueOf(subAssetType.name());
final var subAssetResourceOptional = findSubHostingAssetResource(resourceType);
subAssetResourceOptional.ifPresentOrElse(
this::verifyNotOverspecified,
() -> { throw new ValidationException("sub-asset of type " + resourceType.name() + " required in legacy mode, but missing"); }
);
return builderTransformer.apply(
HsHostingAssetRealEntity.builder()
.type(subAssetType)
.parentAsset(domainSetupAsset))
.build();
}
private Optional<HsHostingAssetSubInsertResource> findSubHostingAssetResource(final HsHostingAssetTypeResource resourceType) {
return getSubHostingAssetResources().stream()
.filter(ha -> ha.getType() == resourceType)
.reduce(Reducer::toSingleElement);
}
// TODO.legacy: while we need to stay compatible, only default values can be used, thus only the type can be specified
private void verifyNotOverspecified(final HsHostingAssetSubInsertResource givenSubAssetResource) {
final var convert = new ToStringConverter().ignoring("assignedToAssetUuid");
final var expectedSubAssetResource = new HsHostingAssetSubInsertResource();
expectedSubAssetResource.setType(givenSubAssetResource.getType());
if ( !convert.from(givenSubAssetResource).equals(convert.from(expectedSubAssetResource)) ) {
throw new ValidationException("sub asset " + givenSubAssetResource.getType() + " is over-specified, in compatibility mode, only default values allowed");
}
}
private String getDomainName() {
return asset.getIdentifier();
}
private List<HsHostingAssetSubInsertResource> getSubHostingAssetResources() {
return asset.getSubHostingAssets();
}
@Override
protected void persist(final HsHostingAsset newHostingAsset) {
super.persist(newHostingAsset);
newHostingAsset.getSubHostingAssets().forEach(super::persist);
}
private <T> T ref(final Class<T> entityClass, final UUID uuid) {
return uuid != null ? emw.getReference(entityClass, uuid) : null;
}
}

View File

@ -0,0 +1,41 @@
package net.hostsharing.hsadminng.hs.hosting.asset.factories;
import jakarta.validation.ValidationException;
import lombok.RequiredArgsConstructor;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntitySaveProcessor;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
@RequiredArgsConstructor
abstract class HostingAssetFactory {
final EntityManagerWrapper emw;
final HsBookingItemRealEntity fromBookingItem;
final HsHostingAssetAutoInsertResource asset;
final StandardMapper standardMapper;
protected abstract HsHostingAsset create();
public String createAndPersist() {
try {
final HsHostingAsset newHostingAsset = create();
persist(newHostingAsset);
return null;
} catch (final ValidationException exc) {
return exc.getMessage();
}
}
protected void persist(final HsHostingAsset newHostingAsset) {
new HostingAssetEntitySaveProcessor(emw, newHostingAsset)
.preprocessEntity()
.validateEntity()
.prepareForSave()
.save()
.validateContext();
}
}

View File

@ -0,0 +1,76 @@
package net.hostsharing.hsadminng.hs.hosting.asset.factories;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.validation.ValidationException;
import jakarta.validation.constraints.NotNull;
import lombok.SneakyThrows;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource;
import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedAppEvent;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
@Component
public class HsBookingItemCreatedListener implements ApplicationListener<BookingItemCreatedAppEvent> {
@Autowired
private EntityManagerWrapper emw;
@Autowired
private ObjectMapper jsonMapper;
@Autowired
private StandardMapper standardMapper;
@Override
@SneakyThrows
public void onApplicationEvent(@NotNull BookingItemCreatedAppEvent bookingItemCreatedAppEvent) {
if (containsAssetJson(bookingItemCreatedAppEvent)) {
createRelatedHostingAsset(bookingItemCreatedAppEvent);
}
}
private static boolean containsAssetJson(final BookingItemCreatedAppEvent bookingItemCreatedAppEvent) {
return bookingItemCreatedAppEvent.getEntity().getAssetJson() != null;
}
private void createRelatedHostingAsset(final BookingItemCreatedAppEvent event) throws JsonProcessingException {
final var newBookingItemRealEntity = event.getEntity().getBookingItem();
final var asset = jsonMapper.readValue(event.getEntity().getAssetJson(), HsHostingAssetAutoInsertResource.class);
final var factory = switch (newBookingItemRealEntity.getType()) {
case PRIVATE_CLOUD, CLOUD_SERVER, MANAGED_SERVER ->
forNowNoAutomaticHostingAssetCreationPossible(emw, newBookingItemRealEntity, asset, standardMapper);
case MANAGED_WEBSPACE -> new ManagedWebspaceHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper);
case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper);
};
if (factory != null) {
final var statusMessage = factory.createAndPersist();
// TODO.impl: once we implement retry, we need to amend this code (persist/merge/delete)
if (statusMessage != null) {
event.getEntity().setStatusMessage(statusMessage);
emw.persist(event.getEntity());
}
}
}
private HostingAssetFactory forNowNoAutomaticHostingAssetCreationPossible(
final EntityManagerWrapper emw,
final HsBookingItemRealEntity fromBookingItem,
final HsHostingAssetAutoInsertResource asset,
final StandardMapper standardMapper
) {
return new HostingAssetFactory(emw, fromBookingItem, asset, standardMapper) {
@Override
protected HsHostingAsset create() {
// TODO.impl: we should validate the asset JSON, but some violations are un-avoidable at that stage
throw new ValidationException("waiting for manual setup of hosting asset for booking item of type " + fromBookingItem.getType());
}
};
}
}

View File

@ -0,0 +1,51 @@
package net.hostsharing.hsadminng.hs.hosting.asset.factories;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetTypeResource;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import jakarta.validation.ValidationException;
import java.util.Optional;
public class ManagedWebspaceHostingAssetFactory extends HostingAssetFactory {
public ManagedWebspaceHostingAssetFactory(
final EntityManagerWrapper emw,
final HsBookingItemRealEntity newBookingItemRealEntity,
final HsHostingAssetAutoInsertResource asset,
final StandardMapper standardMapper) {
super(emw, newBookingItemRealEntity, asset, standardMapper);
}
@Override
protected HsHostingAsset create() {
if (asset.getType() != HsHostingAssetTypeResource.MANAGED_WEBSPACE) {
throw new ValidationException("requires MANAGED_WEBSPACE hosting asset, but got " +
Optional.of(asset)
.map(HsHostingAssetAutoInsertResource::getType)
.map(Enum::name)
.orElse(null));
}
final var managedWebspaceHostingAsset = standardMapper.map(asset, HsHostingAssetRealEntity.class);
managedWebspaceHostingAsset.setBookingItem(fromBookingItem);
emw.createQuery(
"SELECT asset FROM HsHostingAssetRealEntity asset WHERE asset.bookingItem.uuid=:bookingItemUuid",
HsHostingAssetRealEntity.class)
.setParameter("bookingItemUuid", fromBookingItem.getParentItem().getUuid())
.getResultStream().findFirst()
.ifPresent(managedWebspaceHostingAsset::setParentAsset);
return managedWebspaceHostingAsset;
}
@Override
protected void persist(final HsHostingAsset newManagedWebspaceHostingAsset) {
super.persist(newManagedWebspaceHostingAsset);
}
}

View File

@ -42,7 +42,7 @@ public class HostingAssetEntitySaveProcessor {
return this; return this;
} }
// TODO.impl: remove once the migration of legacy data is done // TODO.legacy: remove once the migration of legacy data is done
/// validates the entity itself including its properties, but ignoring some error messages for import of legacy data /// validates the entity itself including its properties, but ignoring some error messages for import of legacy data
public HostingAssetEntitySaveProcessor validateEntityIgnoring(final String... ignoreRegExp) { public HostingAssetEntitySaveProcessor validateEntityIgnoring(final String... ignoreRegExp) {
step("validateEntity", "prepareForSave"); step("validateEntity", "prepareForSave");

View File

@ -15,7 +15,7 @@ import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanPro
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty; import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty; import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
// TODO.impl: make package private once we've migrated the legacy data // TODO.legacy: make package private once we've migrated the legacy data
public class HsDomainDnsSetupHostingAssetValidator extends HostingAssetEntityValidator { public class HsDomainDnsSetupHostingAssetValidator extends HostingAssetEntityValidator {
// according to RFC 1035 (section 5) and RFC 1034 // according to RFC 1035 (section 5) and RFC 1034
@ -33,7 +33,7 @@ public class HsDomainDnsSetupHostingAssetValidator extends HostingAssetEntityVal
RR_REGEX_NAME + RR_REGEX_IN + RR_REGEX_TTL + RR_RECORD_TYPE + RR_RECORD_DATA + RR_COMMENT; RR_REGEX_NAME + RR_REGEX_IN + RR_REGEX_TTL + RR_RECORD_TYPE + RR_RECORD_DATA + RR_COMMENT;
public static final String IDENTIFIER_SUFFIX = "|DNS"; public static final String IDENTIFIER_SUFFIX = "|DNS";
private static List<String> zoneFileErrors = null; // TODO.impl: remove once legacy data is migrated private static List<String> zoneFileErrors = null; // TODO.legacy: remove once legacy data is migrated
HsDomainDnsSetupHostingAssetValidator() { HsDomainDnsSetupHostingAssetValidator() {
super( super(

View File

@ -53,7 +53,7 @@ class HsUnixUserHostingAssetValidator extends HostingAssetEntityValidator {
} }
private static Integer computeUserId(final EntityManager em, final PropertiesProvider propertiesProvider) { private static Integer computeUserId(final EntityManager em, final PropertiesProvider propertiesProvider) {
final Object result = em.createNativeQuery("SELECT nextval('hs_hosting_asset_unixuser_system_id_seq')", Integer.class) final Object result = em.createNativeQuery("SELECT nextval('hs_hosting.asset_unixuser_system_id_seq')", Integer.class)
.getSingleResult(); .getSingleResult();
return (Integer) result; return (Integer) result;
} }

View File

@ -4,7 +4,7 @@ import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeBankAccountsApi; import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeBankAccountsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountInsertResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountResource;
import net.hostsharing.hsadminng.mapper.Mapper; import net.hostsharing.hsadminng.mapper.StandardMapper;
import org.iban4j.BicUtil; import org.iban4j.BicUtil;
import org.iban4j.IbanUtil; import org.iban4j.IbanUtil;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -24,7 +24,7 @@ public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
private Context context; private Context context;
@Autowired @Autowired
private Mapper mapper; private StandardMapper mapper;
@Autowired @Autowired
private HsOfficeBankAccountRepository bankAccountRepo; private HsOfficeBankAccountRepository bankAccountRepo;
@ -32,10 +32,10 @@ public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<List<HsOfficeBankAccountResource>> listBankAccounts( public ResponseEntity<List<HsOfficeBankAccountResource>> listBankAccounts(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final String holder) { final String holder) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var entities = bankAccountRepo.findByOptionalHolderLike(holder); final var entities = bankAccountRepo.findByOptionalHolderLike(holder);
@ -46,11 +46,11 @@ public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<HsOfficeBankAccountResource> addBankAccount( public ResponseEntity<HsOfficeBankAccountResource> addBankAccount(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final HsOfficeBankAccountInsertResource body) { final HsOfficeBankAccountInsertResource body) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
IbanUtil.validate(body.getIban()); IbanUtil.validate(body.getIban());
BicUtil.validate(body.getBic()); BicUtil.validate(body.getBic());
@ -72,11 +72,11 @@ public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<HsOfficeBankAccountResource> getBankAccountByUuid( public ResponseEntity<HsOfficeBankAccountResource> getBankAccountByUuid(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID bankAccountUuid) { final UUID bankAccountUuid) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var result = bankAccountRepo.findByUuid(bankAccountUuid); final var result = bankAccountRepo.findByUuid(bankAccountUuid);
if (result.isEmpty()) { if (result.isEmpty()) {
@ -88,10 +88,10 @@ public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<Void> deleteBankAccountByUuid( public ResponseEntity<Void> deleteBankAccountByUuid(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID BankAccountUuid) { final UUID BankAccountUuid) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var result = bankAccountRepo.deleteByUuid(BankAccountUuid); final var result = bankAccountRepo.deleteByUuid(BankAccountUuid);
if (result == 0) { if (result == 0) {

View File

@ -3,8 +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.DisplayAs; import net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.rbac.rbacobject.BaseEntity; import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.generator.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;
@ -12,14 +12,14 @@ import jakarta.persistence.*;
import java.io.IOException; 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.generator.RbacView.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR; import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify; import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity @Entity
@Table(name = "hs_office_bankaccount_rv") @Table(schema = "hs_office", name = "bankaccount_rv")
@Getter @Getter
@Setter @Setter
@Builder @Builder
@ -62,7 +62,7 @@ public class HsOfficeBankAccountEntity implements BaseEntity<HsOfficeBankAccount
.withIdentityView(SQL.projection("iban")) .withIdentityView(SQL.projection("iban"))
.withUpdatableColumns("holder", "iban", "bic") .withUpdatableColumns("holder", "iban", "bic")
.toRole("global", GUEST).grantPermission(INSERT) .toRole(GLOBAL, GUEST).grantPermission(INSERT)
.createRole(OWNER, (with) -> { .createRole(OWNER, (with) -> {
with.owningUser(CREATOR); with.owningUser(CREATOR);

View File

@ -11,7 +11,7 @@ import lombok.experimental.FieldNameConstants;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.errors.DisplayAs; import net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper; import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
import net.hostsharing.hsadminng.rbac.rbacobject.BaseEntity; import net.hostsharing.hsadminng.persistence.BaseEntity;
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;
@ -54,8 +54,14 @@ public class HsOfficeContact implements Stringifyable, BaseEntity<HsOfficeContac
@Column(name = "caption") @Column(name = "caption")
private String caption; private String caption;
@Builder.Default
@Setter(AccessLevel.NONE)
@Type(JsonType.class)
@Column(name = "postaladdress") @Column(name = "postaladdress")
private String postalAddress; // multiline free-format text private Map<String, String> postalAddress = new HashMap<>();
@Transient
private PatchableMapWrapper<String> postalAddressWrapper;
@Builder.Default @Builder.Default
@Setter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
@ -75,6 +81,17 @@ public class HsOfficeContact implements Stringifyable, BaseEntity<HsOfficeContac
@Transient @Transient
private PatchableMapWrapper<String> phoneNumbersWrapper; private PatchableMapWrapper<String> phoneNumbersWrapper;
public PatchableMapWrapper<String> getPostalAddress() {
return PatchableMapWrapper.of(
postalAddressWrapper,
(newWrapper) -> {postalAddressWrapper = newWrapper;},
postalAddress);
}
public void putPostalAddress(Map<String, String> newPostalAddress) {
getPostalAddress().assign(newPostalAddress);
}
public PatchableMapWrapper<String> getEmailAddresses() { public PatchableMapWrapper<String> getEmailAddresses() {
return PatchableMapWrapper.of( return PatchableMapWrapper.of(
emailAddressesWrapper, emailAddressesWrapper,

View File

@ -1,6 +1,6 @@
package net.hostsharing.hsadminng.hs.office.contact; package net.hostsharing.hsadminng.hs.office.contact;
import net.hostsharing.hsadminng.mapper.Mapper; import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeContactsApi; import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeContactsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactInsertResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactInsertResource;
@ -26,7 +26,7 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
private Context context; private Context context;
@Autowired @Autowired
private Mapper mapper; private StandardMapper mapper;
@Autowired @Autowired
private HsOfficeContactRbacRepository contactRepo; private HsOfficeContactRbacRepository contactRepo;
@ -34,10 +34,10 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<List<HsOfficeContactResource>> listContacts( public ResponseEntity<List<HsOfficeContactResource>> listContacts(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final String caption) { final String caption) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var entities = contactRepo.findContactByOptionalCaptionLike(caption); final var entities = contactRepo.findContactByOptionalCaptionLike(caption);
@ -48,11 +48,11 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<HsOfficeContactResource> addContact( public ResponseEntity<HsOfficeContactResource> addContact(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final HsOfficeContactInsertResource body) { final HsOfficeContactInsertResource body) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var entityToSave = mapper.map(body, HsOfficeContactRbacEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); final var entityToSave = mapper.map(body, HsOfficeContactRbacEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
@ -70,11 +70,11 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<HsOfficeContactResource> getContactByUuid( public ResponseEntity<HsOfficeContactResource> getContactByUuid(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID contactUuid) { final UUID contactUuid) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var result = contactRepo.findByUuid(contactUuid); final var result = contactRepo.findByUuid(contactUuid);
if (result.isEmpty()) { if (result.isEmpty()) {
@ -86,10 +86,10 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<Void> deleteContactByUuid( public ResponseEntity<Void> deleteContactByUuid(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID contactUuid) { final UUID contactUuid) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var result = contactRepo.deleteByUuid(contactUuid); final var result = contactRepo.deleteByUuid(contactUuid);
if (result == 0) { if (result == 0) {
@ -102,12 +102,12 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<HsOfficeContactResource> patchContact( public ResponseEntity<HsOfficeContactResource> patchContact(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID contactUuid, final UUID contactUuid,
final HsOfficeContactPatchResource body) { final HsOfficeContactPatchResource body) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var current = contactRepo.findByUuid(contactUuid).orElseThrow(); final var current = contactRepo.findByUuid(contactUuid).orElseThrow();

View File

@ -18,7 +18,8 @@ class HsOfficeContactEntityPatcher implements EntityPatcher<HsOfficeContactPatch
@Override @Override
public void apply(final HsOfficeContactPatchResource resource) { public void apply(final HsOfficeContactPatchResource resource) {
OptionalFromJson.of(resource.getCaption()).ifPresent(entity::setCaption); OptionalFromJson.of(resource.getCaption()).ifPresent(entity::setCaption);
OptionalFromJson.of(resource.getPostalAddress()).ifPresent(entity::setPostalAddress); Optional.ofNullable(resource.getPostalAddress())
.ifPresent(r -> entity.getPostalAddress().patch(KeyValueMap.from(resource.getPostalAddress())));
Optional.ofNullable(resource.getEmailAddresses()) Optional.ofNullable(resource.getEmailAddresses())
.ifPresent(r -> entity.getEmailAddresses().patch(KeyValueMap.from(resource.getEmailAddresses()))); .ifPresent(r -> entity.getEmailAddresses().patch(KeyValueMap.from(resource.getEmailAddresses())));
Optional.ofNullable(resource.getPhoneNumbers()) Optional.ofNullable(resource.getPhoneNumbers())

View File

@ -3,20 +3,20 @@ package net.hostsharing.hsadminng.hs.office.contact;
import lombok.*; import lombok.*;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.errors.DisplayAs; import net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.io.IOException; import java.io.IOException;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL; import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR; import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
@Entity @Entity
@Table(name = "hs_office_contact_rv") @Table(schema = "hs_office", name = "contact_rv")
@Getter @Getter
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor

View File

@ -10,7 +10,7 @@ import jakarta.persistence.Entity;
import jakarta.persistence.Table; import jakarta.persistence.Table;
@Entity @Entity
@Table(name = "hs_office_contact") @Table(schema = "hs_office", name = "contact")
@Getter @Getter
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor

View File

@ -4,7 +4,7 @@ import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopAssetsApi; import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopAssetsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*;
import net.hostsharing.hsadminng.errors.MultiValidationException; import net.hostsharing.hsadminng.errors.MultiValidationException;
import net.hostsharing.hsadminng.mapper.Mapper; import net.hostsharing.hsadminng.mapper.StandardMapper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.DateTimeFormat.ISO; import org.springframework.format.annotation.DateTimeFormat.ISO;
@ -29,7 +29,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
private Context context; private Context context;
@Autowired @Autowired
private Mapper mapper; private StandardMapper mapper;
@Autowired @Autowired
private HsOfficeCoopAssetsTransactionRepository coopAssetsTransactionRepo; private HsOfficeCoopAssetsTransactionRepository coopAssetsTransactionRepo;
@ -37,12 +37,12 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<List<HsOfficeCoopAssetsTransactionResource>> listCoopAssets( public ResponseEntity<List<HsOfficeCoopAssetsTransactionResource>> listCoopAssets(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID membershipUuid, final UUID membershipUuid,
final @DateTimeFormat(iso = ISO.DATE) LocalDate fromValueDate, final @DateTimeFormat(iso = ISO.DATE) LocalDate fromValueDate,
final @DateTimeFormat(iso = ISO.DATE) LocalDate toValueDate) { final @DateTimeFormat(iso = ISO.DATE) LocalDate toValueDate) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var entities = coopAssetsTransactionRepo.findCoopAssetsTransactionByOptionalMembershipUuidAndDateRange( final var entities = coopAssetsTransactionRepo.findCoopAssetsTransactionByOptionalMembershipUuidAndDateRange(
membershipUuid, membershipUuid,
@ -56,11 +56,11 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
@Override @Override
@Transactional @Transactional
public ResponseEntity<HsOfficeCoopAssetsTransactionResource> addCoopAssetsTransaction( public ResponseEntity<HsOfficeCoopAssetsTransactionResource> addCoopAssetsTransaction(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final HsOfficeCoopAssetsTransactionInsertResource requestBody) { final HsOfficeCoopAssetsTransactionInsertResource requestBody) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
validate(requestBody); validate(requestBody);
final var entityToSave = mapper.map(requestBody, HsOfficeCoopAssetsTransactionEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); final var entityToSave = mapper.map(requestBody, HsOfficeCoopAssetsTransactionEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
@ -79,9 +79,9 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<HsOfficeCoopAssetsTransactionResource> getCoopAssetTransactionByUuid( public ResponseEntity<HsOfficeCoopAssetsTransactionResource> getCoopAssetTransactionByUuid(
final String currentUser, final String assumedRoles, final UUID assetTransactionUuid) { final String currentSubject, final String assumedRoles, final UUID assetTransactionUuid) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var result = coopAssetsTransactionRepo.findByUuid(assetTransactionUuid); final var result = coopAssetsTransactionRepo.findByUuid(assetTransactionUuid);
if (result.isEmpty()) { if (result.isEmpty()) {

View File

@ -8,8 +8,8 @@ import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import net.hostsharing.hsadminng.errors.DisplayAs; import net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity; import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
import net.hostsharing.hsadminng.rbac.rbacobject.BaseEntity; import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.generator.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.GenericGenerator; import org.hibernate.annotations.GenericGenerator;
@ -21,20 +21,20 @@ 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.generator.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.SELECT; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.UPDATE; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.UPDATE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.ADMIN; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.AGENT; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.AGENT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.directlyFetchedByDependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify; import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity @Entity
@Table(name = "hs_office_coopassetstransaction_rv") @Table(schema = "hs_office", name = "coopassettx_rv")
@Getter @Getter
@Setter @Setter
@Builder @Builder

View File

@ -6,7 +6,7 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopShar
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionInsertResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionResource;
import net.hostsharing.hsadminng.errors.MultiValidationException; import net.hostsharing.hsadminng.errors.MultiValidationException;
import net.hostsharing.hsadminng.mapper.Mapper; import net.hostsharing.hsadminng.mapper.StandardMapper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.DateTimeFormat.ISO; import org.springframework.format.annotation.DateTimeFormat.ISO;
@ -31,7 +31,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
private Context context; private Context context;
@Autowired @Autowired
private Mapper mapper; private StandardMapper mapper;
@Autowired @Autowired
private HsOfficeCoopSharesTransactionRepository coopSharesTransactionRepo; private HsOfficeCoopSharesTransactionRepository coopSharesTransactionRepo;
@ -39,12 +39,12 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<List<HsOfficeCoopSharesTransactionResource>> listCoopShares( public ResponseEntity<List<HsOfficeCoopSharesTransactionResource>> listCoopShares(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID membershipUuid, final UUID membershipUuid,
final @DateTimeFormat(iso = ISO.DATE) LocalDate fromValueDate, final @DateTimeFormat(iso = ISO.DATE) LocalDate fromValueDate,
final @DateTimeFormat(iso = ISO.DATE) LocalDate toValueDate) { final @DateTimeFormat(iso = ISO.DATE) LocalDate toValueDate) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var entities = coopSharesTransactionRepo.findCoopSharesTransactionByOptionalMembershipUuidAndDateRange( final var entities = coopSharesTransactionRepo.findCoopSharesTransactionByOptionalMembershipUuidAndDateRange(
membershipUuid, membershipUuid,
@ -58,11 +58,11 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
@Override @Override
@Transactional @Transactional
public ResponseEntity<HsOfficeCoopSharesTransactionResource> addCoopSharesTransaction( public ResponseEntity<HsOfficeCoopSharesTransactionResource> addCoopSharesTransaction(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final HsOfficeCoopSharesTransactionInsertResource requestBody) { final HsOfficeCoopSharesTransactionInsertResource requestBody) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
validate(requestBody); validate(requestBody);
final var entityToSave = mapper.map(requestBody, HsOfficeCoopSharesTransactionEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); final var entityToSave = mapper.map(requestBody, HsOfficeCoopSharesTransactionEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
@ -81,9 +81,9 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<HsOfficeCoopSharesTransactionResource> getCoopShareTransactionByUuid( public ResponseEntity<HsOfficeCoopSharesTransactionResource> getCoopShareTransactionByUuid(
final String currentUser, final String assumedRoles, final UUID shareTransactionUuid) { final String currentSubject, final String assumedRoles, final UUID shareTransactionUuid) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var result = coopSharesTransactionRepo.findByUuid(shareTransactionUuid); final var result = coopSharesTransactionRepo.findByUuid(shareTransactionUuid);
if (result.isEmpty()) { if (result.isEmpty()) {

View File

@ -7,9 +7,9 @@ import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import net.hostsharing.hsadminng.errors.DisplayAs; import net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity; import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacView;
import net.hostsharing.hsadminng.rbac.rbacobject.BaseEntity; import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.generator.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;
@ -19,20 +19,20 @@ 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.generator.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.SELECT; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.UPDATE; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.UPDATE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.ADMIN; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.AGENT; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.AGENT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.directlyFetchedByDependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify; import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity @Entity
@Table(name = "hs_office_coopsharestransaction_rv") @Table(schema = "hs_office", name = "coopsharetx_rv")
@Getter @Getter
@Setter @Setter
@Builder @Builder

View File

@ -7,8 +7,8 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebito
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.HsOfficeRelationRealEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository;
import net.hostsharing.hsadminng.mapper.Mapper; import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.rbac.rbacobject.BaseEntity; import net.hostsharing.hsadminng.persistence.EntityExistsValidator;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.hibernate.Hibernate; import org.hibernate.Hibernate;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -23,7 +23,6 @@ import jakarta.validation.ValidationException;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import static net.hostsharing.hsadminng.errors.DisplayAs.DisplayName;
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR; import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR;
@RestController @RestController
@ -34,7 +33,7 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
private Context context; private Context context;
@Autowired @Autowired
private Mapper mapper; private StandardMapper mapper;
@Autowired @Autowired
private HsOfficeDebitorRepository debitorRepo; private HsOfficeDebitorRepository debitorRepo;
@ -42,17 +41,20 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
@Autowired @Autowired
private HsOfficeRelationRealRepository relrealRepo; private HsOfficeRelationRealRepository relrealRepo;
@Autowired
private EntityExistsValidator entityValidator;
@PersistenceContext @PersistenceContext
private EntityManager em; private EntityManager em;
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<List<HsOfficeDebitorResource>> listDebitors( public ResponseEntity<List<HsOfficeDebitorResource>> listDebitors(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final String name, final String name,
final Integer debitorNumber) { final Integer debitorNumber) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var entities = debitorNumber != null final var entities = debitorNumber != null
? debitorRepo.findDebitorByDebitorNumber(debitorNumber) ? debitorRepo.findDebitorByDebitorNumber(debitorNumber)
@ -65,35 +67,35 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<HsOfficeDebitorResource> addDebitor( public ResponseEntity<HsOfficeDebitorResource> addDebitor(
String currentUser, String currentSubject,
String assumedRoles, String assumedRoles,
HsOfficeDebitorInsertResource body) { HsOfficeDebitorInsertResource body) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
Validate.isTrue(body.getDebitorRel() == null || body.getDebitorRelUuid() == null, Validate.isTrue(body.getDebitorRel() == null || body.getDebitorRelUuid() == null,
"ERROR: [400] exactly one of debitorRel and debitorRelUuid must be supplied, but found both"); "ERROR: [400] exactly one of debitorRel and debitorRelUuid must be supplied, but found both");
Validate.isTrue(body.getDebitorRel() != null || body.getDebitorRelUuid() != null, Validate.isTrue(body.getDebitorRel() != null || body.getDebitorRelUuid() != null,
"ERROR: [400] exactly one of debitorRel and debitorRelUuid must be supplied, but found none"); "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, Validate.isTrue(body.getDebitorRel() == null || body.getDebitorRel().getMark() == null,
"ERROR: [400] debitorRel.mark must be null"); "ERROR: [400] debitorRel.mark must be null");
final var entityToSave = mapper.map(body, HsOfficeDebitorEntity.class); final var entityToSave = mapper.map(body, HsOfficeDebitorEntity.class);
if ( body.getDebitorRel() != null ) { if (body.getDebitorRel() != null) {
body.getDebitorRel().setType(DEBITOR.name()); final var debitorRel = mapper.map("debitorRel.", body.getDebitorRel(), HsOfficeRelationRealEntity.class);
final var debitorRel = mapper.map(body.getDebitorRel(), HsOfficeRelationRealEntity.class); debitorRel.setType(DEBITOR);
validateEntityExists("debitorRel.anchorUuid", debitorRel.getAnchor()); entityValidator.validateEntityExists("debitorRel.anchorUuid", debitorRel.getAnchor());
validateEntityExists("debitorRel.holderUuid", debitorRel.getHolder()); entityValidator.validateEntityExists("debitorRel.holderUuid", debitorRel.getHolder());
validateEntityExists("debitorRel.contactUuid", debitorRel.getContact()); entityValidator.validateEntityExists("debitorRel.contactUuid", debitorRel.getContact());
entityToSave.setDebitorRel(relrealRepo.save(debitorRel)); entityToSave.setDebitorRel(relrealRepo.save(debitorRel));
} else { } else {
final var debitorRelOptional = relrealRepo.findByUuid(body.getDebitorRelUuid()); final var debitorRelOptional = relrealRepo.findByUuid(body.getDebitorRelUuid());
debitorRelOptional.ifPresentOrElse( debitorRelOptional.ifPresentOrElse(
debitorRel -> {entityToSave.setDebitorRel(relrealRepo.save(debitorRel));}, debitorRel -> {entityToSave.setDebitorRel(relrealRepo.save(debitorRel));},
() -> { throw new ValidationException("Unable to find RealRelation by debitorRelUuid: " + body.getDebitorRelUuid());}); () -> {
throw new ValidationException(
"Unable to find RealRelation by debitorRelUuid: " + body.getDebitorRelUuid());
});
} }
final var savedEntity = debitorRepo.save(entityToSave); final var savedEntity = debitorRepo.save(entityToSave);
@ -112,11 +114,11 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<HsOfficeDebitorResource> getDebitorByUuid( public ResponseEntity<HsOfficeDebitorResource> getDebitorByUuid(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID debitorUuid) { final UUID debitorUuid) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var result = debitorRepo.findByUuid(debitorUuid); final var result = debitorRepo.findByUuid(debitorUuid);
if (result.isEmpty()) { if (result.isEmpty()) {
@ -128,10 +130,10 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<Void> deleteDebitorByUuid( public ResponseEntity<Void> deleteDebitorByUuid(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID debitorUuid) { final UUID debitorUuid) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var result = debitorRepo.deleteByUuid(debitorUuid); final var result = debitorRepo.deleteByUuid(debitorUuid);
if (result == 0) { if (result == 0) {
@ -144,12 +146,12 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<HsOfficeDebitorResource> patchDebitor( public ResponseEntity<HsOfficeDebitorResource> patchDebitor(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID debitorUuid, final UUID debitorUuid,
final HsOfficeDebitorPatchResource body) { final HsOfficeDebitorPatchResource body) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var current = debitorRepo.findByUuid(debitorUuid).orElseThrow(); final var current = debitorRepo.findByUuid(debitorUuid).orElseThrow();
@ -160,15 +162,4 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
final var mapped = mapper.map(saved, HsOfficeDebitorResource.class); final var mapped = mapper.map(saved, HsOfficeDebitorResource.class);
return ResponseEntity.ok(mapped); return ResponseEntity.ok(mapped);
} }
// TODO.impl: extract this to some generally usable class?
private <T extends BaseEntity<T>> T validateEntityExists(final String property, final T entitySkeleton) {
final var foundEntity = em.find(entitySkeleton.getClass(), entitySkeleton.getUuid());
if ( foundEntity == null) {
throw new ValidationException("Unable to find " + DisplayName.of(entitySkeleton) + " by " + property + ": " + entitySkeleton.getUuid());
}
//noinspection unchecked
return (T) foundEntity;
}
} }

View File

@ -11,9 +11,9 @@ import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelation; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelation;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
import net.hostsharing.hsadminng.rbac.rbacobject.BaseEntity; import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.generator.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;
@ -40,20 +40,21 @@ import static jakarta.persistence.CascadeType.PERSIST;
import static jakarta.persistence.CascadeType.REFRESH; import static jakarta.persistence.CascadeType.REFRESH;
import static java.util.Optional.ofNullable; import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR; import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingCase; import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL; import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NULLABLE; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NULLABLE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.directlyFetchedByDependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.fetchedBySql; import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.fetchedBySql;
import static net.hostsharing.hsadminng.rbac.generator.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(schema = "hs_office", name = "debitor_rv")
@Getter @Getter
@Setter @Setter
@Builder(toBuilder = true) @Builder(toBuilder = true)
@ -86,10 +87,10 @@ public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>,
value = """ value = """
( (
SELECT DISTINCT partner.uuid SELECT DISTINCT partner.uuid
FROM hs_office_partner_rv partner FROM hs_office.partner_rv partner
JOIN hs_office_relation_rv dRel JOIN hs_office.relation_rv dRel
ON dRel.uuid = debitorreluuid AND dRel.type = 'DEBITOR' ON dRel.uuid = debitorreluuid AND dRel.type = 'DEBITOR'
JOIN hs_office_relation_rv pRel JOIN hs_office.relation_rv pRel
ON pRel.uuid = partner.partnerRelUuid AND pRel.type = 'PARTNER' ON pRel.uuid = partner.partnerRelUuid AND pRel.type = 'PARTNER'
WHERE pRel.holderUuid = dRel.anchorUuid WHERE pRel.holderUuid = dRel.anchorUuid
) )
@ -169,14 +170,14 @@ public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>,
.withIdentityView(SQL.query(""" .withIdentityView(SQL.query("""
SELECT debitor.uuid AS uuid, SELECT debitor.uuid AS uuid,
'D-' || (SELECT partner.partnerNumber 'D-' || (SELECT partner.partnerNumber
FROM hs_office_partner partner FROM hs_office.partner partner
JOIN hs_office_relation partnerRel JOIN hs_office.relation partnerRel
ON partnerRel.uuid = partner.partnerRelUUid AND partnerRel.type = 'PARTNER' ON partnerRel.uuid = partner.partnerRelUUid AND partnerRel.type = 'PARTNER'
JOIN hs_office_relation debitorRel JOIN hs_office.relation debitorRel
ON debitorRel.anchorUuid = partnerRel.holderUuid AND debitorRel.type = 'DEBITOR' ON debitorRel.anchorUuid = partnerRel.holderUuid AND debitorRel.type = 'DEBITOR'
WHERE debitorRel.uuid = debitor.debitorRelUuid) WHERE debitorRel.uuid = debitor.debitorRelUuid)
|| debitorNumberSuffix as idName || debitorNumberSuffix as idName
FROM hs_office_debitor AS debitor FROM hs_office.debitor AS debitor
""")) """))
.withRestrictedViewOrderBy(SQL.projection("defaultPrefix")) .withRestrictedViewOrderBy(SQL.projection("defaultPrefix"))
.withUpdatableColumns( .withUpdatableColumns(
@ -188,7 +189,7 @@ public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>,
"vatBusiness", "vatBusiness",
"vatReverseCharge", "vatReverseCharge",
"defaultPrefix") "defaultPrefix")
.toRole("global", ADMIN).grantPermission(INSERT) .toRole(GLOBAL, ADMIN).grantPermission(INSERT)
.importRootEntityAliasProxy("debitorRel", HsOfficeRelationRbacEntity.class, usingCase(DEBITOR), .importRootEntityAliasProxy("debitorRel", HsOfficeRelationRbacEntity.class, usingCase(DEBITOR),
directlyFetchedByDependsOnColumn(), directlyFetchedByDependsOnColumn(),
@ -208,8 +209,8 @@ public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>,
dependsOnColumn("debitorRelUuid"), dependsOnColumn("debitorRelUuid"),
fetchedBySql(""" fetchedBySql("""
SELECT ${columns} SELECT ${columns}
FROM hs_office_relation AS partnerRel FROM hs_office.relation AS partnerRel
JOIN hs_office_relation AS debitorRel JOIN hs_office.relation AS debitorRel
ON debitorRel.type = 'DEBITOR' AND debitorRel.anchorUuid = partnerRel.holderUuid ON debitorRel.type = 'DEBITOR' AND debitorRel.anchorUuid = partnerRel.holderUuid
WHERE partnerRel.type = 'PARTNER' WHERE partnerRel.type = 'PARTNER'
AND ${REF}.debitorRelUuid = debitorRel.uuid AND ${REF}.debitorRelUuid = debitorRel.uuid

View File

@ -5,7 +5,7 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeMembersh
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipInsertResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipInsertResource;
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.hs.office.generated.api.v1.model.HsOfficeMembershipResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipResource;
import net.hostsharing.hsadminng.mapper.Mapper; import net.hostsharing.hsadminng.mapper.StandardMapper;
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;
@ -24,7 +24,7 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
private Context context; private Context context;
@Autowired @Autowired
private Mapper mapper; private StandardMapper mapper;
@Autowired @Autowired
private HsOfficeMembershipRepository membershipRepo; private HsOfficeMembershipRepository membershipRepo;
@ -32,11 +32,11 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<List<HsOfficeMembershipResource>> listMemberships( public ResponseEntity<List<HsOfficeMembershipResource>> listMemberships(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
UUID partnerUuid, final UUID partnerUuid,
Integer memberNumber) { final Integer memberNumber) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var entities = ( memberNumber != null) final var entities = ( memberNumber != null)
? List.of(membershipRepo.findMembershipByMemberNumber(memberNumber)) ? List.of(membershipRepo.findMembershipByMemberNumber(memberNumber))
@ -50,11 +50,11 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<HsOfficeMembershipResource> addMembership( public ResponseEntity<HsOfficeMembershipResource> addMembership(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final HsOfficeMembershipInsertResource body) { final HsOfficeMembershipInsertResource body) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var entityToSave = mapper.map(body, HsOfficeMembershipEntity.class); final var entityToSave = mapper.map(body, HsOfficeMembershipEntity.class);
@ -73,11 +73,11 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<HsOfficeMembershipResource> getMembershipByUuid( public ResponseEntity<HsOfficeMembershipResource> getMembershipByUuid(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID membershipUuid) { final UUID membershipUuid) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var result = membershipRepo.findByUuid(membershipUuid); final var result = membershipRepo.findByUuid(membershipUuid);
if (result.isEmpty()) { if (result.isEmpty()) {
@ -90,10 +90,10 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<Void> deleteMembershipByUuid( public ResponseEntity<Void> deleteMembershipByUuid(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID membershipUuid) { final UUID membershipUuid) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var result = membershipRepo.deleteByUuid(membershipUuid); final var result = membershipRepo.deleteByUuid(membershipUuid);
if (result == 0) { if (result == 0) {
@ -106,12 +106,12 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<HsOfficeMembershipResource> patchMembership( public ResponseEntity<HsOfficeMembershipResource> patchMembership(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID membershipUuid, final UUID membershipUuid,
final HsOfficeMembershipPatchResource body) { final HsOfficeMembershipPatchResource body) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var current = membershipRepo.findByUuid(membershipUuid).orElseThrow(); final var current = membershipRepo.findByUuid(membershipUuid).orElseThrow();

View File

@ -9,10 +9,10 @@ import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import net.hostsharing.hsadminng.errors.DisplayAs; import net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
import net.hostsharing.hsadminng.rbac.rbacobject.BaseEntity; import net.hostsharing.hsadminng.persistence.BaseEntity;
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.generator.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.generator.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.Type; import org.hibernate.annotations.Type;
@ -38,24 +38,25 @@ import static io.hypersistence.utils.hibernate.type.range.Range.emptyRange;
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.lowerInclusiveFromPostgresDateRange; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.lowerInclusiveFromPostgresDateRange;
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.upperInclusiveFromPostgresDateRange; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.upperInclusiveFromPostgresDateRange;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL; import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.DELETE; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.DELETE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.SELECT; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.UPDATE; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.UPDATE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.ADMIN; import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.AGENT; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.OWNER; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.AGENT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.TENANT; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.OWNER;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.fetchedBySql; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.TENANT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.fetchedBySql;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify; import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity @Entity
@Table(name = "hs_office_membership_rv") @Table(schema = "hs_office", name = "membership_rv")
@Getter @Getter
@Setter @Setter
@Builder @Builder
@ -159,8 +160,8 @@ public class HsOfficeMembershipEntity implements BaseEntity<HsOfficeMembershipEn
.withIdentityView(SQL.query(""" .withIdentityView(SQL.query("""
SELECT m.uuid AS uuid, SELECT m.uuid AS uuid,
'M-' || p.partnerNumber || m.memberNumberSuffix as idName 'M-' || p.partnerNumber || m.memberNumberSuffix as idName
FROM hs_office_membership AS m FROM hs_office.membership AS m
JOIN hs_office_partner AS p ON p.uuid = m.partnerUuid JOIN hs_office.partner AS p ON p.uuid = m.partnerUuid
""")) """))
.withRestrictedViewOrderBy(SQL.projection("validity")) .withRestrictedViewOrderBy(SQL.projection("validity"))
.withUpdatableColumns("validity", "membershipFeeBillable", "status") .withUpdatableColumns("validity", "membershipFeeBillable", "status")
@ -169,12 +170,12 @@ public class HsOfficeMembershipEntity implements BaseEntity<HsOfficeMembershipEn
dependsOnColumn("partnerUuid"), dependsOnColumn("partnerUuid"),
fetchedBySql(""" fetchedBySql("""
SELECT ${columns} SELECT ${columns}
FROM hs_office_partner AS partner FROM hs_office.partner AS partner
JOIN hs_office_relation AS partnerRel ON partnerRel.uuid = partner.partnerRelUuid JOIN hs_office.relation AS partnerRel ON partnerRel.uuid = partner.partnerRelUuid
WHERE partner.uuid = ${REF}.partnerUuid WHERE partner.uuid = ${REF}.partnerUuid
"""), """),
NOT_NULL) NOT_NULL)
.toRole("global", ADMIN).grantPermission(INSERT) .toRole(GLOBAL, ADMIN).grantPermission(INSERT)
.createRole(OWNER, (with) -> { .createRole(OWNER, (with) -> {
with.owningUser(CREATOR); with.owningUser(CREATOR);

View File

@ -2,18 +2,18 @@ package net.hostsharing.hsadminng.hs.office.membership;
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.StandardMapper;
import net.hostsharing.hsadminng.mapper.OptionalFromJson; import net.hostsharing.hsadminng.mapper.OptionalFromJson;
import java.util.Optional; import java.util.Optional;
public class HsOfficeMembershipEntityPatcher implements EntityPatcher<HsOfficeMembershipPatchResource> { public class HsOfficeMembershipEntityPatcher implements EntityPatcher<HsOfficeMembershipPatchResource> {
private final Mapper mapper; private final StandardMapper mapper;
private final HsOfficeMembershipEntity entity; private final HsOfficeMembershipEntity entity;
public HsOfficeMembershipEntityPatcher( public HsOfficeMembershipEntityPatcher(
final Mapper mapper, final StandardMapper mapper,
final HsOfficeMembershipEntity entity) { final HsOfficeMembershipEntity entity) {
this.mapper = mapper; this.mapper = mapper;
this.entity = entity; this.entity = entity;

View File

@ -12,8 +12,8 @@ import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType;
import net.hostsharing.hsadminng.mapper.Mapper; import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.rbac.rbacobject.BaseEntity; import net.hostsharing.hsadminng.persistence.BaseEntity;
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;
@ -36,7 +36,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
private Context context; private Context context;
@Autowired @Autowired
private Mapper mapper; private StandardMapper mapper;
@Autowired @Autowired
private HsOfficePartnerRepository partnerRepo; private HsOfficePartnerRepository partnerRepo;
@ -50,10 +50,10 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<List<HsOfficePartnerResource>> listPartners( public ResponseEntity<List<HsOfficePartnerResource>> listPartners(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final String name) { final String name) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var entities = partnerRepo.findPartnerByOptionalNameLike(name); final var entities = partnerRepo.findPartnerByOptionalNameLike(name);
@ -64,11 +64,11 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<HsOfficePartnerResource> addPartner( public ResponseEntity<HsOfficePartnerResource> addPartner(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final HsOfficePartnerInsertResource body) { final HsOfficePartnerInsertResource body) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var entityToSave = createPartnerEntity(body); final var entityToSave = createPartnerEntity(body);
@ -86,11 +86,11 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<HsOfficePartnerResource> getPartnerByUuid( public ResponseEntity<HsOfficePartnerResource> getPartnerByUuid(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID partnerUuid) { final UUID partnerUuid) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var result = partnerRepo.findByUuid(partnerUuid); final var result = partnerRepo.findByUuid(partnerUuid);
if (result.isEmpty()) { if (result.isEmpty()) {
@ -102,10 +102,10 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<Void> deletePartnerByUuid( public ResponseEntity<Void> deletePartnerByUuid(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID partnerUuid) { final UUID partnerUuid) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var partnerToDelete = partnerRepo.findByUuid(partnerUuid); final var partnerToDelete = partnerRepo.findByUuid(partnerUuid);
if (partnerToDelete.isEmpty()) { if (partnerToDelete.isEmpty()) {
@ -122,12 +122,12 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<HsOfficePartnerResource> patchPartner( public ResponseEntity<HsOfficePartnerResource> patchPartner(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID partnerUuid, final UUID partnerUuid,
final HsOfficePartnerPatchResource body) { final HsOfficePartnerPatchResource body) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var current = partnerRepo.findByUuid(partnerUuid).orElseThrow(); final var current = partnerRepo.findByUuid(partnerUuid).orElseThrow();
final var previousPartnerRel = current.getPartnerRel(); final var previousPartnerRel = current.getPartnerRel();

View File

@ -2,9 +2,9 @@ package net.hostsharing.hsadminng.hs.office.partner;
import lombok.*; import lombok.*;
import net.hostsharing.hsadminng.errors.DisplayAs; import net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.rbac.rbacobject.BaseEntity; import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.generator.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;
@ -13,13 +13,14 @@ 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.generator.RbacView.GLOBAL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify; import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity @Entity
@Table(name = "hs_office_partner_details_rv") @Table(schema = "hs_office", name = "partner_details_rv")
@Getter @Getter
@Setter @Setter
@Builder @Builder
@ -70,9 +71,9 @@ public class HsOfficePartnerDetailsEntity implements BaseEntity<HsOfficePartnerD
return rbacViewFor("partnerDetails", HsOfficePartnerDetailsEntity.class) return rbacViewFor("partnerDetails", HsOfficePartnerDetailsEntity.class)
.withIdentityView(SQL.query(""" .withIdentityView(SQL.query("""
SELECT partnerDetails.uuid as uuid, partner_iv.idName as idName SELECT partnerDetails.uuid as uuid, partner_iv.idName as idName
FROM hs_office_partner_details AS partnerDetails FROM hs_office.partner_details AS partnerDetails
JOIN hs_office_partner partner ON partner.detailsUuid = partnerDetails.uuid JOIN hs_office.partner partner ON partner.detailsUuid = partnerDetails.uuid
JOIN hs_office_partner_iv partner_iv ON partner_iv.uuid = partner.uuid JOIN hs_office.partner_iv partner_iv ON partner_iv.uuid = partner.uuid
""")) """))
.withRestrictedViewOrderBy(SQL.expression("uuid")) .withRestrictedViewOrderBy(SQL.expression("uuid"))
.withUpdatableColumns( .withUpdatableColumns(
@ -82,7 +83,7 @@ public class HsOfficePartnerDetailsEntity implements BaseEntity<HsOfficePartnerD
"birthName", "birthName",
"birthday", "birthday",
"dateOfDeath") "dateOfDeath")
.toRole("global", ADMIN).grantPermission(INSERT) .toRole(GLOBAL, ADMIN).grantPermission(INSERT)
// The grants are defined in HsOfficePartnerEntity.rbac() // The grants are defined in HsOfficePartnerEntity.rbac()
// because they have to be changed when its partnerRel changes, // because they have to be changed when its partnerRel changes,

View File

@ -10,10 +10,10 @@ import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContact;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
import net.hostsharing.hsadminng.rbac.rbacobject.BaseEntity; import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelation; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelation;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.generator.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;
@ -24,18 +24,19 @@ import java.io.IOException;
import java.util.UUID; import java.util.UUID;
import static jakarta.persistence.CascadeType.*; import static jakarta.persistence.CascadeType.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*; import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.SELECT; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.directlyFetchedByDependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
import static java.util.Optional.ofNullable; import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify; import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity @Entity
@Table(name = "hs_office_partner_rv") @Table(schema = "hs_office", name = "partner_rv")
@Getter @Getter
@Setter @Setter
@Builder @Builder
@ -103,7 +104,7 @@ public class HsOfficePartnerEntity implements Stringifyable, BaseEntity<HsOffice
return rbacViewFor("partner", HsOfficePartnerEntity.class) return rbacViewFor("partner", HsOfficePartnerEntity.class)
.withIdentityView(SQL.projection("'P-' || partnerNumber")) .withIdentityView(SQL.projection("'P-' || partnerNumber"))
.withUpdatableColumns("partnerRelUuid") .withUpdatableColumns("partnerRelUuid")
.toRole("global", ADMIN).grantPermission(INSERT) .toRole(GLOBAL, ADMIN).grantPermission(INSERT)
.importRootEntityAliasProxy("partnerRel", HsOfficeRelationRbacEntity.class, .importRootEntityAliasProxy("partnerRel", HsOfficeRelationRbacEntity.class,
usingDefaultCase(), usingDefaultCase(),

View File

@ -1,6 +1,6 @@
package net.hostsharing.hsadminng.hs.office.person; package net.hostsharing.hsadminng.hs.office.person;
import net.hostsharing.hsadminng.mapper.Mapper; import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficePersonsApi; import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficePersonsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonInsertResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonInsertResource;
@ -23,7 +23,7 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
private Context context; private Context context;
@Autowired @Autowired
private Mapper mapper; private StandardMapper mapper;
@Autowired @Autowired
private HsOfficePersonRepository personRepo; private HsOfficePersonRepository personRepo;
@ -31,10 +31,10 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<List<HsOfficePersonResource>> listPersons( public ResponseEntity<List<HsOfficePersonResource>> listPersons(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final String caption) { final String caption) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var entities = personRepo.findPersonByOptionalNameLike(caption); final var entities = personRepo.findPersonByOptionalNameLike(caption);
@ -45,11 +45,11 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<HsOfficePersonResource> addPerson( public ResponseEntity<HsOfficePersonResource> addPerson(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final HsOfficePersonInsertResource body) { final HsOfficePersonInsertResource body) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var entityToSave = mapper.map(body, HsOfficePersonEntity.class); final var entityToSave = mapper.map(body, HsOfficePersonEntity.class);
@ -67,11 +67,11 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<HsOfficePersonResource> getPersonByUuid( public ResponseEntity<HsOfficePersonResource> getPersonByUuid(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID personUuid) { final UUID personUuid) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var result = personRepo.findByUuid(personUuid); final var result = personRepo.findByUuid(personUuid);
if (result.isEmpty()) { if (result.isEmpty()) {
@ -83,10 +83,10 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<Void> deletePersonByUuid( public ResponseEntity<Void> deletePersonByUuid(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID personUuid) { final UUID personUuid) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var result = personRepo.deleteByUuid(personUuid); final var result = personRepo.deleteByUuid(personUuid);
if (result == 0) { if (result == 0) {
@ -99,12 +99,12 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<HsOfficePersonResource> patchPerson( public ResponseEntity<HsOfficePersonResource> patchPerson(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID personUuid, final UUID personUuid,
final HsOfficePersonPatchResource body) { final HsOfficePersonPatchResource body) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var current = personRepo.findByUuid(personUuid).orElseThrow(); final var current = personRepo.findByUuid(personUuid).orElseThrow();

View File

@ -3,9 +3,9 @@ package net.hostsharing.hsadminng.hs.office.person;
import lombok.*; import lombok.*;
import lombok.experimental.FieldNameConstants; import lombok.experimental.FieldNameConstants;
import net.hostsharing.hsadminng.errors.DisplayAs; import net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.rbac.rbacobject.BaseEntity; import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.generator.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;
@ -14,15 +14,16 @@ import jakarta.persistence.*;
import java.io.IOException; 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.generator.RbacView.GLOBAL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR; import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify; import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
// TODO.refa: split HsOfficePersonEntity into Real+Rbac-Entity
@Entity @Entity
@Table(name = "hs_office_person_rv") @Table(schema = "hs_office", name = "person_rv")
@Getter @Getter
@Setter @Setter
@Builder @Builder
@ -80,7 +81,7 @@ public class HsOfficePersonEntity implements BaseEntity<HsOfficePersonEntity>, S
return rbacViewFor("person", HsOfficePersonEntity.class) return rbacViewFor("person", HsOfficePersonEntity.class)
.withIdentityView(SQL.projection("concat(tradeName, familyName, givenName)")) .withIdentityView(SQL.projection("concat(tradeName, familyName, givenName)"))
.withUpdatableColumns("personType", "title", "salutation", "tradeName", "givenName", "familyName") .withUpdatableColumns("personType", "title", "salutation", "tradeName", "givenName", "familyName")
.toRole("global", GUEST).grantPermission(INSERT) .toRole(GLOBAL, GUEST).grantPermission(INSERT)
.createRole(OWNER, (with) -> { .createRole(OWNER, (with) -> {
with.permission(DELETE); with.permission(DELETE);

View File

@ -5,7 +5,7 @@ import lombok.experimental.FieldNameConstants;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
import net.hostsharing.hsadminng.rbac.rbacobject.BaseEntity; import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable; import net.hostsharing.hsadminng.stringify.Stringifyable;

View File

@ -5,7 +5,7 @@ import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealRepository
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeRelationsApi; 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.StandardMapper;
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;
@ -28,7 +28,7 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
private Context context; private Context context;
@Autowired @Autowired
private Mapper mapper; private StandardMapper mapper;
@Autowired @Autowired
private HsOfficeRelationRbacRepository relationRbacRepo; private HsOfficeRelationRbacRepository relationRbacRepo;
@ -37,7 +37,7 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
private HsOfficePersonRepository holderRepo; private HsOfficePersonRepository holderRepo;
@Autowired @Autowired
private HsOfficeContactRealRepository contactrealRepo; private HsOfficeContactRealRepository realContactRepo;
@PersistenceContext @PersistenceContext
private EntityManager em; private EntityManager em;
@ -45,14 +45,19 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<List<HsOfficeRelationResource>> listRelations( public ResponseEntity<List<HsOfficeRelationResource>> listRelations(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID personUuid, final UUID personUuid,
final HsOfficeRelationTypeResource relationType) { final HsOfficeRelationTypeResource relationType,
context.define(currentUser, assumedRoles); final String personData,
final String contactData) {
context.define(currentSubject, assumedRoles);
final var entities = relationRbacRepo.findRelationRelatedToPersonUuidAndRelationType(personUuid, final List<HsOfficeRelationRbacEntity> entities =
mapper.map(relationType, HsOfficeRelationType.class)); relationRbacRepo.findRelationRelatedToPersonUuidRelationTypePersonAndContactData(
personUuid,
relationType == null ? null : HsOfficeRelationType.valueOf(relationType.name()),
personData, contactData);
final var resources = mapper.mapList(entities, HsOfficeRelationResource.class, final var resources = mapper.mapList(entities, HsOfficeRelationResource.class,
RELATION_ENTITY_TO_RESOURCE_POSTMAPPER); RELATION_ENTITY_TO_RESOURCE_POSTMAPPER);
@ -62,11 +67,11 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<HsOfficeRelationResource> addRelation( public ResponseEntity<HsOfficeRelationResource> addRelation(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final HsOfficeRelationInsertResource body) { final HsOfficeRelationInsertResource body) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var entityToSave = new HsOfficeRelationRbacEntity(); final var entityToSave = new HsOfficeRelationRbacEntity();
entityToSave.setType(HsOfficeRelationType.valueOf(body.getType())); entityToSave.setType(HsOfficeRelationType.valueOf(body.getType()));
@ -77,7 +82,7 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
entityToSave.setHolder(holderRepo.findByUuid(body.getHolderUuid()).orElseThrow( entityToSave.setHolder(holderRepo.findByUuid(body.getHolderUuid()).orElseThrow(
() -> new NoSuchElementException("cannot find Person by holderUuid: " + body.getHolderUuid()) () -> new NoSuchElementException("cannot find Person by holderUuid: " + body.getHolderUuid())
)); ));
entityToSave.setContact(contactrealRepo.findByUuid(body.getContactUuid()).orElseThrow( entityToSave.setContact(realContactRepo.findByUuid(body.getContactUuid()).orElseThrow(
() -> new NoSuchElementException("cannot find Contact by contactUuid: " + body.getContactUuid()) () -> new NoSuchElementException("cannot find Contact by contactUuid: " + body.getContactUuid())
)); ));
@ -96,11 +101,11 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<HsOfficeRelationResource> getRelationByUuid( public ResponseEntity<HsOfficeRelationResource> getRelationByUuid(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID relationUuid) { final UUID relationUuid) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var result = relationRbacRepo.findByUuid(relationUuid); final var result = relationRbacRepo.findByUuid(relationUuid);
if (result.isEmpty()) { if (result.isEmpty()) {
@ -112,10 +117,10 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<Void> deleteRelationByUuid( public ResponseEntity<Void> deleteRelationByUuid(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID relationUuid) { final UUID relationUuid) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var result = relationRbacRepo.deleteByUuid(relationUuid); final var result = relationRbacRepo.deleteByUuid(relationUuid);
if (result == 0) { if (result == 0) {
@ -128,12 +133,12 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<HsOfficeRelationResource> patchRelation( public ResponseEntity<HsOfficeRelationResource> patchRelation(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID relationUuid, final UUID relationUuid,
final HsOfficeRelationPatchResource body) { final HsOfficeRelationPatchResource body) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var current = relationRbacRepo.findByUuid(relationUuid).orElseThrow(); final var current = relationRbacRepo.findByUuid(relationUuid).orElseThrow();
@ -144,7 +149,6 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
return ResponseEntity.ok(mapped); return ResponseEntity.ok(mapped);
} }
final BiConsumer<HsOfficeRelationRbacEntity, HsOfficeRelationResource> RELATION_ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { final BiConsumer<HsOfficeRelationRbacEntity, HsOfficeRelationResource> RELATION_ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
resource.setAnchor(mapper.map(entity.getAnchor(), HsOfficePersonResource.class)); resource.setAnchor(mapper.map(entity.getAnchor(), HsOfficePersonResource.class));
resource.setHolder(mapper.map(entity.getHolder(), HsOfficePersonResource.class)); resource.setHolder(mapper.map(entity.getHolder(), HsOfficePersonResource.class));

View File

@ -7,34 +7,34 @@ import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.errors.DisplayAs; import net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRbacEntity; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRbacEntity;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.io.IOException; import java.io.IOException;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.CaseDef.inCaseOf; import static net.hostsharing.hsadminng.rbac.generator.RbacView.CaseDef.inCaseOf;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.CaseDef.inOtherCases; import static net.hostsharing.hsadminng.rbac.generator.RbacView.CaseDef.inOtherCases;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL; import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.DELETE; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.DELETE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.SELECT; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.UPDATE; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.UPDATE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR; import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.ADMIN; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.AGENT; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.AGENT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.OWNER; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.OWNER;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.REFERRER; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.REFERRER;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.TENANT; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.TENANT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.directlyFetchedByDependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
@Entity @Entity
@Table(name = "hs_office_relation_rv") @Table(schema = "hs_office", name = "relation_rv")
@NoArgsConstructor @NoArgsConstructor
@Getter @Getter
@Setter @Setter
@ -45,12 +45,12 @@ public class HsOfficeRelationRbacEntity extends HsOfficeRelation {
public static RbacView rbac() { public static RbacView rbac() {
return rbacViewFor("relation", HsOfficeRelationRbacEntity.class) return rbacViewFor("relation", HsOfficeRelationRbacEntity.class)
.withIdentityView(SQL.projection(""" .withIdentityView(SQL.projection("""
(select idName from hs_office_person_iv p where p.uuid = anchorUuid) (select idName from hs_office.person_iv p where p.uuid = anchorUuid)
|| '-with-' || target.type || '-' || '-with-' || target.type || '-'
|| (select idName from hs_office_person_iv p where p.uuid = holderUuid) || (select idName from hs_office.person_iv p where p.uuid = holderUuid)
""")) """))
.withRestrictedViewOrderBy(SQL.expression( .withRestrictedViewOrderBy(SQL.expression(
"(select idName from hs_office_person_iv p where p.uuid = target.holderUuid)")) "(select idName from hs_office.person_iv p where p.uuid = target.holderUuid)"))
.withUpdatableColumns("contactUuid") .withUpdatableColumns("contactUuid")
.importEntityAlias("anchorPerson", HsOfficePersonEntity.class, usingDefaultCase(), .importEntityAlias("anchorPerson", HsOfficePersonEntity.class, usingDefaultCase(),
dependsOnColumn("anchorUuid"), dependsOnColumn("anchorUuid"),

View File

@ -12,26 +12,62 @@ public interface HsOfficeRelationRbacRepository extends Repository<HsOfficeRelat
Optional<HsOfficeRelationRbacEntity> findByUuid(UUID id); Optional<HsOfficeRelationRbacEntity> findByUuid(UUID id);
default List<HsOfficeRelationRbacEntity> findRelationRelatedToPersonUuidAndRelationType(@NotNull UUID personUuid, HsOfficeRelationType relationType) { @Query(value = """
return findRelationRelatedToPersonUuidAndRelationTypeString(personUuid, relationType.toString()); SELECT p.* FROM hs_office.relation_rv AS p
WHERE p.anchorUuid = :personUuid OR p.holderUuid = :personUuid
""", nativeQuery = true)
List<HsOfficeRelationRbacEntity> findRelationRelatedToPersonUuid(@NotNull UUID personUuid);
/**
* Finds relations by a conjunction of optional criteria, including anchorPerson, holderPerson and contact data.
* *
* @param personUuid the optional UUID of the anchorPerson or holderPerson
* @param relationType the type of the relation
* @param personData a string to match the persons tradeName, familyName or givenName (use '%' for wildcard), case ignored
* @param contactData a string to match the contacts caption, postalAddress, emailAddresses or phoneNumbers (use '%' for wildcard), case ignored
* @return a list of (accessible) relations which match all given criteria
*/
default List<HsOfficeRelationRbacEntity> findRelationRelatedToPersonUuidRelationTypePersonAndContactData(
final UUID personUuid,
final HsOfficeRelationType relationType,
final String personData,
final String contactData) {
return findRelationRelatedToPersonUuidRelationTypePersonAndContactDataImpl(
personUuid, toStringOrNull(relationType), toSqlLikeOperand(personData), toSqlLikeOperand(contactData));
} }
@Query(value = """ @Query(value = """
SELECT p.* FROM hs_office_relation_rv AS p SELECT rel FROM HsOfficeRelationRbacEntity AS rel
WHERE p.anchorUuid = :personUuid OR p.holderUuid = :personUuid WHERE (:relationType IS NULL OR CAST(rel.type AS String) = :relationType)
""", nativeQuery = true) AND ( :personUuid IS NULL
List<HsOfficeRelationRbacEntity> findRelationRelatedToPersonUuid(@NotNull UUID personUuid); OR rel.anchor.uuid = :personUuid OR rel.holder.uuid = :personUuid )
AND ( :personData IS NULL
@Query(value = """ OR lower(rel.anchor.tradeName) LIKE :personData OR lower(rel.holder.tradeName) LIKE :personData
SELECT p.* FROM hs_office_relation_rv AS p OR lower(rel.anchor.familyName) LIKE :personData OR lower(rel.holder.familyName) LIKE :personData
WHERE (:relationType IS NULL OR p.type = cast(:relationType AS HsOfficeRelationType)) OR lower(rel.anchor.givenName) LIKE :personData OR lower(rel.holder.givenName) LIKE :personData )
AND ( p.anchorUuid = :personUuid OR p.holderUuid = :personUuid) AND ( :contactData IS NULL
""", nativeQuery = true) OR lower(rel.contact.caption) LIKE :contactData
List<HsOfficeRelationRbacEntity> findRelationRelatedToPersonUuidAndRelationTypeString(@NotNull UUID personUuid, String relationType); OR lower(CAST(rel.contact.postalAddress AS String)) LIKE :contactData
OR lower(CAST(rel.contact.emailAddresses AS String)) LIKE :contactData
OR lower(CAST(rel.contact.phoneNumbers AS String)) LIKE :contactData )
""")
List<HsOfficeRelationRbacEntity> findRelationRelatedToPersonUuidRelationTypePersonAndContactDataImpl(
final UUID personUuid,
final String relationType,
final String personData,
final String contactData);
HsOfficeRelationRbacEntity save(final HsOfficeRelationRbacEntity entity); HsOfficeRelationRbacEntity save(final HsOfficeRelationRbacEntity entity);
long count(); long count();
int deleteByUuid(UUID uuid); int deleteByUuid(UUID uuid);
private static String toSqlLikeOperand(final String text) {
return text == null ? null : ("%" + text.toLowerCase() + "%");
}
private static String toStringOrNull(final HsOfficeRelationType relationType) {
return relationType == null ? null : relationType.name();
}
} }

View File

@ -11,7 +11,7 @@ import jakarta.persistence.Table;
@Entity @Entity
@Table(name = "hs_office_relation") @Table(schema = "hs_office", name = "relation")
@NoArgsConstructor @NoArgsConstructor
@Getter @Getter
@Setter @Setter

View File

@ -13,18 +13,18 @@ public interface HsOfficeRelationRealRepository extends Repository<HsOfficeRelat
Optional<HsOfficeRelationRealEntity> findByUuid(UUID id); Optional<HsOfficeRelationRealEntity> findByUuid(UUID id);
default List<HsOfficeRelationRealEntity> findRelationRelatedToPersonUuidAndRelationType(@NotNull UUID personUuid, HsOfficeRelationType relationType) { default List<HsOfficeRelationRealEntity> findRelationRelatedToPersonUuidAndRelationType(@NotNull UUID personUuid, HsOfficeRelationType relationType) {
return findRelationRelatedToPersonUuidAndRelationTypeString(personUuid, relationType.toString()); return findRelationRelatedToPersonUuidAndRelationTypeString(personUuid, relationType == null ? null : relationType.toString());
} }
@Query(value = """ @Query(value = """
SELECT p.* FROM hs_office_relation AS p SELECT p.* FROM hs_office.relation AS p
WHERE p.anchorUuid = :personUuid OR p.holderUuid = :personUuid WHERE p.anchorUuid = :personUuid OR p.holderUuid = :personUuid
""", nativeQuery = true) """, nativeQuery = true)
List<HsOfficeRelationRealEntity> findRelationRelatedToPersonUuid(@NotNull UUID personUuid); List<HsOfficeRelationRealEntity> findRelationRelatedToPersonUuid(@NotNull UUID personUuid);
@Query(value = """ @Query(value = """
SELECT p.* FROM hs_office_relation AS p SELECT p.* FROM hs_office.relation AS p
WHERE (:relationType IS NULL OR p.type = cast(:relationType AS HsOfficeRelationType)) WHERE (:relationType IS NULL OR p.type = cast(:relationType AS hs_office.RelationType))
AND ( p.anchorUuid = :personUuid OR p.holderUuid = :personUuid) AND ( p.anchorUuid = :personUuid OR p.holderUuid = :personUuid)
""", nativeQuery = true) """, nativeQuery = true)
List<HsOfficeRelationRealEntity> findRelationRelatedToPersonUuidAndRelationTypeString(@NotNull UUID personUuid, String relationType); List<HsOfficeRelationRealEntity> findRelationRelatedToPersonUuidAndRelationTypeString(@NotNull UUID personUuid, String relationType);

View File

@ -5,7 +5,7 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeSepaMand
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandateInsertResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandateInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandatePatchResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandatePatchResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandateResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandateResource;
import net.hostsharing.hsadminng.mapper.Mapper; import net.hostsharing.hsadminng.mapper.StandardMapper;
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;
@ -28,7 +28,7 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
private Context context; private Context context;
@Autowired @Autowired
private Mapper mapper; private StandardMapper mapper;
@Autowired @Autowired
private HsOfficeSepaMandateRepository sepaMandateRepo; private HsOfficeSepaMandateRepository sepaMandateRepo;
@ -39,10 +39,10 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<List<HsOfficeSepaMandateResource>> listSepaMandatesByIban( public ResponseEntity<List<HsOfficeSepaMandateResource>> listSepaMandatesByIban(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final String iban) { final String iban) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var entities = sepaMandateRepo.findSepaMandateByOptionalIban(iban); final var entities = sepaMandateRepo.findSepaMandateByOptionalIban(iban);
@ -54,11 +54,11 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<HsOfficeSepaMandateResource> addSepaMandate( public ResponseEntity<HsOfficeSepaMandateResource> addSepaMandate(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final HsOfficeSepaMandateInsertResource body) { final HsOfficeSepaMandateInsertResource body) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var entityToSave = mapper.map(body, HsOfficeSepaMandateEntity.class, SEPA_MANDATE_RESOURCE_TO_ENTITY_POSTMAPPER); final var entityToSave = mapper.map(body, HsOfficeSepaMandateEntity.class, SEPA_MANDATE_RESOURCE_TO_ENTITY_POSTMAPPER);
@ -77,11 +77,11 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<HsOfficeSepaMandateResource> getSepaMandateByUuid( public ResponseEntity<HsOfficeSepaMandateResource> getSepaMandateByUuid(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID sepaMandateUuid) { final UUID sepaMandateUuid) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var result = sepaMandateRepo.findByUuid(sepaMandateUuid); final var result = sepaMandateRepo.findByUuid(sepaMandateUuid);
if (result.isEmpty()) { if (result.isEmpty()) {
@ -94,10 +94,10 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<Void> deleteSepaMandateByUuid( public ResponseEntity<Void> deleteSepaMandateByUuid(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID sepaMandateUuid) { final UUID sepaMandateUuid) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var result = sepaMandateRepo.deleteByUuid(sepaMandateUuid); final var result = sepaMandateRepo.deleteByUuid(sepaMandateUuid);
if (result == 0) { if (result == 0) {
@ -110,12 +110,12 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<HsOfficeSepaMandateResource> patchSepaMandate( public ResponseEntity<HsOfficeSepaMandateResource> patchSepaMandate(
final String currentUser, final String currentSubject,
final String assumedRoles, final String assumedRoles,
final UUID sepaMandateUuid, final UUID sepaMandateUuid,
final HsOfficeSepaMandatePatchResource body) { final HsOfficeSepaMandatePatchResource body) {
context.define(currentUser, assumedRoles); context.define(currentSubject, assumedRoles);
final var current = sepaMandateRepo.findByUuid(sepaMandateUuid).orElseThrow(); final var current = sepaMandateRepo.findByUuid(sepaMandateUuid).orElseThrow();

View File

@ -7,8 +7,8 @@ import net.hostsharing.hsadminng.errors.DisplayAs;
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.hs.office.relation.HsOfficeRelationRbacEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
import net.hostsharing.hsadminng.rbac.rbacobject.BaseEntity; import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.generator.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;
@ -20,20 +20,20 @@ import java.util.UUID;
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR; import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR;
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.generator.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingCase; import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL; import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR; import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.*; import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify; import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity @Entity
@Table(name = "hs_office_sepamandate_rv") @Table(schema = "hs_office", name = "sepamandate_rv")
@Getter @Getter
@Setter @Setter
@Builder @Builder
@ -104,8 +104,8 @@ public class HsOfficeSepaMandateEntity implements Stringifyable, BaseEntity<HsOf
return rbacViewFor("sepaMandate", HsOfficeSepaMandateEntity.class) return rbacViewFor("sepaMandate", HsOfficeSepaMandateEntity.class)
.withIdentityView(query(""" .withIdentityView(query("""
select sm.uuid as uuid, ba.iban || '-' || sm.validity as idName select sm.uuid as uuid, ba.iban || '-' || sm.validity as idName
from hs_office_sepamandate sm from hs_office.sepamandate sm
join hs_office_bankaccount ba on ba.uuid = sm.bankAccountUuid join hs_office.bankaccount ba on ba.uuid = sm.bankAccountUuid
""")) """))
.withRestrictedViewOrderBy(expression("validity")) .withRestrictedViewOrderBy(expression("validity"))
.withUpdatableColumns("reference", "agreement", "validity") .withUpdatableColumns("reference", "agreement", "validity")
@ -114,8 +114,8 @@ public class HsOfficeSepaMandateEntity implements Stringifyable, BaseEntity<HsOf
dependsOnColumn("debitorUuid"), dependsOnColumn("debitorUuid"),
fetchedBySql(""" fetchedBySql("""
SELECT ${columns} SELECT ${columns}
FROM hs_office_relation debitorRel FROM hs_office.relation debitorRel
JOIN hs_office_debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid JOIN hs_office.debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid
WHERE debitor.uuid = ${REF}.debitorUuid WHERE debitor.uuid = ${REF}.debitorUuid
"""), """),
NOT_NULL) NOT_NULL)

View File

@ -130,7 +130,7 @@ public abstract class HsEntityValidator<E extends PropertiesProvider> {
} }
public Map<String, Object> revampProperties(final EntityManager em, final E entity, final Map<String, Object> config) { public Map<String, Object> revampProperties(final EntityManager em, final E entity, final Map<String, Object> config) {
final var copy = new HashMap<>(config); final var copy = config != null ? new HashMap<>(config) : new HashMap();
stream(propertyValidators).forEach(p -> { stream(propertyValidators).forEach(p -> {
if (p.isWriteOnly()) { if (p.isWriteOnly()) {
copy.remove(p.propertyName); copy.remove(p.propertyName);

View File

@ -56,6 +56,10 @@ public class IntegerProperty<P extends IntegerProperty<P>> extends ValidatablePr
return unit; return unit;
} }
public Integer min() {
return min;
}
public Integer max() { public Integer max() {
return max; return max;
} }

View File

@ -31,7 +31,7 @@ public class PasswordProperty extends StringProperty<PasswordProperty> {
@Override @Override
protected void validate(final List<String> result, final String propValue, final PropertiesProvider propProvider) { protected void validate(final List<String> result, final String propValue, final PropertiesProvider propProvider) {
// TODO.impl: remove after legacy data is migrated // TODO.legacy: remove after legacy data is migrated
if (HashGenerator.using(hashedUsing).couldBeHash(propValue) && propValue.length() > this.maxLength()) { if (HashGenerator.using(hashedUsing).couldBeHash(propValue) && propValue.length() > this.maxLength()) {
// already hashed => do not validate // already hashed => do not validate
return; return;

View File

@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.validation;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Setter; import lombok.Setter;
import net.hostsharing.hsadminng.mapper.Array; import net.hostsharing.hsadminng.mapper.Array;
import org.apache.commons.lang3.ArrayUtils;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -83,11 +84,15 @@ public class StringProperty<P extends StringProperty<P>> extends ValidatableProp
} }
/// predefined values, similar to fixed values in a combobox /// predefined values, similar to fixed values in a combobox
public P provided(final String... provided) { public P provided(final String firstProvidedValue, final String... moreProvidedValues) {
this.provided = provided; this.provided = ArrayUtils.addAll(new String[]{firstProvidedValue}, moreProvidedValues);
return self(); return self();
} }
public String[] provided() {
return this.provided;
}
/** /**
* The property value is not disclosed in error messages. * The property value is not disclosed in error messages.
* *
@ -109,7 +114,11 @@ public class StringProperty<P extends StringProperty<P>> extends ValidatableProp
@Override @Override
protected String display(final String propValue) { protected String display(final String propValue) {
return undisclosed ? "provided value" : ("'" + propValue + "'"); return undisclosed
? "provided value"
: propValue != null
? ("'" + propValue + "'")
: null;
} }
@Override @Override

View File

@ -0,0 +1,11 @@
package net.hostsharing.hsadminng.lambda;
import lombok.experimental.UtilityClass;
@UtilityClass
public class Reducer {
public static <T> T toSingleElement(T ignoredLast, T ignoredNext) {
throw new AssertionError("only a single entity expected");
}
}

View File

@ -1,28 +1,30 @@
package net.hostsharing.hsadminng.mapper; package net.hostsharing.hsadminng.mapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import org.modelmapper.ModelMapper; import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
import jakarta.persistence.EntityManager;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.PersistenceContext;
import jakarta.validation.ValidationException; import jakarta.validation.ValidationException;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.List; import java.util.List;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.Arrays.stream;
import static net.hostsharing.hsadminng.errors.DisplayAs.DisplayName; import static net.hostsharing.hsadminng.errors.DisplayAs.DisplayName;
/** /**
* A nicer API for ModelMapper. * A nicer API for ModelMapper.
*/ */
public class Mapper extends ModelMapper { abstract class Mapper extends ModelMapper {
@PersistenceContext EntityManagerWrapper em;
EntityManager em;
public Mapper() { Mapper(@Autowired final EntityManagerWrapper em) {
this.em = em;
getConfiguration().setAmbiguityIgnored(true); getConfiguration().setAmbiguityIgnored(true);
} }
@ -45,8 +47,12 @@ public class Mapper extends ModelMapper {
@Override @Override
public <D> D map(final Object source, final Class<D> destinationType) { public <D> D map(final Object source, final Class<D> destinationType) {
return map("", source, destinationType);
}
public <D> D map(final String namePrefix, final Object source, final Class<D> destinationType) {
final var target = super.map(source, destinationType); final var target = super.map(source, destinationType);
for (Field f : destinationType.getDeclaredFields()) { for (Field f : getDeclaredFieldsIncludingSuperClasses(destinationType)) {
if (f.getAnnotation(ManyToOne.class) == null) { if (f.getAnnotation(ManyToOne.class) == null) {
continue; continue;
} }
@ -64,18 +70,30 @@ public class Mapper extends ModelMapper {
if (subEntityUuid == null) { if (subEntityUuid == null) {
continue; continue;
} }
ReflectionUtils.setField(f, target, findEntityById(f.getType(), subEntityUuid)); ReflectionUtils.setField(f, target, fetchEntity(namePrefix + f.getName() + ".uuid", f.getType(), subEntityUuid));
} }
return target; return target;
} }
private Object findEntityById(final Class<?> entityClass, final Object subEntityUuid) { private static <D> Field[] getDeclaredFieldsIncludingSuperClasses(final Class<D> destinationType) {
// using getReference would be more efficent, but results in very technical error messages if (destinationType == null) {
final var entity = em.find(entityClass, subEntityUuid); return new Field[0];
}
return Stream.concat(
stream(destinationType.getDeclaredFields()),
stream(getDeclaredFieldsIncludingSuperClasses(destinationType.getSuperclass())))
.toArray(Field[]::new);
}
public <E> E fetchEntity(final String propertyName, final Class<E> entityClass, final Object subEntityUuid) {
final var entity = em.getReference(entityClass, subEntityUuid);
if (entity != null) { if (entity != null) {
return entity; return entity;
} }
throw new ValidationException("Unable to find " + DisplayName.of(entityClass) + " by uuid: " + subEntityUuid); throw new ValidationException(
"Unable to find " + DisplayName.of(entityClass) +
" by " + propertyName + ": " + subEntityUuid);
} }
public <S, T> T map(final S source, final Class<T> targetClass, final BiConsumer<S, T> postMapper) { public <S, T> T map(final S source, final Class<T> targetClass, final BiConsumer<S, T> postMapper) {
@ -86,4 +104,13 @@ public class Mapper extends ModelMapper {
postMapper.accept(source, target); postMapper.accept(source, target);
return target; return target;
} }
public <S, T> T map(final String namePrefix, final S source, final Class<T> targetClass, final BiConsumer<S, T> postMapper) {
if (source == null) {
return null;
}
final var target = map(source, targetClass);
postMapper.accept(source, target);
return target;
}
} }

View File

@ -1,13 +0,0 @@
package net.hostsharing.hsadminng.mapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MapperConfiguration {
@Bean
public Mapper modelMapper() {
return new Mapper();
}
}

View File

@ -0,0 +1,17 @@
package net.hostsharing.hsadminng.mapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* A nicer API for ModelMapper in standard mode.
*/
@Component
public class StandardMapper extends Mapper {
public StandardMapper(@Autowired final EntityManagerWrapper em) {
super(em);
getConfiguration().setAmbiguityIgnored(true);
}
}

View File

@ -0,0 +1,22 @@
package net.hostsharing.hsadminng.mapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import static org.modelmapper.convention.MatchingStrategies.STRICT;
/**
* A nicer API for ModelMapper in strict mode.
*
* <p>This makes sure that resource.whateverUuid does not accidentally get mapped to entity.uuid,
* if resource.uuid does not exist.</p>
*/
@Component
public class StrictMapper extends Mapper {
public StrictMapper(@Autowired final EntityManagerWrapper em) {
super(em);
getConfiguration().setMatchingStrategy(STRICT);
}
}

View File

@ -0,0 +1,44 @@
package net.hostsharing.hsadminng.mapper;
import java.util.*;
import static java.util.stream.Collectors.joining;
public class ToStringConverter {
final public Set<String> ignoredFields = new HashSet<>();
public ToStringConverter ignoring(final String fieldName) {
ignoredFields.add(fieldName);
return this;
}
public String from(final Object obj) {
return "{ " +
Arrays.stream(obj.getClass().getDeclaredFields())
.filter(f -> !ignoredFields.contains(f.getName()))
.map(field -> {
try {
field.setAccessible(true);
return field.getName() + ": " + field.get(obj);
} catch (IllegalAccessException e) {
// ignore inaccessible fields
return null;
}
})
.filter(Objects::nonNull)
.collect(joining(", "))
+ " }";
}
public String from(final Map<?, ?> map) {
return "{ "
+ map.keySet().stream()
.filter(key -> !ignoredFields.contains(key.toString()))
.sorted()
.map(k -> Map.entry(k, map.get(k)))
.map(e -> e.getKey() + ": " + e.getValue())
.collect(joining(", "))
+ " }";
}
}

View File

@ -1,11 +1,10 @@
package net.hostsharing.hsadminng.rbac.rbacobject; package net.hostsharing.hsadminng.persistence;
import org.hibernate.Hibernate; import org.hibernate.Hibernate;
import java.util.UUID; import java.util.UUID;
// TODO.impl: this class does not really belong into this package, but there is no right place yet
public interface BaseEntity<T extends BaseEntity<?>> { public interface BaseEntity<T extends BaseEntity<?>> {
UUID getUuid(); UUID getUuid();

View File

@ -0,0 +1,38 @@
package net.hostsharing.hsadminng.persistence;
import net.hostsharing.hsadminng.errors.DisplayAs.DisplayName;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import jakarta.persistence.Entity;
import jakarta.validation.ValidationException;
@Service
public class EntityExistsValidator {
@Autowired
private EntityManagerWrapper em;
public <T extends BaseEntity<T>> void validateEntityExists(final String property, final T entitySkeleton) {
final var foundEntity = em.find(entityClass(entitySkeleton), entitySkeleton.getUuid());
if ( foundEntity == null) {
throw new ValidationException("Unable to find " + DisplayName.of(entitySkeleton) + " by " + property + ": " + entitySkeleton.getUuid());
}
}
private static <T extends BaseEntity<T>> Class<?> entityClass(final T entityOrProxy) {
final var entityClass = entityClass(entityOrProxy.getClass());
if (entityClass == null) {
throw new IllegalArgumentException("@Entity not found in superclass hierarchy of " + entityOrProxy.getClass());
}
return entityClass;
}
private static Class<?> entityClass(final Class<?> entityOrProxyClass) {
return entityOrProxyClass.isAnnotationPresent(Entity.class)
? entityOrProxyClass
: entityOrProxyClass.getSuperclass() == null
? null
: entityClass(entityOrProxyClass.getSuperclass());
}
}

View File

@ -1,4 +1,4 @@
package net.hostsharing.hsadminng.rbac.rbacdef; package net.hostsharing.hsadminng.rbac.generator;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
@ -6,12 +6,12 @@ import java.util.function.BinaryOperator;
import java.util.stream.Stream; import java.util.stream.Stream;
import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.joining;
import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.NEW; import static net.hostsharing.hsadminng.rbac.generator.PostgresTriggerReference.NEW;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.PERM_TO_ROLE; import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacGrantDefinition.GrantType.PERM_TO_ROLE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.ADMIN; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.GUEST; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.GUEST;
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with; import static net.hostsharing.hsadminng.rbac.generator.StringWriter.with;
import static org.apache.commons.lang3.StringUtils.capitalize; import static org.apache.commons.lang3.StringUtils.capitalize;
import static org.apache.commons.lang3.StringUtils.uncapitalize; import static org.apache.commons.lang3.StringUtils.uncapitalize;
@ -46,7 +46,7 @@ public class InsertTriggerGenerator {
private void generateInsertPermissionGrants(final StringWriter plPgSql) { private void generateInsertPermissionGrants(final StringWriter plPgSql) {
plPgSql.writeLn(""" plPgSql.writeLn("""
-- ============================================================================ -- ============================================================================
--changeset ${liquibaseTagPrefix}-rbac-GRANTING-INSERT-PERMISSION:1 endDelimiter:--// --changeset InsertTriggerGenerator:${liquibaseTagPrefix}-rbac-GRANTING-INSERT-PERMISSION endDelimiter:--//
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
""", """,
with("liquibaseTagPrefix", liquibaseTagPrefix)); with("liquibaseTagPrefix", liquibaseTagPrefix));
@ -55,7 +55,7 @@ public class InsertTriggerGenerator {
plPgSql.writeLn(""" plPgSql.writeLn("""
-- granting INSERT permission to ${rawSubTable} ---------------------------- -- granting INSERT permission to ${rawSubTable} ----------------------------
""", """,
with("rawSubTable", g.getSuperRoleDef().getEntityAlias().getRawTableName())); with("rawSubTable", g.getSuperRoleDef().getEntityAlias().getRawTableNameWithSchema()));
if (isGrantToADifferentTable(g)) { if (isGrantToADifferentTable(g)) {
plPgSql.writeLn( plPgSql.writeLn(
@ -67,13 +67,13 @@ public class InsertTriggerGenerator {
declare declare
row ${rawSuperTable}; row ${rawSuperTable};
begin begin
call defineContext('create INSERT INTO ${rawSubTable} permissions for pre-exising ${rawSuperTable} rows'); call base.defineContext('create INSERT INTO ${rawSubTable} permissions for pre-exising ${rawSuperTable} rows');
FOR row IN SELECT * FROM ${rawSuperTable} FOR row IN SELECT * FROM ${rawSuperTable}
${whenCondition} ${whenCondition}
LOOP LOOP
call grantPermissionToRole( call rbac.grantPermissionToRole(
createPermission(row.uuid, 'INSERT', '${rawSubTable}'), rbac.createPermission(row.uuid, 'INSERT', '${rawSubTable}'),
${superRoleRef}); ${superRoleRef});
END LOOP; END LOOP;
end; end;
@ -84,40 +84,40 @@ public class InsertTriggerGenerator {
? "WHERE type = '${value}'" ? "WHERE type = '${value}'"
.replace("${value}", g.getSuperRoleDef().getEntityAlias().usingCase().value) .replace("${value}", g.getSuperRoleDef().getEntityAlias().usingCase().value)
: "-- unconditional for all rows in that table"), : "-- unconditional for all rows in that table"),
with("rawSuperTable", g.getSuperRoleDef().getEntityAlias().getRawTableName()), with("rawSuperTable", g.getSuperRoleDef().getEntityAlias().getRawTableNameWithSchema()),
with("rawSubTable", g.getPermDef().getEntityAlias().getRawTableName()), with("rawSubTable", g.getPermDef().getEntityAlias().getRawTableNameWithSchema()),
with("superRoleRef", toRoleDescriptor(g.getSuperRoleDef(), "row"))); with("superRoleRef", toRoleDescriptor(g.getSuperRoleDef(), "row")));
} else { } else {
plPgSql.writeLn(""" plPgSql.writeLn("""
-- Granting INSERT INTO hs_hosting_asset permissions to specified role of pre-existing hs_hosting_asset rows slipped, -- Granting INSERT INTO hs_hosting.asset permissions to specified role of pre-existing hs_hosting.asset rows slipped,
-- because there cannot yet be any pre-existing rows in the same table yet. -- because there cannot yet be any pre-existing rows in the same table yet.
""", """,
with("rawSuperTable", g.getSuperRoleDef().getEntityAlias().getRawTableName()), with("rawSuperTable", g.getSuperRoleDef().getEntityAlias().getRawTableNameWithSchema()),
with("rawSubTable", g.getPermDef().getEntityAlias().getRawTableName())); with("rawSubTable", g.getPermDef().getEntityAlias().getRawTableNameWithSchema()));
} }
plPgSql.writeLn(""" plPgSql.writeLn("""
/** /**
Grants ${rawSubTable} INSERT permission to specified role of new ${rawSuperTable} rows. Grants ${rawSubTable} INSERT permission to specified role of new ${rawSuperTable} rows.
*/ */
create or replace function new_${rawSubTable}_grants_insert_to_${rawSuperTable}_tf() create or replace function ${rawSubTableSchemaPrefix}${rawSubTableShortName}_grants_insert_to_${rawSuperTableShortName}_tf()
returns trigger returns trigger
language plpgsql language plpgsql
strict as $$ strict as $$
begin begin
${ifConditionThen} ${ifConditionThen}
call grantPermissionToRole( call rbac.grantPermissionToRole(
createPermission(NEW.uuid, 'INSERT', '${rawSubTable}'), rbac.createPermission(NEW.uuid, 'INSERT', '${rawSubTable}'),
${superRoleRef}); ${superRoleRef});
${ifConditionEnd} ${ifConditionEnd}
return NEW; return NEW;
end; $$; end; $$;
-- z_... is to put it at the end of after insert triggers, to make sure the roles exist -- ..._z_... is to put it at the end of after insert triggers, to make sure the roles exist
create trigger z_new_${rawSubTable}_grants_insert_to_${rawSuperTable}_tg create trigger ${rawSubTableName}_z_grants_after_insert_tg
after insert on ${rawSuperTable} after insert on ${rawSuperTableWithSchema}
for each row for each row
execute procedure new_${rawSubTable}_grants_insert_to_${rawSuperTable}_tf(); execute procedure ${rawSubTableSchemaPrefix}${rawSubTableShortName}_grants_insert_to_${rawSuperTableShortName}_tf();
""", """,
with("ifConditionThen", g.getSuperRoleDef().getEntityAlias().isCaseDependent() with("ifConditionThen", g.getSuperRoleDef().getEntityAlias().isCaseDependent()
// TODO.impl: .type needs to be dynamically generated // TODO.impl: .type needs to be dynamically generated
@ -127,8 +127,13 @@ public class InsertTriggerGenerator {
? "end if;" ? "end if;"
: "-- end."), : "-- end."),
with("superRoleRef", toRoleDescriptor(g.getSuperRoleDef(), NEW.name())), with("superRoleRef", toRoleDescriptor(g.getSuperRoleDef(), NEW.name())),
with("rawSuperTableWithSchema", g.getSuperRoleDef().getEntityAlias().getRawTableNameWithSchema()),
with("rawSuperTableShortName", g.getSuperRoleDef().getEntityAlias().getRawTableShortName()),
with("rawSuperTable", g.getSuperRoleDef().getEntityAlias().getRawTableName()), with("rawSuperTable", g.getSuperRoleDef().getEntityAlias().getRawTableName()),
with("rawSubTable", g.getPermDef().getEntityAlias().getRawTableName())); with("rawSubTable", g.getPermDef().getEntityAlias().getRawTableNameWithSchema()),
with("rawSubTableSchemaPrefix", g.getPermDef().getEntityAlias().getRawTableSchemaPrefix()),
with("rawSubTableName", g.getPermDef().getEntityAlias().getRawTableName()),
with("rawSubTableShortName", g.getPermDef().getEntityAlias().getRawTableShortName()));
}); });
} }
@ -136,7 +141,7 @@ public class InsertTriggerGenerator {
private void generateInsertPermissionTriggerAlwaysDisallow(final StringWriter plPgSql) { private void generateInsertPermissionTriggerAlwaysDisallow(final StringWriter plPgSql) {
plPgSql.writeLn(""" plPgSql.writeLn("""
-- ============================================================================ -- ============================================================================
--changeset ${liquibaseTagPrefix}-rbac-ALWAYS-DISALLOW-INSERT:1 endDelimiter:--// --changeset InsertTriggerGenerator:${liquibaseTagPrefix}-rbac-ALWAYS-DISALLOW-INSERT endDelimiter:--//
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
""", """,
with("liquibaseTagPrefix", liquibaseTagPrefix)); with("liquibaseTagPrefix", liquibaseTagPrefix));
@ -150,14 +155,15 @@ public class InsertTriggerGenerator {
returns trigger returns trigger
language plpgsql as $$ language plpgsql as $$
begin begin
raise exception '[403] insert into ${rawSubTable} values(%) not allowed regardless of current subject, no insert permissions granted at all', NEW; raise exception '[403] insert into ${rawSubTableWithSchema} values(%) not allowed regardless of current subject, no insert permissions granted at all', NEW;
end; $$; end; $$;
create trigger ${rawSubTable}_insert_permission_check_tg create trigger ${rawSubTable}_insert_permission_check_tg
before insert on ${rawSubTable} before insert on ${rawSubTable}
for each row for each row
execute procedure ${rawSubTable}_insert_permission_missing_tf(); execute procedure ${rawSubTableWithSchema}_insert_permission_missing_tf();
""", """,
with("rawSubTableWithSchema", rbacDef.getRootEntityAlias().getRawTableNameWithSchema()),
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName())); with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()));
plPgSql.writeLn("--//"); plPgSql.writeLn("--//");
@ -179,7 +185,7 @@ public class InsertTriggerGenerator {
private void generateInsertPermissionsCheckHeader(final StringWriter plPgSql) { private void generateInsertPermissionsCheckHeader(final StringWriter plPgSql) {
plPgSql.writeLn(""" plPgSql.writeLn("""
-- ============================================================================ -- ============================================================================
--changeset ${rawSubTable}-rbac-CHECKING-INSERT-PERMISSION:1 endDelimiter:--// --changeset InsertTriggerGenerator:${liquibaseTagPrefix}-rbac-CHECKING-INSERT-PERMISSION endDelimiter:--//
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
/** /**
@ -192,7 +198,8 @@ public class InsertTriggerGenerator {
superObjectUuid uuid; superObjectUuid uuid;
begin begin
""", """,
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName())); with("liquibaseTagPrefix", liquibaseTagPrefix),
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableNameWithSchema()));
plPgSql.chopEmptyLines(); plPgSql.chopEmptyLines();
} }
@ -206,7 +213,7 @@ public class InsertTriggerGenerator {
if (g.getSuperRoleDef().isGlobal(GUEST)) { if (g.getSuperRoleDef().isGlobal(GUEST)) {
plPgSql.writeLn( plPgSql.writeLn(
""" """
-- check INSERT INSERT permission for global anyone -- check INSERT permission for rbac.global anyone
if ${caseCondition}true then if ${caseCondition}true then
return NEW; return NEW;
end if; end if;
@ -215,8 +222,8 @@ public class InsertTriggerGenerator {
} else if (g.getSuperRoleDef().isGlobal(ADMIN)) { } else if (g.getSuperRoleDef().isGlobal(ADMIN)) {
plPgSql.writeLn( plPgSql.writeLn(
""" """
-- check INSERT INSERT if global ADMIN -- check INSERT permission if rbac.global ADMIN
if ${caseCondition}isGlobalAdmin() then if ${caseCondition}rbac.isGlobalAdmin() then
return NEW; return NEW;
end if; end if;
""", """,
@ -225,25 +232,25 @@ public class InsertTriggerGenerator {
plPgSql.writeLn( plPgSql.writeLn(
""" """
-- check INSERT permission via direct foreign key: NEW.${refColumn} -- check INSERT permission via direct foreign key: NEW.${refColumn}
if ${caseCondition}hasInsertPermission(NEW.${refColumn}, '${rawSubTable}') then if ${caseCondition}rbac.hasInsertPermission(NEW.${refColumn}, '${rawSubTable}') then
return NEW; return NEW;
end if; end if;
""", """,
with("caseCondition", caseCondition), with("caseCondition", caseCondition),
with("refColumn", superRoleEntityAlias.dependsOnColumName()), with("refColumn", superRoleEntityAlias.dependsOnColumName()),
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName())); with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableNameWithSchema()));
} else { } else {
plPgSql.writeLn( plPgSql.writeLn(
""" """
-- check INSERT permission via indirect foreign key: NEW.${refColumn} -- check INSERT permission via indirect foreign key: NEW.${refColumn}
superObjectUuid := (${fetchSql}); superObjectUuid := (${fetchSql});
assert superObjectUuid is not null, 'object uuid fetched depending on ${rawSubTable}.${refColumn} must not be null, also check fetchSql in RBAC DSL'; assert superObjectUuid is not null, 'object uuid fetched depending on ${rawSubTable}.${refColumn} must not be null, also check fetchSql in RBAC DSL';
if ${caseCondition}hasInsertPermission(superObjectUuid, '${rawSubTable}') then if ${caseCondition}rbac.hasInsertPermission(superObjectUuid, '${rawSubTable}') then
return NEW; return NEW;
end if; end if;
""", """,
with("caseCondition", caseCondition), with("caseCondition", caseCondition),
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()), with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableNameWithSchema()),
with("refColumn", superRoleEntityAlias.dependsOnColumName()), with("refColumn", superRoleEntityAlias.dependsOnColumName()),
with("fetchSql", g.getSuperRoleDef().getEntityAlias().fetchSql().sql), with("fetchSql", g.getSuperRoleDef().getEntityAlias().fetchSql().sql),
with("columns", g.getSuperRoleDef().getEntityAlias().aliasName() + ".uuid"), with("columns", g.getSuperRoleDef().getEntityAlias().aliasName() + ".uuid"),
@ -254,16 +261,17 @@ public class InsertTriggerGenerator {
private void generateInsertPermissionsChecksFooter(final StringWriter plPgSql) { private void generateInsertPermissionsChecksFooter(final StringWriter plPgSql) {
plPgSql.writeLn(); plPgSql.writeLn();
plPgSql.writeLn(""" plPgSql.writeLn("""
raise exception '[403] insert into ${rawSubTable} values(%) not allowed for current subjects % (%)', raise exception '[403] insert into ${rawSubTableWithSchema} values(%) not allowed for current subjects % (%)',
NEW, currentSubjects(), currentSubjectsUuids(); NEW, base.currentSubjects(), rbac.currentSubjectOrAssumedRolesUuids();
end; $$; end; $$;
create trigger ${rawSubTable}_insert_permission_check_tg create trigger ${rawSubTable}_insert_permission_check_tg
before insert on ${rawSubTable} before insert on ${rawSubTableWithSchema}
for each row for each row
execute procedure ${rawSubTable}_insert_permission_check_tf(); execute procedure ${rawSubTableWithSchema}_insert_permission_check_tf();
--// --//
""", """,
with("rawSubTableWithSchema", rbacDef.getRootEntityAlias().getRawTableNameWithSchema()),
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName())); with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()));
} }
@ -272,7 +280,7 @@ public class InsertTriggerGenerator {
} }
private boolean isGrantToADifferentTable(final RbacView.RbacGrantDefinition g) { private boolean isGrantToADifferentTable(final RbacView.RbacGrantDefinition g) {
return !rbacDef.getRootEntityAlias().getRawTableName().equals(g.getSuperRoleDef().getEntityAlias().getRawTableName()); return !rbacDef.getRootEntityAlias().getRawTableNameWithSchema().equals(g.getSuperRoleDef().getEntityAlias().getRawTableNameWithSchema());
} }
private Stream<RbacView.RbacGrantDefinition> getInsertGrants() { private Stream<RbacView.RbacGrantDefinition> getInsertGrants() {
@ -317,7 +325,7 @@ public class InsertTriggerGenerator {
private String toRoleDescriptor(final RbacView.RbacRoleDefinition roleDef, final String ref) { private String toRoleDescriptor(final RbacView.RbacRoleDefinition roleDef, final String ref) {
final var functionName = toVar(roleDef); final var functionName = roleDef.descriptorFunctionName();
if (roleDef.getEntityAlias().isGlobal()) { if (roleDef.getEntityAlias().isGlobal()) {
return functionName + "()"; return functionName + "()";
} }

View File

@ -1,4 +1,4 @@
package net.hostsharing.hsadminng.rbac.rbacdef; package net.hostsharing.hsadminng.rbac.generator;
public enum PostgresTriggerReference { public enum PostgresTriggerReference {
NEW, OLD NEW, OLD

View File

@ -1,6 +1,6 @@
package net.hostsharing.hsadminng.rbac.rbacdef; package net.hostsharing.hsadminng.rbac.generator;
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with; import static net.hostsharing.hsadminng.rbac.generator.StringWriter.with;
public class RbacIdentityViewGenerator { public class RbacIdentityViewGenerator {
private final RbacView rbacDef; private final RbacView rbacDef;
@ -12,13 +12,13 @@ public class RbacIdentityViewGenerator {
this.rbacDef = rbacDef; this.rbacDef = rbacDef;
this.liquibaseTagPrefix = liquibaseTagPrefix; this.liquibaseTagPrefix = liquibaseTagPrefix;
this.simpleEntityVarName = rbacDef.getRootEntityAlias().simpleName(); this.simpleEntityVarName = rbacDef.getRootEntityAlias().simpleName();
this.rawTableName = rbacDef.getRootEntityAlias().getRawTableName(); this.rawTableName = rbacDef.getRootEntityAlias().getRawTableNameWithSchema();
} }
void generateTo(final StringWriter plPgSql) { void generateTo(final StringWriter plPgSql) {
plPgSql.writeLn(""" plPgSql.writeLn("""
-- ============================================================================ -- ============================================================================
--changeset ${liquibaseTagPrefix}-rbac-IDENTITY-VIEW:1 endDelimiter:--// --changeset RbacIdentityViewGenerator:${liquibaseTagPrefix}-rbac-IDENTITY-VIEW endDelimiter:--//
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
""", """,
with("liquibaseTagPrefix", liquibaseTagPrefix)); with("liquibaseTagPrefix", liquibaseTagPrefix));
@ -26,13 +26,13 @@ public class RbacIdentityViewGenerator {
plPgSql.writeLn( plPgSql.writeLn(
switch (rbacDef.getIdentityViewSqlQuery().part) { switch (rbacDef.getIdentityViewSqlQuery().part) {
case SQL_PROJECTION -> """ case SQL_PROJECTION -> """
call generateRbacIdentityViewFromProjection('${rawTableName}', call rbac.generateRbacIdentityViewFromProjection('${rawTableName}',
$idName$ $idName$
${identityViewSqlPart} ${identityViewSqlPart}
$idName$); $idName$);
"""; """;
case SQL_QUERY -> """ case SQL_QUERY -> """
call generateRbacIdentityViewFromQuery('${rawTableName}', call rbac.generateRbacIdentityViewFromQuery('${rawTableName}',
$idName$ $idName$
${identityViewSqlPart} ${identityViewSqlPart}
$idName$); $idName$);

View File

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

View File

@ -1,9 +1,9 @@
package net.hostsharing.hsadminng.rbac.rbacdef; package net.hostsharing.hsadminng.rbac.generator;
import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.joining;
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.indented; import static net.hostsharing.hsadminng.rbac.generator.StringWriter.indented;
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with; import static net.hostsharing.hsadminng.rbac.generator.StringWriter.with;
public class RbacRestrictedViewGenerator { public class RbacRestrictedViewGenerator {
private final RbacView rbacDef; private final RbacView rbacDef;
@ -13,15 +13,15 @@ public class RbacRestrictedViewGenerator {
public RbacRestrictedViewGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) { public RbacRestrictedViewGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) {
this.rbacDef = rbacDef; this.rbacDef = rbacDef;
this.liquibaseTagPrefix = liquibaseTagPrefix; this.liquibaseTagPrefix = liquibaseTagPrefix;
this.rawTableName = rbacDef.getRootEntityAlias().getRawTableName(); this.rawTableName = rbacDef.getRootEntityAlias().getRawTableNameWithSchema();
} }
void generateTo(final StringWriter plPgSql) { void generateTo(final StringWriter plPgSql) {
plPgSql.writeLn(""" plPgSql.writeLn("""
-- ============================================================================ -- ============================================================================
--changeset ${liquibaseTagPrefix}-rbac-RESTRICTED-VIEW:1 endDelimiter:--// --changeset RbacRestrictedViewGenerator:${liquibaseTagPrefix}-rbac-RESTRICTED-VIEW endDelimiter:--//
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
call generateRbacRestrictedView('${rawTableName}', call rbac.generateRbacRestrictedView('${rawTableName}',
$orderBy$ $orderBy$
${orderBy} ${orderBy}
$orderBy$, $orderBy$,

View File

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

View File

@ -1,8 +1,8 @@
package net.hostsharing.hsadminng.rbac.rbacdef; package net.hostsharing.hsadminng.rbac.generator;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import net.hostsharing.hsadminng.rbac.rbacobject.BaseEntity; import net.hostsharing.hsadminng.persistence.BaseEntity;
import org.reflections.Reflections; import org.reflections.Reflections;
import org.reflections.scanners.TypeAnnotationsScanner; import org.reflections.scanners.TypeAnnotationsScanner;
@ -12,7 +12,6 @@ import jakarta.persistence.Version;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.*;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -23,25 +22,26 @@ import static java.util.Arrays.asList;
import static java.util.Arrays.stream; import static java.util.Arrays.stream;
import static java.util.Collections.max; import static java.util.Collections.max;
import static java.util.Optional.ofNullable; import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL; import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.PERM_TO_ROLE; import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacGrantDefinition.GrantType.PERM_TO_ROLE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.ROLE_TO_ROLE; import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacGrantDefinition.GrantType.ROLE_TO_ROLE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR; import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.Part.AUTO_FETCH; import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.Part.AUTO_FETCH;
import static org.apache.commons.collections4.SetUtils.hashSet; import static org.apache.commons.collections4.SetUtils.hashSet;
import static org.apache.commons.lang3.StringUtils.capitalize;
import static org.apache.commons.lang3.StringUtils.uncapitalize; import static org.apache.commons.lang3.StringUtils.uncapitalize;
@Getter @Getter
// TODO.refa: rename to RbacDSL // TODO.refa: rename to RbacDSL
public class RbacView { public class RbacView {
public static final String GLOBAL = "global"; public static final String GLOBAL = "rbac.global";
public static final String OUTPUT_BASEDIR = "src/main/resources/db/changelog"; public static final String OUTPUT_BASEDIR = "src/main/resources/db/changelog";
private final EntityAlias rootEntityAlias; private final EntityAlias rootEntityAlias;
private final Set<RbacUserReference> userDefs = new LinkedHashSet<>(); private final Set<RbacSubjectReference> userDefs = new LinkedHashSet<>();
private final Set<RbacRoleDefinition> roleDefs = new LinkedHashSet<>(); private final Set<RbacRoleDefinition> roleDefs = new LinkedHashSet<>();
private final Set<RbacPermissionDefinition> permDefs = new LinkedHashSet<>(); private final Set<RbacPermissionDefinition> permDefs = new LinkedHashSet<>();
private final Map<String, EntityAlias> entityAliases = new HashMap<>() { private final Map<String, EntityAlias> entityAliases = new HashMap<>() {
@ -90,15 +90,15 @@ public class RbacView {
* @param <E> * @param <E>
* a JPA entity class extending RbacObject * a JPA entity class extending RbacObject
*/ */
public static <E extends BaseEntity> RbacView rbacViewFor(final String alias, final Class<E> entityClass) { public static <E extends BaseEntity<?>> RbacView rbacViewFor(final String alias, final Class<E> entityClass) {
return new RbacView(alias, entityClass); return new RbacView(alias, entityClass);
} }
RbacView(final String alias, final Class<? extends BaseEntity> entityClass) { RbacView(final String alias, final Class<? extends BaseEntity<?>> entityClass) {
rootEntityAlias = new EntityAlias(alias, entityClass); rootEntityAlias = new EntityAlias(alias, entityClass);
entityAliases.put(alias, rootEntityAlias); entityAliases.put(alias, rootEntityAlias);
new RbacUserReference(CREATOR); new RbacSubjectReference(CREATOR);
entityAliases.put("global", new EntityAlias("global")); entityAliases.put("rbac.global", new EntityAlias("rbac.global"));
} }
/** /**
@ -121,7 +121,7 @@ public class RbacView {
* <p>An identity view is a view which maps an objectUuid to an idName. * <p>An identity view is a view which maps an objectUuid to an idName.
* The idName should be a human-readable representation of the row, but as short as possible. * The idName should be a human-readable representation of the row, but as short as possible.
* The idName must only consist of letters (A-Z, a-z), digits (0-9), dash (-), dot (.) and unserscore '_'. * The idName must only consist of letters (A-Z, a-z), digits (0-9), dash (-), dot (.) and unserscore '_'.
* It's used to create the object-specific-role-names like test_customer#abc:ADMIN - here 'abc' is the idName. * It's used to create the object-specific-role-names like rbactest.customer#abc:ADMIN - here 'abc' is the idName.
* The idName not necessarily unique in a table, but it should be avoided. * The idName not necessarily unique in a table, but it should be avoided.
* </p> * </p>
* *
@ -287,9 +287,9 @@ public class RbacView {
* @param <EC> * @param <EC>
* a JPA entity class extending RbacObject * a JPA entity class extending RbacObject
*/ */
public <EC extends BaseEntity> RbacView importRootEntityAliasProxy( public <EC extends BaseEntity<?>> RbacView importRootEntityAliasProxy(
final String aliasName, final String aliasName,
final Class<? extends BaseEntity> entityClass, final Class<? extends BaseEntity<?>> entityClass,
final ColumnValue forCase, final ColumnValue forCase,
final SQL fetchSql, final SQL fetchSql,
final Column dependsOnColum) { final Column dependsOnColum) {
@ -313,7 +313,7 @@ public class RbacView {
* a JPA entity class extending RbacObject * a JPA entity class extending RbacObject
*/ */
public RbacView importSubEntityAlias( public RbacView importSubEntityAlias(
final String aliasName, final Class<? extends BaseEntity> entityClass, final String aliasName, final Class<? extends BaseEntity<?>> entityClass,
final SQL fetchSql, final Column dependsOnColum) { final SQL fetchSql, final Column dependsOnColum) {
importEntityAliasImpl(aliasName, entityClass, usingDefaultCase(), fetchSql, dependsOnColum, true, NOT_NULL); importEntityAliasImpl(aliasName, entityClass, usingDefaultCase(), fetchSql, dependsOnColum, true, NOT_NULL);
return this; return this;
@ -350,14 +350,14 @@ public class RbacView {
* a JPA entity class extending RbacObject * a JPA entity class extending RbacObject
*/ */
public RbacView importEntityAlias( public RbacView importEntityAlias(
final String aliasName, final Class<? extends BaseEntity> entityClass, final ColumnValue usingCase, final String aliasName, final Class<? extends BaseEntity<?>> entityClass, final ColumnValue usingCase,
final Column dependsOnColum, final SQL fetchSql, final Nullable nullable) { final Column dependsOnColum, final SQL fetchSql, final Nullable nullable) {
importEntityAliasImpl(aliasName, entityClass, usingCase, fetchSql, dependsOnColum, false, nullable); importEntityAliasImpl(aliasName, entityClass, usingCase, fetchSql, dependsOnColum, false, nullable);
return this; return this;
} }
private EntityAlias importEntityAliasImpl( private EntityAlias importEntityAliasImpl(
final String aliasName, final Class<? extends BaseEntity> entityClass, final ColumnValue usingCase, final String aliasName, final Class<? extends BaseEntity<?>> entityClass, final ColumnValue usingCase,
final SQL fetchSql, final Column dependsOnColum, boolean asSubEntity, final Nullable nullable) { final SQL fetchSql, final Column dependsOnColum, boolean asSubEntity, final Nullable nullable) {
final var entityAlias = ofNullable(entityAliases.get(aliasName)) final var entityAlias = ofNullable(entityAliases.get(aliasName))
@ -467,7 +467,7 @@ public class RbacView {
return new RbacExampleRole(entityAlias, role); return new RbacExampleRole(entityAlias, role);
} }
private RbacGrantDefinition grantRoleToUser(final RbacRoleDefinition roleDefinition, final RbacUserReference user) { private RbacGrantDefinition grantRoleToSubject(final RbacRoleDefinition roleDefinition, final RbacSubjectReference user) {
return findOrCreateGrantDef(roleDefinition, user).toCreate(); return findOrCreateGrantDef(roleDefinition, user).toCreate();
} }
@ -548,7 +548,7 @@ public class RbacView {
} }
public RbacView grantPermission(final Permission perm) { public RbacView grantPermission(final Permission perm) {
final var forTable = rootEntityAlias.getRawTableName(); final var forTable = rootEntityAlias.getRawTableNameWithSchema();
findOrCreateGrantDef(findRbacPerm(rootEntityAlias, perm, forTable), superRoleDef).toCreate(); findOrCreateGrantDef(findRbacPerm(rootEntityAlias, perm, forTable), superRoleDef).toCreate();
return RbacView.this; return RbacView.this;
} }
@ -564,7 +564,7 @@ public class RbacView {
@EqualsAndHashCode @EqualsAndHashCode
public class RbacGrantDefinition { public class RbacGrantDefinition {
private final RbacUserReference userDef; private final RbacSubjectReference userDef;
private final RbacRoleDefinition superRoleDef; private final RbacRoleDefinition superRoleDef;
private final RbacRoleDefinition subRoleDef; private final RbacRoleDefinition subRoleDef;
private final RbacPermissionDefinition permDef; private final RbacPermissionDefinition permDef;
@ -605,7 +605,7 @@ public class RbacView {
register(this); register(this);
} }
public RbacGrantDefinition(final RbacRoleDefinition roleDef, final RbacUserReference userDef) { public RbacGrantDefinition(final RbacRoleDefinition roleDef, final RbacSubjectReference userDef) {
this.userDef = userDef; this.userDef = userDef;
this.subRoleDef = roleDef; this.subRoleDef = roleDef;
this.superRoleDef = null; this.superRoleDef = null;
@ -770,8 +770,8 @@ public class RbacView {
* @return * @return
* The grant definition for further chained calls. * The grant definition for further chained calls.
*/ */
public RbacGrantDefinition owningUser(final RbacUserReference.UserRole userRole) { public RbacGrantDefinition owningUser(final RbacSubjectReference.UserRole userRole) {
return grantRoleToUser(this, findUserRef(userRole)); return grantRoleToSubject(this, findUserRef(userRole));
} }
/** /**
@ -831,14 +831,18 @@ public class RbacView {
public boolean isGlobal(final Role role) { public boolean isGlobal(final Role role) {
return entityAlias.isGlobal() && this.role == role; return entityAlias.isGlobal() && this.role == role;
} }
public String descriptorFunctionName() {
return entityAlias.getRawTableNameWithSchema() + "_" + capitalize(role.name());
}
} }
public RbacUserReference findUserRef(final RbacUserReference.UserRole userRole) { public RbacSubjectReference findUserRef(final RbacSubjectReference.UserRole userRole) {
return userDefs.stream().filter(u -> u.role == userRole).findFirst().orElseThrow(); return userDefs.stream().filter(u -> u.role == userRole).findFirst().orElseThrow();
} }
@EqualsAndHashCode @EqualsAndHashCode
public class RbacUserReference { public class RbacSubjectReference {
public enum UserRole { public enum UserRole {
GLOBAL_ADMIN, GLOBAL_ADMIN,
@ -847,7 +851,7 @@ public class RbacView {
final UserRole role; final UserRole role;
public RbacUserReference(final UserRole creator) { public RbacSubjectReference(final UserRole creator) {
this.role = creator; this.role = creator;
userDefs.add(this); userDefs.add(this);
} }
@ -885,7 +889,7 @@ public class RbacView {
.orElseGet(() -> new RbacPermissionDefinition(entityAlias, perm, tableName, true)); // TODO: true => toCreate .orElseGet(() -> new RbacPermissionDefinition(entityAlias, perm, tableName, true)); // TODO: true => toCreate
} }
private RbacGrantDefinition findOrCreateGrantDef(final RbacRoleDefinition roleDefinition, final RbacUserReference user) { private RbacGrantDefinition findOrCreateGrantDef(final RbacRoleDefinition roleDefinition, final RbacSubjectReference user) {
return grantDefs.stream() return grantDefs.stream()
.filter(g -> g.subRoleDef == roleDefinition && g.userDef == user) .filter(g -> g.subRoleDef == roleDefinition && g.userDef == user)
.findFirst() .findFirst()
@ -911,18 +915,18 @@ public class RbacView {
return distinctGrantDef; return distinctGrantDef;
} }
record EntityAlias(String aliasName, Class<? extends BaseEntity> entityClass, ColumnValue usingCase, SQL fetchSql, Column dependsOnColum, boolean isSubEntity, Nullable nullable) { record EntityAlias(String aliasName, Class<? extends BaseEntity<?>> entityClass, ColumnValue usingCase, SQL fetchSql, Column dependsOnColum, boolean isSubEntity, Nullable nullable) {
public EntityAlias(final String aliasName) { public EntityAlias(final String aliasName) {
this(aliasName, null, null, null, null, false, null); this(aliasName, null, null, null, null, false, null);
} }
public EntityAlias(final String aliasName, final Class<? extends BaseEntity> entityClass) { public EntityAlias(final String aliasName, final Class<? extends BaseEntity<?>> entityClass) {
this(aliasName, entityClass, null, null, null, false, null); this(aliasName, entityClass, null, null, null, false, null);
} }
boolean isGlobal() { boolean isGlobal() {
return aliasName().equals("global"); return aliasName().equals("rbac.global");
} }
boolean isPlaceholder() { boolean isPlaceholder() {
@ -937,7 +941,7 @@ public class RbacView {
return switch (fetchSql.part) { return switch (fetchSql.part) {
case SQL_QUERY -> fetchSql; case SQL_QUERY -> fetchSql;
case AUTO_FETCH -> case AUTO_FETCH ->
SQL.query("SELECT * FROM " + getRawTableName() + " WHERE uuid = ${ref}." + dependsOnColum.column); SQL.query("SELECT * FROM " + getRawTableNameWithSchema() + " WHERE uuid = ${ref}." + dependsOnColum.column);
default -> throw new IllegalStateException("unexpected SQL definition: " + fetchSql); default -> throw new IllegalStateException("unexpected SQL definition: " + fetchSql);
}; };
} }
@ -960,11 +964,35 @@ public class RbacView {
: uncapitalize(withoutEntitySuffix(entityClass.getSimpleName())); : uncapitalize(withoutEntitySuffix(entityClass.getSimpleName()));
} }
String getRawTableName() { String getRawTableNameWithSchema() {
if ( aliasName.equals("global")) { if ( aliasName.equals("rbac.global")) {
return "global"; // TODO: maybe we should introduce a GlobalEntity class? return "rbac.global"; // TODO: maybe we should introduce a GlobalEntity class?
} }
return withoutRvSuffix(entityClass.getAnnotation(Table.class).name()); return qualifiedRealTableName(entityClass);
}
String getRawTableSchemaPrefix() {
final var rawTableNameWithSchema = getRawTableNameWithSchema();
final var parts = rawTableNameWithSchema.split("\\.");
final var rawTableSchemaPrefix = parts.length > 1 ? parts[0] + "." : "";
return rawTableSchemaPrefix;
}
String getRawTableName() {
final var rawTableNameWithSchema = getRawTableNameWithSchema();
final var parts = rawTableNameWithSchema.split("\\.");
final var rawTableName = parts.length > 1 ? parts[1] : rawTableNameWithSchema;
return rawTableName;
}
String getRawTableShortName() {
// TODO.impl: some combined function and trigger names are too long
// maybe we should shorten the table name e.g. hs_office.coopsharetx -> hsof.coopsharetx
// this is just a workaround:
return getRawTableName()
.replace("hs_office.", "hsof.")
.replace("hs_booking.", "hsbk_")
.replace("hs_hosting.", "hsho_");
} }
String dependsOnColumName() { String dependsOnColumName() {
@ -984,8 +1012,12 @@ public class RbacView {
} }
} }
public static String withoutRvSuffix(final String tableName) { public static String qualifiedRealTableName(final Class<? extends BaseEntity<?>> entityClass) {
return tableName.substring(0, tableName.length() - "_rv".length()); final var tableAnnotation = entityClass.getAnnotation(Table.class);
final var schema = tableAnnotation.schema();
final var tableName = tableAnnotation.name();
final var realTableName = tableName.substring(0, tableName.length() - "_rv".length());
return (schema.isEmpty() ? "" : (schema + ".")) + realTableName;
} }
public enum Role { public enum Role {
@ -1166,7 +1198,7 @@ public class RbacView {
} }
String map(final String originalAliasName) { String map(final String originalAliasName) {
if (outerAliasNames.contains(originalAliasName) || originalAliasName.equals("global")) { if (outerAliasNames.contains(originalAliasName) || originalAliasName.equals("rbac.global")) {
return originalAliasName; return originalAliasName;
} }
if (originalAliasName.equals(importedRbacView.rootEntityAlias.aliasName)) { if (originalAliasName.equals(importedRbacView.rootEntityAlias.aliasName)) {
@ -1244,13 +1276,14 @@ public class RbacView {
public static Set<Class<? extends BaseEntity>> findRbacEntityClasses(String packageName) { public static Set<Class<? extends BaseEntity>> findRbacEntityClasses(String packageName) {
final var reflections = new Reflections(packageName, TypeAnnotationsScanner.class); final var reflections = new Reflections(packageName, TypeAnnotationsScanner.class);
return reflections.getTypesAnnotatedWith(Entity.class).stream() final Set<Class<? extends BaseEntity>> rbacEntityClasses = reflections.getTypesAnnotatedWith(Entity.class).stream()
.filter(c -> stream(c.getInterfaces()).anyMatch(i -> i== BaseEntity.class)) .filter(BaseEntity.class::isAssignableFrom)
.filter(c -> stream(c.getDeclaredMethods()) .filter(c -> stream(c.getDeclaredMethods())
.anyMatch(m -> m.getName().equals("rbac") && Modifier.isStatic(m.getModifiers())) .anyMatch(m -> m.getName().equals("rbac") && isStatic(m.getModifiers()))
) )
.map(RbacView::castToSubclassOfBaseEntity) .map(RbacView::castToSubclassOfBaseEntity)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
return rbacEntityClasses;
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")

View File

@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.rbac.rbacdef; package net.hostsharing.hsadminng.rbac.generator;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.CaseDef; import net.hostsharing.hsadminng.rbac.generator.RbacView.CaseDef;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import java.nio.file.*; import java.nio.file.*;
@ -12,7 +12,7 @@ import java.util.stream.Stream;
import static java.util.Comparator.comparing; import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.joining;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.*; import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacGrantDefinition.GrantType.*;
public class RbacViewMermaidFlowchartGenerator { public class RbacViewMermaidFlowchartGenerator {

View File

@ -1,4 +1,4 @@
package net.hostsharing.hsadminng.rbac.rbacdef; package net.hostsharing.hsadminng.rbac.generator;
import lombok.SneakyThrows; import lombok.SneakyThrows;
@ -6,8 +6,8 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardOpenOption; import java.nio.file.StandardOpenOption;
import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.NEW; import static net.hostsharing.hsadminng.rbac.generator.PostgresTriggerReference.NEW;
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with; import static net.hostsharing.hsadminng.rbac.generator.StringWriter.with;
public class RbacViewPostgresGenerator { public class RbacViewPostgresGenerator {
@ -17,7 +17,7 @@ public class RbacViewPostgresGenerator {
public RbacViewPostgresGenerator(final RbacView forRbacDef) { public RbacViewPostgresGenerator(final RbacView forRbacDef) {
rbacDef = forRbacDef; rbacDef = forRbacDef;
liqibaseTagPrefix = rbacDef.getRootEntityAlias().getRawTableName().replace("_", "-"); liqibaseTagPrefix = rbacDef.getRootEntityAlias().getRawTableNameWithSchema().replace("_", "-").replace(".", "-");
plPgSql.writeLn(""" plPgSql.writeLn("""
--liquibase formatted sql --liquibase formatted sql
-- This code generated was by ${generator}, do not amend manually. -- This code generated was by ${generator}, do not amend manually.

Some files were not shown because too many files have changed in this diff Show More