Merge remote-tracking branch 'origin/bugfix/hosting-assets-import-into-external-db' into feature/add-scenario-test-for-deceased-partner-with-community-of-heirs

This commit is contained in:
Michael Hoennig 2025-02-17 09:06:33 +01:00
commit a8964017ca
259 changed files with 42447 additions and 3194 deletions

View File

@ -1,4 +1,4 @@
# For using the alias gw-importOfficeData or gw-importHostingAssets, # For using the alias gw-importHostingAssets,
# copy the file .tc-environment to .environment (ignored by git) # copy the file .tc-environment to .environment (ignored by git)
# and amend them according to your external DB. # and amend them according to your external DB.
@ -71,7 +71,6 @@ function importLegacyData() {
./gradlew $target --rerun ./gradlew $target --rerun
fi fi
} }
alias gw-importOfficeData='importLegacyData importOfficeData'
alias gw-importHostingAssets='importLegacyData importHostingAssets' alias gw-importHostingAssets='importLegacyData importHostingAssets'
alias podman-start='systemctl --user enable --now podman.socket && systemctl --user status podman.socket && ls -la /run/user/$UID/podman/podman.sock' alias podman-start='systemctl --user enable --now podman.socket && systemctl --user status podman.socket && ls -la /run/user/$UID/podman/podman.sock'
@ -90,11 +89,54 @@ alias pg-sql-restore='gunzip --stdout | docker exec -i hsadmin-ng-postgres psql
alias fp='grep -r '@Accepts' src | sed -e 's/^.*@/@/g' | sort -u | wc -l' 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-check='. .aliases; . .tc-environment; gw test check -x pitest'
alias gw-check='. .aliases; gw test check -x pitest'
# HOWTO: run all 'normal' tests (by default without scenario+import-tests): `gw-test`
# You can also mention specific targets: `gw-test importHostingAssets`, in that case only these tests are executed.
# This will always use the environment from `.tc-environment`.
#
# HOWTO: re-run tests even if no changed can be detected: `gw-test --rerun`
# You can also mention specific targets: `gw-test scenarioTest --rerun`.
# This will always use the environment from `.tc-environment`.
#
# HOWTO: run all tests (unit, integration+acceptance, import and scenario): `gw-test --all`
# You can also re-run all these tests, which will take ~20min: `gw-test --all --rerun`
# This will always use the environment from `.tc-environment`.
#
function _gwTest1() {
echo
printf -- '=%0.s' {1..80}; echo
echo "RUNNING gw $@"
printf -- '-%0.s' {1..80}; echo
./gradlew "$@"
printf -- '-%0.s' {1..80}; echo
echo "DONE gw $@"
}
function _gwTest() {
. .aliases
. .tc-environment
rm -f /tmp/gwTest.tmp
if [ "$1" == "--all" ]; then
shift # to remove the --all from $@
# delierately in separate gradlew-calls to avoid Testcontains-PostgreSQL problem spillover
time (_gwTest1 unitTest "$@" &&
_gwTest1 officeIntegrationTest bookingIntegrationTest hostingIntegrationTest "$@" &&
_gwTest1 scenarioTest "$@" &&
_gwTest1 importHostingAssets "$@");
elif [ $# -eq 0 ] || [[ $1 == -* ]]; then
time _gwTest1 test "$@";
else
time _gwTest1 "$@";
fi
printf -- '=%0.s' {1..80}; echo
}
alias gw-test=_gwTest
alias howto=bin/howto
alias cas-curl=bin/cas-curl
# 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-importHostingAssets-in-docker-compose='
docker-compose -f etc/docker-compose.yml down && docker-compose -f etc/docker-compose.yml down &&
docker-compose -f etc/docker-compose.yml up -d && sleep 10 && docker-compose -f etc/docker-compose.yml up -d && sleep 10 &&
time gw-importHostingAssets' time gw-importHostingAssets'
@ -104,5 +146,6 @@ if [ ! -f .environment ]; then
fi fi
source .environment source .environment
alias scenario-reports-upload='./gradlew scenarioTests convertMarkdownToHtml && ssh hsh03-hsngdev@h50.hostsharing.net "rm -f doms/hsngdev.hs-example.de/htdocs-ssl/scenarios/office/*.html" && scp build/doc/scenarios/*.html hsh03-hsngdev@h50.hostsharing.net:doms/hsngdev.hs-example.de/htdocs-ssl/scenarios/office' alias scenario-reports-upload='./gradlew scenarioTest convertMarkdownToHtml && ssh hsh03-hsngdev@h50.hostsharing.net "rm -f doms/hsngdev.hs-example.de/htdocs-ssl/scenarios/office/*.html" && scp build/doc/scenarios/*.html hsh03-hsngdev@h50.hostsharing.net:doms/hsngdev.hs-example.de/htdocs-ssl/scenarios/office'
alias scenario-reports-open='open https://hsngdev.hs-example.de/scenarios/office' alias scenario-reports-open='open https://hsngdev.hs-example.de/scenarios/office'

View File

@ -7,6 +7,7 @@
<entry key="HSADMINNG_POSTGRES_ADMIN_USERNAME" value="postgres" /> <entry key="HSADMINNG_POSTGRES_ADMIN_USERNAME" value="postgres" />
<entry key="HSADMINNG_POSTGRES_JDBC_URL" value="jdbc:postgresql://localhost:5432/postgres" /> <entry key="HSADMINNG_POSTGRES_JDBC_URL" value="jdbc:postgresql://localhost:5432/postgres" />
<entry key="HSADMINNG_POSTGRES_RESTRICTED_USERNAME" value="restricted" /> <entry key="HSADMINNG_POSTGRES_RESTRICTED_USERNAME" value="restricted" />
<entry key="HSADMINNG_MIGRATION_DATA_PATH" value="migration" />
</map> </map>
</option> </option>
<option name="executionName" /> <option name="executionName" />

View File

@ -3,9 +3,9 @@
<ExternalSystemSettings> <ExternalSystemSettings>
<option name="env"> <option name="env">
<map> <map>
<entry key="HSADMINNG_MIGRATION_DATA_PATH" value="migration" />
<entry key="HSADMINNG_POSTGRES_ADMIN_USERNAME" value="admin" /> <entry key="HSADMINNG_POSTGRES_ADMIN_USERNAME" value="admin" />
<entry key="HSADMINNG_POSTGRES_RESTRICTED_USERNAME" value="restricted" /> <entry key="HSADMINNG_POSTGRES_RESTRICTED_USERNAME" value="restricted" />
<entry key="HSADMINNG_SUPERUSER" value="import-superuser@hostsharing.net" />
</map> </map>
</option> </option>
<option name="executionName" /> <option name="executionName" />

View File

@ -1,103 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="ImportOfficeData" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="env">
<map>
<entry key="HSADMINNG_MIGRATION_DATA_PATH" value="migration" />
<entry key="HSADMINNG_POSTGRES_ADMIN_USERNAME" value="admin" />
<entry key="HSADMINNG_POSTGRES_RESTRICTED_USERNAME" value="restricted" />
</map>
</option>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value=":importOfficeData" />
<option value="--tests" />
<option value="&quot;net.hostsharing.hsadminng.hs.migration.ImportOfficeData&quot;" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<EXTENSION ID="com.intellij.execution.ExternalSystemRunConfigurationJavaExtension">
<extension name="coverage" sample_coverage="false" />
</EXTENSION>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>true</RunAsTest>
<method v="2" />
</configuration>
<configuration default="false" name="ImportOfficeData" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="env">
<map>
<entry key="HSADMINNG_MIGRATION_DATA_PATH" value="migration" />
<entry key="HSADMINNG_POSTGRES_ADMIN_USERNAME" value="admin" />
<entry key="HSADMINNG_POSTGRES_RESTRICTED_USERNAME" value="restricted" />
</map>
</option>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value=":importOfficeData" />
<option value="--tests" />
<option value="&quot;net.hostsharing.hsadminng.hs.office.migration.ImportOfficeData&quot;" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<EXTENSION ID="com.intellij.execution.ExternalSystemRunConfigurationJavaExtension">
<extension name="coverage" sample_coverage="false" />
</EXTENSION>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>true</RunAsTest>
<method v="2" />
</configuration>
<configuration default="false" name="ImportOfficeData" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="env">
<map>
<entry key="HSADMINNG_POSTGRES_ADMIN_USERNAME" value="admin" />
<entry key="HSADMINNG_POSTGRES_RESTRICTED_USERNAME" value="restricted" />
</map>
</option>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value=":importOfficeData" />
<option value="--tests" />
<option value="&quot;net.hostsharing.hsadminng.hs.migration.ImportOfficeData&quot;" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<EXTENSION ID="com.intellij.execution.ExternalSystemRunConfigurationJavaExtension">
<extension name="coverage" sample_coverage="false" />
</EXTENSION>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>true</RunAsTest>
<method v="2" />
</configuration>
</component>

View File

@ -1,8 +1,7 @@
unset HSADMINNG_POSTGRES_JDBC_URL # dynamically set, different for normal tests and imports source .unset-environment
export HSADMINNG_POSTGRES_ADMIN_USERNAME=admin
export HSADMINNG_POSTGRES_ADMIN_PASSWORD=
export HSADMINNG_POSTGRES_RESTRICTED_USERNAME=restricted export HSADMINNG_POSTGRES_RESTRICTED_USERNAME=restricted
export HSADMINNG_SUPERUSER=superuser-alex@hostsharing.net export HSADMINNG_POSTGRES_ADMIN_USERNAME=admin
export HSADMINNG_MIGRATION_DATA_PATH=migration export HSADMINNG_SUPERUSER=import-superuser@hostsharing.net
export LIQUIBASE_CONTEXT=
export LANG=en_US.UTF-8 export LANG=en_US.UTF-8

View File

@ -4,5 +4,5 @@ unset HSADMINNG_POSTGRES_ADMIN_PASSWORD
unset HSADMINNG_POSTGRES_RESTRICTED_USERNAME unset HSADMINNG_POSTGRES_RESTRICTED_USERNAME
unset HSADMINNG_SUPERUSER unset HSADMINNG_SUPERUSER
unset HSADMINNG_MIGRATION_DATA_PATH unset HSADMINNG_MIGRATION_DATA_PATH
unset LIQUIBASE_CONTEXT unset HSADMINNG_OFFICE_DATA_SQL_FILE

25
Jenkinsfile vendored
View File

@ -35,19 +35,34 @@ pipeline {
stage ('Tests') { stage ('Tests') {
parallel { parallel {
stage('Unit-/Integration/Acceptance-Tests') { stage('Unit-Tests') {
steps { steps {
sh './gradlew check --no-daemon -x pitest -x dependencyCheckAnalyze -x importOfficeData -x importHostingAssets' sh './gradlew unitTest --no-daemon'
} }
} }
stage('Import-Tests') { stage('General-Tests') {
steps { steps {
sh './gradlew importOfficeData importHostingAssets --no-daemon' sh './gradlew generalTest --no-daemon'
}
}
stage('Office-Tests') {
steps {
sh './gradlew officeIntegrationTest --no-daemon'
}
}
stage('Booking+Hosting-Tests') {
steps {
sh './gradlew bookingIntegrationTest hostingIntegrationTest --no-daemon'
}
}
stage('Test-Imports') {
steps {
sh './gradlew importHostingAssets --no-daemon'
} }
} }
stage ('Scenario-Tests') { stage ('Scenario-Tests') {
steps { steps {
sh './gradlew scenarioTests --no-daemon' sh './gradlew scenarioTest --no-daemon'
} }
} }
} }

148
README.md
View File

@ -27,7 +27,13 @@ For architecture consider the files in the `doc` and `adr` folder.
- [OWASP Security Vulnerability Check](#owasp-security-vulnerability-check) - [OWASP Security Vulnerability Check](#owasp-security-vulnerability-check)
- [Dependency-License-Compatibility](#dependency-license-compatibility) - [Dependency-License-Compatibility](#dependency-license-compatibility)
- [Dependency Version Upgrade](#dependency-version-upgrade) - [Dependency Version Upgrade](#dependency-version-upgrade)
- [Biggest Flaws in our Architecture](#biggest-flaws-in-our-architecture)
- [The RBAC System is too Complicated](#the-rbac-system-is-too-complicated)
- [The Mapper is Error-Prone](#the-mapper-is-error-prone)
- [Too Many Business-Rules Implemented in Controllers](#too-many-business-rules-implemented-in-controllers)
- [How To ...](#how-to-...) - [How To ...](#how-to-...)
- [How to Run the Application With Other Profiles, e.g. production](#)
- [How to Do a Clean Run of the Application](#how-to-do-a-clean-run-of-the-application)
- [How to Configure .pgpass for the Default PostgreSQL Database?](#how-to-configure-.pgpass-for-the-default-postgresql-database?) - [How to Configure .pgpass for the Default PostgreSQL Database?](#how-to-configure-.pgpass-for-the-default-postgresql-database?)
- [How to Run the Tests Against a Local User-Space Podman Daemon?](#how-to-run-the-tests-against-a-local-user-space-podman-daemon?) - [How to Run the Tests Against a Local User-Space Podman Daemon?](#how-to-run-the-tests-against-a-local-user-space-podman-daemon?)
- [Install and Run Podman](#install-and-run-podman) - [Install and Run Podman](#install-and-run-podman)
@ -40,6 +46,7 @@ For architecture consider the files in the `doc` and `adr` folder.
- [How to Amend Liquibase SQL Changesets?](#how-to-amend-liquibase-sql-changesets?) - [How to Amend Liquibase SQL Changesets?](#how-to-amend-liquibase-sql-changesets?)
- [How to Re-Generate Spring-Controller-Interfaces from OpenAPI specs?](#how-to-re-generate-spring-controller-interfaces-from-openapi-specs?) - [How to Re-Generate Spring-Controller-Interfaces from OpenAPI specs?](#how-to-re-generate-spring-controller-interfaces-from-openapi-specs?)
- [How to Generate Database Table Diagrams?](#how-to-generate-database-table-diagrams?) - [How to Generate Database Table Diagrams?](#how-to-generate-database-table-diagrams?)
- [How to Add (Real) Admin Users](#how-to-add-(real)-admin-users)
- [Further Documentation](#further-documentation) - [Further Documentation](#further-documentation)
<!-- generated TOC end. --> <!-- generated TOC end. -->
@ -51,10 +58,11 @@ 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
- optionally: PostgreSQL Server 15.5-bookworm - optionally: PostgreSQL Server 15.5-bookworm, if you want to use the database directly, not just via Docker
(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/`.
- 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*.
- Python 3 is expected in /usr/bin/python3 if you want to run the `howto` tool (see `bin/howto`)
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:
@ -64,32 +72,55 @@ If you have at least Docker and the Java JDK installed in appropriate versions a
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 - takes >10min even on a fast machine gw test # compiles and runs unit- and integration-tests - takes >10min even on a fast machine
gw scenarioTests # compiles and scenario-tests - takes ~1min on a decent machine # `gw test` does NOT run import- and scenario-tests.
# Use `gw-test` instead to make sure .tc-environment is sourced.
gw scenarioTest # compiles and scenario-tests - takes ~1min on a decent machine
# Use `gw-test scenarioTest` instead to make sure .tc-environment is sourced.
howto test # shows more test information about how to run 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 and you want to keep the data, run this: # if the container has been built already and you want to keep the data, run this:
pg-sql-start pg-sql-start
gw bootRun # compiles and runs the application on localhost:8080 Next, compile and run the application on `localhost:8080` and the management server on `localhost:8081`:
# this disables CAS-authentication, for using the REST-API with CAS-authentication, see `bin/cas-curl`.
export HSADMINNG_CAS_SERVER=
# this runs the application with test-data and all modules:
gw bootRun --args='--spring.profiles.active=dev,complete,test-data'
The meaning of these profiles is:
- **dev**: the PostgreSQL users are created via Liquibase
- **complete**: all modules are started
- **test-data**: some test data inserted
Running just `gw bootRun` would just run the *office* module, not insert any test-data and
require the PostgreSQL users created in the database (see env-vars in `.aliases`).
Now we can access the REST API, e.g. using curl:
# the following command should reply with "pong": # the following command should reply with "pong":
curl -f http://localhost:8080/api/ping curl -f -s http://localhost:8080/api/ping
# 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 -f\ curl -f -s\
-H 'current-subject: superuser-alex@hostsharing.net' \ -H 'current-subject: superuser-alex@hostsharing.net' \
http://localhost:8080/api/test/customers \ http://localhost:8080/api/test/customers \
| jq # just if `jq` is installed, to prettyprint the output | jq # just if `jq` is installed, to prettyprint the output
# 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 -f\ curl -f -s\
-H 'current-subject: superuser-alex@hostsharing.net' -H 'assumed-roles: rbactest.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 \
| jq | jq
# add a new customer # add a new customer
curl -f\ curl -f -s\
-H 'current-subject: 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 \
@ -101,7 +132,7 @@ Also try for example 'admin@xxx.example.com' or 'unknown@example.org'.
If you want a formatted JSON output, you can pipe the result to `jq` or similar. If you want a formatted JSON output, you can pipe the result to `jq` or similar.
And to see the full, currently implemented, API, open http://localhost:8080/swagger-ui/index.html. And to see the full, currently implemented, API, open http://localhost:8081/actuator/swagger-ui/index.html (uses management-port and thus bypasses authentication).
If you still need to install some of these tools, find some hints in the next chapters. If you still need to install some of these tools, find some hints in the next chapters.
@ -181,7 +212,7 @@ To generate the TOC (Table of Contents), a little bash script from a
Given this is in PATH as `md-toc`, use: Given this is in PATH as `md-toc`, use:
```shell ```shell
md-toc <README.md 2 4 | cut -c5-' md-toc <README.md 2 4 | cut -c5-
``` ```
To render the Markdown files, especially to watch embedded PlantUML diagrams, you can use one of the following methods: To render the Markdown files, especially to watch embedded PlantUML diagrams, you can use one of the following methods:
@ -419,36 +450,42 @@ Some of these rules are checked with *ArchUnit* unit tests.
### Run Tests from Command Line ### Run Tests from Command Line
Run all tests which have not yet been passed with the current source code: Run all unit-, integration- and acceptance-tests which have not yet been passed with the current source code:
```shell ```shell
gw test gw test # uses the current environment, especially HSADMINNG_POSTGRES_JDBC_URL
```
If the referenced database is not empty, the tests might fail.
To explicitly use the Testcontainers-environment, run:
```shell
gw-test # uses the environment from .tc-environment
``` ```
Force running all tests: Force running all tests:
```shell ```shell
gw cleanTest test gw-test --rerun
``` ```
To find more options about running tests, try `howto test`.
### Spotless Code Formatting ### Spotless Code Formatting
Code formatting for Java is checked via *spotless*. Code formatting for Java is checked via *spotless*.
The formatting style can be checked with this command:
```shell
gw spotlessCheck
```
This task is also included in `gw build` and `gw check`.
To apply formatting rules, use: To apply formatting rules, use:
```shell ```shell
gw spotlessApply gw-spotless
``` ```
The gradle task spotlessCheck is also included in `gw build` and `gw check`,
thus if the formatting is not compliant to the rules, the build is going to fail.
### JaCoCo Test Code Coverage Check ### JaCoCo Test Code Coverage Check
This project uses the JaCoCo test code coverage report with limit checks. This project uses the JaCoCo test code coverage report with limit checks.
@ -486,13 +523,12 @@ Classes to be scanned, tests to be executed and thresholds are configured in [bu
A report is generated under [build/reports/pitest/index.html](./build/reports/pitest/index.html). A report is generated under [build/reports/pitest/index.html](./build/reports/pitest/index.html).
A link to the report is also printed after the `pitest` run. A link to the report is also printed after the `pitest` run.
This task is also executed as part of `gw check`. <!-- TODO.test: This task is also executed as part of `gw check`. -->
#### Remark #### Remark
In this project, there is little business logic in *Java* code; In this project, there is a large amount of code is in *plsql*, especially for RBAC.
most business code is in *plsql* *Java* ist mostly used for mapping and validating REST calls to database queries.
and *Java* ist mostly used for mapping REST calls to database queries.
This mapping ist mostly done through *Spring* annotations and other implicit code. This mapping ist mostly done through *Spring* annotations and other implicit code.
Therefore, there are only few unit tests and thus mutation testing has limited value. Therefore, there are only few unit tests and thus mutation testing has limited value.
@ -526,7 +562,7 @@ In case of suppression, a note must be added to explain why it does not apply to
See also: https://jeremylong.github.io/DependencyCheck/dependency-check-gradle/index.html. See also: https://jeremylong.github.io/DependencyCheck/dependency-check-gradle/index.html.
### Dependency-License-Compatibility ### How to Check Dependency-License-Compatibility
The `gw check` phase depends on a dependency-license-compatibility check. The `gw check` phase depends on a dependency-license-compatibility check.
If any dependency violates the configured [list of allowed licenses](etc/allowed-licenses.json), the build will fail. If any dependency violates the configured [list of allowed licenses](etc/allowed-licenses.json), the build will fail.
@ -556,7 +592,7 @@ The generated license can be found here: [index.html](build/reports/dependency-l
More information can be found on the [project's website](https://github.com/jk1/Gradle-License-Report). More information can be found on the [project's website](https://github.com/jk1/Gradle-License-Report).
### Dependency Version Upgrade ### How to Upgrade Versions of Dependencies
Dependency versions can be automatically upgraded to the latest available version: Dependency versions can be automatically upgraded to the latest available version:
@ -584,8 +620,9 @@ This way we would get rid of all explicit grants within the same DB-row
and would not need the `rbac.role` table anymore. and would not need the `rbac.role` table anymore.
We would also reduce the depth of the expensive recursive CTE-query. We would also reduce the depth of the expensive recursive CTE-query.
This has to be explored further. This has to be explored further. For now, we just keep it in mind and avoid roles+grants
For now, we just keep it in mind and which would not fit into a simplified system with a fixed role-type-system.
### The Mapper is Error-Prone ### The Mapper is Error-Prone
@ -609,6 +646,36 @@ Besides the following *How Tos* you can also find several *How Tos* in the sourc
grep -r HOWTO src grep -r HOWTO src
``` ```
also try this (assumed you've sourced .aliases):
```sh
howto
```
### How to Run the Application With Other Profiles, e.g. production:
Add `--args='--spring.profiles.active=...` with the wanted profile selector:
```sh
gw bootRun --args='--spring.profiles.active=external-db,only -office,without-test-data'
```
These profiles mean:
- **external-db**: an external PostgreSQL database is used with the PostgreSQL users already created as specified in the environment
- **only-office**: only the Office module is started, but neither the Booking nor the Hosting modules
- **without-test-data**: no test-data is inserted
### How to Do a Clean Run of the Application
If you frequently need to run with a fresh database and a clean build, you can use this:
```sh
export HSADMINNG_CAS_SERVER=
gw clean && pg-sql-reset && sleep 5 && gw bootRun' 2>&1 | tee log
```
### How to Configure .pgpass for the Default PostgreSQL Database? ### How to Configure .pgpass for the Default PostgreSQL Database?
To access the default database schema as used during development, add this line to your `.pgpass` file in your users home directory: To access the default database schema as used during development, add this line to your `.pgpass` file in your users home directory:
@ -807,6 +874,29 @@ postgres-autodoc
The output will list the generated files. The output will list the generated files.
### How to Add (Real) Admin Users
```sql
DO $$
DECLARE
-- replace with your admin account names
admin_users TEXT[] := ARRAY['admin-1', 'admin-2', 'admin-3'];
admin TEXT;
BEGIN
-- run as superuser
call base.defineContext('adding real admin users', null, null, null);
-- for all new admin accounts
FOREACH admin IN ARRAY admin_users LOOP
call rbac.grantRoleToSubjectUnchecked(
rbac.findRoleId(rbac.global_ADMIN()), -- granted by role
rbac.findRoleId(rbac.global_ADMIN()), -- role to grant
rbac.create_subject(admin)); -- creates the new admin account
END LOOP;
END $$;
```
## Further Documentation ## Further Documentation
- the `doc` directory contains architecture concepts and a glossary - the `doc` directory contains architecture concepts and a glossary

249
bin/cas-curl Executable file
View File

@ -0,0 +1,249 @@
#!/bin/bash
if [ "$2" == "--show-password" ]; then
HSADMINNG_CAS_SHOW_PASSWORD=yes
shift
else
HSADMINNG_CAS_SHOW_PASSWORD=
fi
if [ "$1" == "--trace" ]; then
function trace() {
echo "$*" >&2
}
function doCurl() {
set -x
if [ -z "$HSADMINNG_CAS_ASSUME" ]; then
curl --fail-with-body \
--header "Authorization: $HSADMINNG_CAS_TICKET" \
"$@"
else
curl --fail-with-body \
--header "Authorization: $HSADMINNG_CAS_TICKET" \
--header "assumed-roles: $HSADMINNG_CAS_ASSUME" \
"$@"
fi
set +x
}
shift
else
function trace() {
: # noop
}
function doCurl() {
curl --fail-with-body --header "Authorization: $HSADMINNG_CAS_TICKET" "$@"
}
fi
export HSADMINNG_CAS_ASSUME_HEADER
if [ -f ~/.cas-curl-assume ]; then
HSADMINNG_CAS_ASSUME="$(cat ~/.cas-curl-assume)"
else
HSADMINNG_CAS_ASSUME=
fi
if [ -z "$HSADMINNG_CAS_LOGIN" ] || [ -z "$HSADMINNG_CAS_VALIDATE" ] || \
[ -z "$HSADMINNG_CAS_SERVICE_ID" ]; then
cat >&2 <<EOF
ERROR: environment incomplete
please set the following environment variables:
export HSADMINNG_CAS_LOGIN=https://login.hostsharing.net/cas/v1/tickets
export HSADMINNG_CAS_VALIDATE=https://login.hostsharing.net/cas/proxyValidate
export HSADMINNG_CAS_USERNAME=<<optionally, your username, or leave empty after '='>>
export HSADMINNG_CAS_PASSWORD=<<optionally, your password, or leave empty after '='>>
export HSADMINNG_CAS_SERVICE_ID=https://hsadminng.hostsharing.net:443/
EOF
exit 1
fi
function casCurlDocumentation() {
cat <<EOF
curl-wrapper utilizing CAS-authentication for hsadmin-ng
usage: $0 [--trace] [--show-password] <<command>> [parameters]
commands:
EOF
# filters out help texts (containing double-# and following lines with leading single-#) from the commands itself
# (the '' makes sure that this line is not found, just the lines with actual help texts)
sed -n '/#''#/ {x; p; x; s/#''#//; p; :a; n; /^[[:space:]]*#/!b; s/^[[:space:]]*#//; p; ba}' <$0
}
function casLogin() {
# ticket granting ticket exists and not expired?
if find ~/.cas-login-tgt -type f -size +0c -mmin -60 2>/dev/null | grep -q .; then
return
fi
if [ -z "$HSADMINNG_CAS_USERNAME" ]; then
read -e -p "Username: " HSADMINNG_CAS_USERNAME
fi
if [ -z "$HSADMINNG_CAS_PASSWORD" ]; then
read -s -e -p "Password: " HSADMINNG_CAS_PASSWORD
fi
if [ "$HSADMINNG_CAS_SHOW_PASSWORD" == "--show-password" ]; then
HSADMINNG_CAS_PASSWORD_DISPLAY=$HSADMINNG_CAS_PASSWORD
else
HSADMINNG_CAS_PASSWORD_DISPLAY="<<password hidden - use --show-password to show>>"
fi
# Do NOT use doCurl here! We do neither want to print the password nor pass a CAS service ticket.
trace "+ curl --fail-with-body -s -i -X POST \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d \"username=$HSADMINNG_CAS_USERNAME&password=$HSADMINNG_CAS_PASSWORD_DISPLAY\" \
$HSADMINNG_CAS_LOGIN -o ~/.cas-login-tgt.response -D -"
HSADMINNG_CAS_TGT=`curl --fail-with-body -s -i -X POST \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d "username=$HSADMINNG_CAS_USERNAME&password=$HSADMINNG_CAS_PASSWORD" \
$HSADMINNG_CAS_LOGIN -o ~/.cas-login-tgt.response -D - \
| grep -i "^Location: " | sed -e 's/^Location: //' -e 's/\\r//'`
if [ -z "$HSADMINNG_CAS_TGT" ]; then
echo "ERROR: could not get ticket granting ticket" >&2
cat ~/.cas-login-tgt.response >&2
exit 1
fi
echo "$HSADMINNG_CAS_TGT" >~/.cas-login-tgt
trace "$HSADMINNG_CAS_TGT"
}
function casLogout() {
rm -f ~/.cas-login-tgt
}
function casTicket() {
HSADMINNG_CAS_TGT=$(<~/.cas-login-tgt)
if [[ -z "$HSADMINNG_CAS_TGT" ]]; then
echo "ERROR: cannot get CAS ticket granting ticket for $HSADMINNG_CAS_USERNAME" >&2
exit 1
fi
trace "CAS-TGT: $HSADMINNG_CAS_TGT"
trace "fetching CAS service ticket"
trace "curl -s -d \"service=$HSADMINNG_CAS_SERVICE_ID\" $HSADMINNG_CAS_TGT"
HSADMINNG_CAS_TICKET=$(curl -s -d "service=$HSADMINNG_CAS_SERVICE_ID" $HSADMINNG_CAS_TGT)
if [[ -z "$HSADMINNG_CAS_TICKET" ]]; then
echo "ERROR: cannot get CAS service ticket" >&2
exit 1
fi
echo $HSADMINNG_CAS_TICKET
}
function casValidate() {
HSADMINNG_CAS_TICKET=`casTicket`
trace "validating CAS-TICKET: $HSADMINNG_CAS_TICKET"
# Do NOT use doCurl here! We do not pass a CAS service ticket.
trace curl -i -s $HSADMINNG_CAS_VALIDATE?ticket=${HSADMINNG_CAS_TICKET}\&service=${HSADMINNG_CAS_SERVICE_ID}
HSADMINNG_CAS_USER=`curl -i -s $HSADMINNG_CAS_VALIDATE?ticket=${HSADMINNG_CAS_TICKET}\&service=${HSADMINNG_CAS_SERVICE_ID} | grep -oPm1 "(?<=<cas:user>)[^<]+"`
if [ -z "$HSADMINNG_CAS_USER" ]; then
echo "validation failed" >&2
exit 1
fi
echo "CAS-User: $HSADMINNG_CAS_USER"
}
case "${1,,}" in
# -- generic commands --------------------------------------------------------------------------
""|"-h"|"--help"|"help") ## prints documentation about commands and options
casCurlDocumentation
exit
;;
"env") ## prints all related HSADMINNG_CAS_... environment variables; use '--show-password' to show the password as well
# example: cas-curl env --show-password
echo "HSADMINNG_CAS_LOGIN: $HSADMINNG_CAS_LOGIN"
echo "HSADMINNG_CAS_VALIDATE: $HSADMINNG_CAS_VALIDATE"
echo "HSADMINNG_CAS_USERNAME: $HSADMINNG_CAS_USERNAME"
if [ "$2" == "--show-password" ]; then
echo "HSADMINNG_CAS_PASSWORD: $HSADMINNG_CAS_PASSWORD"
elif [ -z "$HSADMINNG_CAS_PASSWORD" ]; then
echo "HSADMINNG_CAS_PASSWORD: <<not given>>"
else
echo "HSADMINNG_CAS_PASSWORD: <<given, but hidden - add --show-password to show>>"
fi
echo "HSADMINNG_CAS_SERVICE_ID: $HSADMINNG_CAS_SERVICE_ID"
;;
# --- authentication-related commands ------------------------------------------------------------
"login") ## reads username+password and fetches ticket granting ticket (bypasses HSADMINNG_CAS_USERNAME+HSADMINNG_CAS_PASSWORD)
# example: cas-curl login
casLogout
export HSADMINNG_CAS_USERNAME=
export HSADMINNG_CAS_PASSWORD=
casLogin
;;
"assume") ## assumes the given comma-separated roles
# example using object-id-name: cas-curl assume 'hs_office.relation#ExampleMandant-with-PARTNER-ExamplePartner:AGENT'
# example using object-uuid: cas-curl assume 'hs_office.relation#1d3bc468-c5c8-11ef-9d0d-4751ecfda2b7:AGENT'
shift
if [ -z "$1" ]; then
echo "ERROR: requires comma-separated list of roles to assume" >&2
exit 1
fi
echo "$1" >~/.cas-curl-assume
;;
"unassume") ## do not assume any particular role anymore, use the plain user as RBAC subject
rm ~/.cas-curl-assume
;;
"validate") ## validates current ticket granting ticket and prints currently logged in user
casValidate
;;
"logout") ## logout, deletes ticket granting ticket
casLogout
;;
# --- HTTP-commands ----------------------------------------------------------------------
"get") ## HTTP GET, add URL as parameter
# example: cas-curl GET http://localhost:8080/api/hs/office/partners/P-10003 | jq
# hint: '| jq' is just for human-readable formatted JSON output
shift
casLogin
HSADMINNG_CAS_TICKET=`casTicket`
doCurl "$*"
;;
"post") ## HTTP POST, add curl options to specify the request body and the URL as last parameter
# example: cas-curl POST \
# -d '{ "prefix":"ttt", "reference":80001, "adminUserName":"admin@ttt.example.com" }' \
# http://localhost:8080/api/test/customers | jq
# hint: '| jq' is just for human-readable formatted JSON output
shift
casLogin
HSADMINNG_CAS_TICKET=`casTicket`
doCurl --header "Content-Type: application/json" -X POST "$@"
;;
"patch") ## HTTP PATCH, add curl options to specify the request body and the URL as last parameterparameter
# example: cas-curl PATCH \
# -d '{ "reference":80002 }' \
# http://localhost:8080/api/test/customers/ae90ac2a-4728-4ca9-802e-a0d0108b2324 | jq
# hint: '| jq' is just for human-readable formatted JSON output
shift
casLogin
HSADMINNG_CAS_TICKET=`casTicket`
doCurl --header "Content-Type: application/json" -X POST "$*"
;;
"delete") ## HTTP DELETE, add curl options to specify the request body and the URL as last parameter
# example: cas-curl DELETE http://localhost:8080/api/hs/office/persons/ae90ac2a-4728-4ca9-802e-a0d0108b2324
shift
casLogin
HSADMINNG_CAS_TICKET=`casTicket`
curl -X POST "$@"
;;
*)
cat >&2 <<EOF
unknown command: '$1'
valid commands: help, login, logout, validate, get, post, patch, delete
EOF
exit 1
;;
esac

86
bin/howto Executable file
View File

@ -0,0 +1,86 @@
#!/usr/bin/python3
import os
import sys
from urllib.parse import urljoin, quote
def path_to_file_uri(path):
"""
Converts a file path to a file URI.
Handles absolute and relative paths.
"""
abs_path = os.path.abspath(path)
return urljoin("file://", quote(abs_path))
def is_binary_file(filepath, chunk_size=1024):
"""
Prüft, ob eine Datei binär ist, indem sie den Inhalt der Datei auf nicht-druckbare Zeichen untersucht.
"""
try:
with open(filepath, "rb") as file:
chunk = file.read(chunk_size)
if b"\0" in chunk: # Nullbyte ist ein typisches Zeichen für Binärdateien
return True
return False
except Exception as e:
print(f"Fehler beim Prüfen, ob Datei binär ist: {filepath}: {e}")
return True
def search_keywords_in_files(keywords):
if not keywords:
print("Bitte geben Sie mindestens ein Stichwort an.")
sys.exit(1)
# Allowed comment symbols
comment_symbols = {"//", "#", ";"}
for root, dirs, files in os.walk("."):
# Ausschließen bestimmter Verzeichnisse
dirs[:] = [d for d in dirs if d not in {".git", "build"}]
for file in files:
filepath = os.path.join(root, file)
# Überspringen von Binärdateien
if is_binary_file(filepath):
continue
try:
with open(filepath, "r", encoding="utf-8") as f:
lines = f.readlines()
for line_number, line in enumerate(lines, start=1):
stripped_line = line.lstrip() # Entfernt führende Leerzeichen
for symbol in comment_symbols:
if stripped_line.startswith(symbol):
# Entfernt das Kommentarzeichen und nachfolgende Leerzeichen
howtoMatch = stripped_line[len(symbol):].lstrip()
if howtoMatch.startswith(("HOWTO ", "HOWTO: ", "How to ")):
if all(keyword in howtoMatch.lower() for keyword in keywords):
# Titelzeile ohne Kommentarzeichen
print(howtoMatch.rstrip())
# Ausgabe nachfolgender Zeilen mit dem gleichen Kommentar-Präfix
for subsequent_line in lines[line_number:]:
subsequent_line = subsequent_line.lstrip()
if subsequent_line.startswith(symbol):
# Entfernt Kommentarzeichen aus Folgezeilen
print("\t" + subsequent_line[len(symbol):].strip())
else:
break
# Link mit Zeilennummer
print(f"--> {path_to_file_uri(filepath)}:{line_number}")
# Abstand zwischen Matches
print()
break
except Exception as e:
print(f"Fehler beim Lesen der Datei {filepath}: {e}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Verwendung: bin/howto <Stichwort1> <Stichwort2> ...")
sys.exit(1)
search_keywords_in_files([arg.lower() for arg in sys.argv[1:]])

View File

@ -1,17 +1,20 @@
plugins { plugins {
id 'java' id 'java'
id 'org.springframework.boot' version '3.3.4' id 'org.springframework.boot' version '3.4.1'
id 'io.spring.dependency-management' version '1.1.6' id 'io.spring.dependency-management' version '1.1.7' // manages implicit dependencies
id 'io.openapiprocessor.openapi-processor' version '2023.2' id 'io.openapiprocessor.openapi-processor' version '2023.2' // generates Controller-interface and resources from API-spec
id 'com.github.jk1.dependency-license-report' version '2.9' id 'com.github.jk1.dependency-license-report' version '2.9' // checks dependency-license compatibility
id "org.owasp.dependencycheck" version "10.0.4" id "org.owasp.dependencycheck" version "12.0.1" // checks dependencies for known vulnerabilities
id "com.diffplug.spotless" version "6.25.0" id "com.diffplug.spotless" version "7.0.2" // formats + checks formatting for source-code
id 'jacoco' id 'jacoco' // determines code-coverage of tests
id 'info.solidsoft.pitest' version '1.15.0' id 'info.solidsoft.pitest' version '1.15.0' // performs mutation testing
id 'se.patrikerdes.use-latest-versions' version '0.2.18' id 'se.patrikerdes.use-latest-versions' version '0.2.18' // updates module and plugin versions
id 'com.github.ben-manes.versions' version '0.51.0' id 'com.github.ben-manes.versions' version '0.52.0' // determines which dependencies have updates
} }
// HOWTO: find out which dependency versions are managed by Spring Boot:
// https://docs.spring.io/spring-boot/appendix/dependency-versions/coordinates.html
group = 'net.hostsharing' group = 'net.hostsharing'
version = '0.0.1-SNAPSHOT' version = '0.0.1-SNAPSHOT'
@ -20,6 +23,9 @@ wrapper {
gradleVersion = '8.5' gradleVersion = '8.5'
} }
// TODO.impl: self-attaching is deprecated, see:
// https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#0.3
configurations { configurations {
compileOnly { compileOnly {
extendsFrom annotationProcessor extendsFrom annotationProcessor
@ -60,25 +66,25 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2' implementation 'com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.10.0'
implementation 'org.springdoc:springdoc-openapi:2.6.0' implementation 'org.springdoc:springdoc-openapi:2.8.3'
implementation 'org.postgresql:postgresql:42.7.4' implementation 'org.postgresql:postgresql'
implementation 'org.liquibase:liquibase-core:4.29.2' implementation 'org.liquibase:liquibase-core'
implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.8.3' implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.9.0'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.0' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
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.12.0' implementation 'org.apache.commons:commons-text:1.13.0'
implementation 'net.java.dev.jna:jna:5.15.0' implementation 'net.java.dev.jna:jna:5.16.0'
implementation 'org.modelmapper:modelmapper:3.2.1' implementation 'org.modelmapper:modelmapper:3.2.2'
implementation 'org.iban4j:iban4j:3.2.10-RELEASE' implementation 'org.iban4j:iban4j:3.2.10-RELEASE'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.3'
implementation 'org.webjars:swagger-ui:5.17.14'
implementation 'org.reflections:reflections:0.10.2' implementation 'org.reflections:reflections:0.10.2'
compileOnly 'org.projectlombok:lombok' compileOnly 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok' testCompileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools' // TODO.impl: version conflict with SpringDoc, check later and re-enable if fixed
// developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok' testAnnotationProcessor 'org.projectlombok:lombok'
@ -90,9 +96,10 @@ dependencies {
testImplementation 'org.testcontainers:postgresql' testImplementation 'org.testcontainers:postgresql'
testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0' 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:3.0' testImplementation 'org.hamcrest:hamcrest-core'
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'
testImplementation 'org.wiremock:wiremock-standalone:3.10.0'
} }
dependencyManagement { dependencyManagement {
@ -173,7 +180,9 @@ openapiProcessor {
} }
} }
sourceSets.main.java.srcDir 'build/generated/sources/openapi' sourceSets.main.java.srcDir 'build/generated/sources/openapi'
abstract class ProcessSpring extends DefaultTask {} abstract class ProcessSpring extends DefaultTask {}
tasks.register('processSpring', ProcessSpring) tasks.register('processSpring', ProcessSpring)
['processSpringRoot', ['processSpringRoot',
'processSpringRbac', 'processSpringRbac',
@ -204,7 +213,7 @@ openApiGenerate.dependsOn processSpring
spotless { spotless {
java { java {
removeUnusedImports() removeUnusedImports()
indentWithSpaces(4) leadingTabsToSpaces(4)
endWithNewline() endWithNewline()
toggleOffOn() toggleOffOn()
@ -218,7 +227,7 @@ project.tasks.check.dependsOn(spotlessCheck)
// HACK: no idea why spotless uses the output of these tasks, but we get warnings without those // HACK: no idea why spotless uses the output of these tasks, but we get warnings without those
project.tasks.spotlessJava.dependsOn( project.tasks.spotlessJava.dependsOn(
tasks.generateLicenseReport, tasks.generateLicenseReport,
tasks.pitest, // tasks.pitest, TODO.test: PiTest currently does not work, needs to be fixed
tasks.jacocoTestReport, tasks.jacocoTestReport,
tasks.processResources, tasks.processResources,
tasks.processTestResources) tasks.processTestResources)
@ -247,19 +256,21 @@ licenseReport {
} }
project.tasks.check.dependsOn(checkLicense) project.tasks.check.dependsOn(checkLicense)
// JaCoCo Test Code Coverage // HOWTO: run all tests except import- and scenario-tests: gw test
jacoco {
toolVersion = "0.8.10"
}
test { test {
finalizedBy jacocoTestReport // generate report after tests finalizedBy jacocoTestReport // generate report after tests
excludes = [ excludes = [
'net.hostsharing.hsadminng.**.generated.**', 'net.hostsharing.hsadminng.**.generated.**',
] ]
useJUnitPlatform { useJUnitPlatform {
excludeTags 'importOfficeData', 'importHostingData', 'scenarioTest' excludeTags 'importHostingAssets', 'scenarioTest'
} }
} }
// JaCoCo Test Code Coverage for unit-tests
jacoco {
toolVersion = "0.8.10"
}
jacocoTestReport { jacocoTestReport {
dependsOn test dependsOn test
afterEvaluate { afterEvaluate {
@ -324,13 +335,63 @@ jacocoTestCoverageVerification {
} }
} }
tasks.register('importOfficeData', Test) { // HOWTO: run all unit-tests which don't need a database: gw-test unitTest
tasks.register('unitTest', Test) {
useJUnitPlatform { useJUnitPlatform {
includeTags 'importOfficeData' excludeTags 'importHostingAssets', 'scenarioTest', 'generalIntegrationTest',
'officeIntegrationTest', 'bookingIntegrationTest', 'hostingIntegrationTest'
} }
group 'verification' group 'verification'
description 'run the import jobs as tests' description 'runs all unit-tests which do not need a database'
mustRunAfter spotlessJava
}
// HOWTO: run all integration tests which are not specific to a module, like base, rbac, config etc.
tasks.register('generalIntegrationTest', Test) {
useJUnitPlatform {
includeTags 'generalIntegrationTest'
}
group 'verification'
description 'runs integration tests which are not specific to a module, like base, rbac, config etc.'
mustRunAfter spotlessJava
}
// HOWTO: run all integration tests of the office module: gw-test officeIntegrationTest
tasks.register('officeIntegrationTest', Test) {
useJUnitPlatform {
includeTags 'officeIntegrationTest'
}
group 'verification'
description 'runs integration tests of the office module'
mustRunAfter spotlessJava
}
// HOWTO: run all integration tests of the booking module: gw-test bookingIntegrationTest
tasks.register('bookingIntegrationTest', Test) {
useJUnitPlatform {
includeTags 'bookingIntegrationTest'
}
group 'verification'
description 'runs integration tests of the booking module'
mustRunAfter spotlessJava
}
// HOWTO: run all integration tests of the hosting module: gw-test hostingIntegrationTest
tasks.register('hostingIntegrationTest', Test) {
useJUnitPlatform {
includeTags 'hostingIntegrationTest'
}
group 'verification'
description 'runs integration tests of the hosting module'
mustRunAfter spotlessJava mustRunAfter spotlessJava
} }
@ -346,7 +407,7 @@ tasks.register('importHostingAssets', Test) {
mustRunAfter spotlessJava mustRunAfter spotlessJava
} }
tasks.register('scenarioTests', Test) { tasks.register('scenarioTest', Test) {
useJUnitPlatform { useJUnitPlatform {
includeTags 'scenarioTest' includeTags 'scenarioTest'
} }
@ -367,7 +428,7 @@ pitest {
] ]
targetTests = ['net.hostsharing.hsadminng.**.*UnitTest', 'net.hostsharing.hsadminng.**.*RestTest'] targetTests = ['net.hostsharing.hsadminng.**.*UnitTest', 'net.hostsharing.hsadminng.**.*RestTest']
excludedTestClasses = ['**AcceptanceTest*', '**IntegrationTest*'] excludedTestClasses = ['**AcceptanceTest*', '**IntegrationTest*', '**ImportHostingAssets']
pitestVersion = '1.17.0' pitestVersion = '1.17.0'
junit5PluginVersion = '1.1.0' junit5PluginVersion = '1.1.0'
@ -382,7 +443,7 @@ pitest {
outputFormats = ['XML', 'HTML'] outputFormats = ['XML', 'HTML']
timestampedReports = false timestampedReports = false
} }
project.tasks.check.dependsOn(project.tasks.pitest) // project.tasks.check.dependsOn(project.tasks.pitest) TODO.test: PiTest currently does not work, needs to be fixed
project.tasks.pitest.doFirst { // Why not doLast? See README.md! project.tasks.pitest.doFirst { // Why not doLast? See README.md!
println "PiTest Mutation Report: file:///${project.rootDir}/build/reports/pitest/index.html" println "PiTest Mutation Report: file:///${project.rootDir}/build/reports/pitest/index.html"
} }
@ -446,7 +507,7 @@ tasks.register('convertMarkdownToHtml') {
} }
} }
} }
convertMarkdownToHtml.dependsOn scenarioTests convertMarkdownToHtml.dependsOn scenarioTest
// shortcut for compiling all files // shortcut for compiling all files
tasks.register('compile') { tasks.register('compile') {

View File

@ -50,6 +50,7 @@ classDiagram
UNKNOWN: nur für Import UNKNOWN: nur für Import
NATURAL_PERSON: natürliche Person NATURAL_PERSON: natürliche Person
LEGAL_PERSON: z.B. GmbH, e.K., eG, e.V. LEGAL_PERSON: z.B. GmbH, e.K., eG, e.V.
ORGANIZATIONAL_UNIT: z.B. "Admin-Team", "Buchhaltung"
INCORORATED_FIRM: z.B. OHG, Partnerschaftsgesellschaft INCORORATED_FIRM: z.B. OHG, Partnerschaftsgesellschaft
UNINCORPORATED_FIRM: z.B. GbR, ARGE, Erbengemeinschaft UNINCORPORATED_FIRM: z.B. GbR, ARGE, Erbengemeinschaft
PUBLIC_INSTITUTION: KdöR, AöR [ohne Registergericht/Registernummer] PUBLIC_INSTITUTION: KdöR, AöR [ohne Registergericht/Registernummer]

View File

@ -5,9 +5,23 @@
{ "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 v2.0" },
{ "moduleLicense": "Apache License Version 2.0" },
{ "moduleLicense": "Apache License, Version 2.0" }, { "moduleLicense": "Apache License, Version 2.0" },
{ "moduleLicense": "The Apache License, Version 2.0" },
{ "moduleLicense": "The Apache Software License, Version 2.0" }, { "moduleLicense": "The Apache Software License, Version 2.0" },
{
"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"
},
{
"moduleLicense": null,
"moduleVersion": "1.0.0",
"moduleName": "org.jspecify:jspecify"
},
{ "moduleLicense": "BSD License" }, { "moduleLicense": "BSD License" },
{ "moduleLicense": "BSD-2-Clause" }, { "moduleLicense": "BSD-2-Clause" },
{ "moduleLicense": "BSD-3-Clause" }, { "moduleLicense": "BSD-3-Clause" },
@ -46,14 +60,8 @@
{ {
"moduleLicense": "Public Domain, per Creative Commons CC0", "moduleLicense": "Public Domain, per Creative Commons CC0",
"moduleVersion": "2.0.3" "moduleVersion": "2.0.3"
},
{
"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

@ -9,8 +9,12 @@
</suppress> </suppress>
<suppress> <suppress>
<notes><![CDATA[ <notes><![CDATA[
Malicious HTTP redirect in JAXB on a REST-endpoint is not that dangerous. file name: logback-core-1.5.12.jar
A successful attack requires the user to have write access to a configuration file or environment vars.
]]></notes> ]]></notes>
<cve>CVE-2024-9329</cve> <packageUrl regex="true">^pkg:maven/ch\.qos\.logback/logback-core@.*$</packageUrl>
<cpe>cpe:/a:qos:logback</cpe>
<cve>CVE-2024-12798</cve>
</suppress> </suppress>
</suppressions> </suppressions>

View File

@ -0,0 +1,105 @@
package net.hostsharing.hsadminng.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.endpoint.SanitizableData;
import org.springframework.boot.actuate.endpoint.SanitizingFunction;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
// HOWTO: exclude sensitive values, like passwords and other secrets, from being show by actuator endpoints:
// either use: add your custom keys to management.endpoint.additionalKeysToSanitize,
// or, if you need more heuristics, amend this code down here.
@Component
public class ActuatorSanitizer implements SanitizingFunction {
private static final String[] REGEX_PARTS = {"*", "$", "^", "+"};
private static final Set<String> DEFAULT_KEYS_TO_SANITIZE = Set.of(
"password", "secret", "token", ".*credentials.*", "vcap_services", "^vcap\\.services.*$", "sun.java.command", "^spring[._]application[._]json$"
);
private static final Set<String> URI_USERINFO_KEYS = Set.of(
"uri", "uris", "url", "urls", "address", "addresses"
);
private static final Pattern URI_USERINFO_PATTERN = Pattern.compile("^\\[?[A-Za-z][A-Za-z0-9\\+\\.\\-]+://.+:(.*)@.+$");
private final List<Pattern> keysToSanitize = new ArrayList<>();
public ActuatorSanitizer(@Value("${management.endpoint.additionalKeysToSanitize:}") final List<String> additionalKeysToSanitize) {
addKeysToSanitize(DEFAULT_KEYS_TO_SANITIZE);
addKeysToSanitize(URI_USERINFO_KEYS);
addKeysToSanitize(additionalKeysToSanitize);
}
@Override
public SanitizableData apply(final SanitizableData data) {
if (data.getValue() == null) {
return data;
}
for (final Pattern pattern : keysToSanitize) {
if (pattern.matcher(data.getKey()).matches()) {
if (keyIsUriWithUserInfo(pattern)) {
return data.withValue(sanitizeUris(data.getValue().toString()));
}
return data.withValue(SanitizableData.SANITIZED_VALUE);
}
}
return data;
}
private void addKeysToSanitize(final Collection<String> keysToSanitize) {
for (final String key : keysToSanitize) {
this.keysToSanitize.add(getPattern(key));
}
}
private Pattern getPattern(final String value) {
if (isRegex(value)) {
return Pattern.compile(value, Pattern.CASE_INSENSITIVE);
}
return Pattern.compile(".*" + value + "$", Pattern.CASE_INSENSITIVE);
}
private boolean isRegex(final String value) {
for (final String part : REGEX_PARTS) {
if (value.contains(part)) {
return true;
}
}
return false;
}
private boolean keyIsUriWithUserInfo(final Pattern pattern) {
for (String uriKey : URI_USERINFO_KEYS) {
if (pattern.matcher(uriKey).matches()) {
return true;
}
}
return false;
}
private Object sanitizeUris(final String value) {
return Arrays.stream(value.split(",")).map(this::sanitizeUri).collect(Collectors.joining(","));
}
private String sanitizeUri(final String value) {
final var matcher = URI_USERINFO_PATTERN.matcher(value);
final var password = matcher.matches() ? matcher.group(1) : null;
if (password != null) {
return StringUtils.replace(value, ":" + password + "@", ":" + SanitizableData.SANITIZED_VALUE + "@");
}
return value;
}
}

View File

@ -0,0 +1,54 @@
package net.hostsharing.hsadminng.config;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import java.util.*;
public class AuthenticatedHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final Map<String, String> customHeaders = new HashMap<>();
public AuthenticatedHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
public void addHeader(final String name, final String value) {
customHeaders.put(name, value);
}
@Override
public String getHeader(final String name) {
// Check custom headers first
final var customHeaderValue = customHeaders.get(name);
if (customHeaderValue != null) {
return customHeaderValue;
}
// Fall back to the original headers
return super.getHeader(name);
}
@Override
public Enumeration<String> getHeaderNames() {
// Combine original headers and custom headers
final var headerNames = new HashSet<>(customHeaders.keySet());
final var originalHeaderNames = super.getHeaderNames();
while (originalHeaderNames.hasMoreElements()) {
headerNames.add(originalHeaderNames.nextElement());
}
return Collections.enumeration(headerNames);
}
@Override
public Enumeration<String> getHeaders(final String name) {
// Combine original headers and custom header
final var values = new HashSet<String>();
if (customHeaders.containsKey(name)) {
values.add(customHeaders.get(name));
}
final var originalValues = super.getHeaders(name);
while (originalValues.hasMoreElements()) {
values.add(originalValues.nextElement());
}
return Collections.enumeration(values);
}
}

View File

@ -0,0 +1,39 @@
package net.hostsharing.hsadminng.config;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.stereotype.Component;
@Component
public class AuthenticationFilter implements Filter {
@Autowired
private Authenticator authenticator;
@Override
@SneakyThrows
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) {
final var httpRequest = (HttpServletRequest) request;
final var httpResponse = (HttpServletResponse) response;
try {
final var currentSubject = authenticator.authenticate(httpRequest);
final var authenticatedRequest = new AuthenticatedHttpServletRequestWrapper(httpRequest);
authenticatedRequest.addHeader("current-subject", currentSubject);
chain.doFilter(authenticatedRequest, response);
} catch (final BadCredentialsException exc) {
// TODO.impl: should not be necessary if ResponseStatusException worked
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
}
}

View File

@ -0,0 +1,8 @@
package net.hostsharing.hsadminng.config;
import jakarta.servlet.http.HttpServletRequest;
public interface Authenticator {
String authenticate(final HttpServletRequest httpRequest);
}

View File

@ -0,0 +1,71 @@
package net.hostsharing.hsadminng.config;
import io.micrometer.core.annotation.Timed;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.client.RestTemplate;
import org.xml.sax.SAXException;
import jakarta.servlet.http.HttpServletRequest;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
public class CasAuthenticator implements Authenticator {
@Value("${hsadminng.cas.server}")
private String casServerUrl;
@Value("${hsadminng.cas.service}")
private String serviceUrl;
private final RestTemplate restTemplate = new RestTemplate();
@SneakyThrows
@Timed("app.cas.authenticate")
public String authenticate(final HttpServletRequest httpRequest) {
final var userName = StringUtils.isBlank(casServerUrl)
? bypassCurrentSubject(httpRequest)
: casValidation(httpRequest);
final var authentication = new UsernamePasswordAuthenticationToken(userName, null, null);
SecurityContextHolder.getContext().setAuthentication(authentication);
return authentication.getName();
}
private static String bypassCurrentSubject(final HttpServletRequest httpRequest) {
final var userName = httpRequest.getHeader("current-subject");
System.err.println("CasAuthenticator.bypassCurrentSubject: " + userName);
return userName;
}
private String casValidation(final HttpServletRequest httpRequest)
throws SAXException, IOException, ParserConfigurationException {
final var ticket = httpRequest.getHeader("Authorization");
final var url = casServerUrl + "/p3/serviceValidate" +
"?service=" + serviceUrl +
"&ticket=" + ticket;
System.err.println("CasAuthenticator.casValidation using URL: " + url);
final var response = restTemplate.getForObject(url, String.class);
final var doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
.parse(new java.io.ByteArrayInputStream(response.getBytes()));
if (doc.getElementsByTagName("cas:authenticationSuccess").getLength() == 0) {
// TODO.impl: for unknown reasons, this results in a 403 FORBIDDEN
// throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "CAS service ticket could not be validated");
System.err.println("CAS service ticket could not be validated");
System.err.println("CAS-validation-URL: " + url);
System.err.println(response);
throw new BadCredentialsException("CAS service ticket could not be validated");
}
final var userName = doc.getElementsByTagName("cas:user").item(0).getTextContent();
System.err.println("CAS-user: " + userName);
return userName;
}
}

View File

@ -5,6 +5,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
@Configuration @Configuration
@ -22,6 +23,14 @@ public class WebSecurityConfig {
.requestMatchers("/actuator/**").permitAll() .requestMatchers("/actuator/**").permitAll()
.anyRequest().authenticated() .anyRequest().authenticated()
) )
.csrf(AbstractHttpConfigurer::disable)
.build(); .build();
} }
@Bean
@Profile("!test")
public Authenticator casServiceTicketValidator() {
return new CasAuthenticator();
}
} }

View File

@ -58,7 +58,7 @@ public class Context {
cast(:currentTask as varchar(127)), cast(:currentTask as varchar(127)),
cast(:currentRequest as text), cast(:currentRequest as text),
cast(:currentSubject as varchar(63)), cast(:currentSubject as varchar(63)),
cast(:assumedRoles as varchar(1023))); cast(:assumedRoles as text));
"""); """);
query.setParameter("currentTask", shortenToMaxLength(currentTask, 127)); query.setParameter("currentTask", shortenToMaxLength(currentTask, 127));
query.setParameter("currentRequest", currentRequest); query.setParameter("currentRequest", currentRequest);

View File

@ -97,6 +97,7 @@ public class RestResponseEntityExceptionHandler
return errorResponse(request, HttpStatus.valueOf(statusCode.value()), return errorResponse(request, HttpStatus.valueOf(statusCode.value()),
Optional.ofNullable(response.getBody()).map(Object::toString).orElse(firstMessageLine(exc))); Optional.ofNullable(response.getBody()).map(Object::toString).orElse(firstMessageLine(exc)));
} }
@Override @Override
@SuppressWarnings("unchecked,rawtypes") @SuppressWarnings("unchecked,rawtypes")
protected ResponseEntity handleHttpMessageNotReadable( protected ResponseEntity handleHttpMessageNotReadable(
@ -131,7 +132,7 @@ public class RestResponseEntityExceptionHandler
final HttpStatusCode status, final HttpStatusCode status,
final WebRequest request) { final WebRequest request) {
final var errorList = exc final var errorList = exc
.getAllValidationResults() .getParameterValidationResults()
.stream() .stream()
.map(ParameterValidationResult::getResolvableErrors) .map(ParameterValidationResult::getResolvableErrors)
.flatMap(Collection::stream) .flatMap(Collection::stream)

View File

@ -5,6 +5,7 @@ import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import net.hostsharing.hsadminng.errors.DisplayAs; import net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.rbac.role.WithRoleId;
import net.hostsharing.hsadminng.repr.Stringify; import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable; import net.hostsharing.hsadminng.repr.Stringifyable;
@ -24,7 +25,7 @@ import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@DisplayAs("BookingDebitor") @DisplayAs("BookingDebitor")
public class HsBookingDebitorEntity implements Stringifyable { public class HsBookingDebitorEntity implements Stringifyable, WithRoleId {
public static final String DEBITOR_NUMBER_TAG = "D-"; public static final String DEBITOR_NUMBER_TAG = "D-";

View File

@ -2,11 +2,13 @@ package net.hostsharing.hsadminng.hs.booking.debitor;
import io.micrometer.core.annotation.Timed; import io.micrometer.core.annotation.Timed;
import org.springframework.data.repository.Repository; import org.springframework.data.repository.Repository;
import org.springframework.context.annotation.Profile;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@Profile("!only-office")
public interface HsBookingDebitorRepository extends Repository<HsBookingDebitorEntity, UUID> { public interface HsBookingDebitorRepository extends Repository<HsBookingDebitorEntity, UUID> {
@Timed("app.booking.debitor.repo.findByUuid") @Timed("app.booking.debitor.repo.findByUuid")

View File

@ -1,10 +1,12 @@
package net.hostsharing.hsadminng.hs.booking.item; package net.hostsharing.hsadminng.hs.booking.item;
import io.micrometer.core.annotation.Timed; import io.micrometer.core.annotation.Timed;
import org.springframework.context.annotation.Profile;
import org.springframework.data.repository.Repository; import org.springframework.data.repository.Repository;
import java.util.UUID; import java.util.UUID;
@Profile("!only-office")
public interface BookingItemCreatedEventRepository extends Repository<BookingItemCreatedEventEntity, UUID> { public interface BookingItemCreatedEventRepository extends Repository<BookingItemCreatedEventEntity, UUID> {
@Timed("app.booking.items.repo.save") @Timed("app.booking.items.repo.save")

View File

@ -16,6 +16,7 @@ import net.hostsharing.hsadminng.mapper.StrictMapper;
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.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Profile;
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;
@ -30,6 +31,7 @@ import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
@RestController @RestController
@Profile("!only-office")
public class HsBookingItemController implements HsBookingItemsApi { public class HsBookingItemController implements HsBookingItemsApi {
@Autowired @Autowired

View File

@ -5,8 +5,8 @@ import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRbacEntity; import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRbacEntity;
import net.hostsharing.hsadminng.rbac.generator.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL; import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
import jakarta.persistence.AttributeOverride; import jakarta.persistence.AttributeOverride;
import jakarta.persistence.AttributeOverrides; import jakarta.persistence.AttributeOverrides;
@ -15,20 +15,20 @@ 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.generator.RbacView.Column.dependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.GLOBAL;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NULLABLE; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Nullable.NULLABLE;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.DELETE; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.DELETE;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.INSERT; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.SELECT; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.UPDATE; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.UPDATE;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.AGENT; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.AGENT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.OWNER; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.OWNER;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.TENANT; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.TENANT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
@Entity @Entity
@Table(schema = "hs_booking", name = "item_rv") @Table(schema = "hs_booking", name = "item_rv")
@ -41,7 +41,7 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
}) })
public class HsBookingItemRbacEntity extends HsBookingItem { public class HsBookingItemRbacEntity extends HsBookingItem {
public static RbacView rbac() { public static RbacSpec rbac() {
return rbacViewFor("bookingItem", HsBookingItemRbacEntity.class) return rbacViewFor("bookingItem", HsBookingItemRbacEntity.class)
.withIdentityView(SQL.projection("caption")) .withIdentityView(SQL.projection("caption"))
.withRestrictedViewOrderBy(SQL.expression("validity")) .withRestrictedViewOrderBy(SQL.expression("validity"))

View File

@ -1,12 +1,14 @@
package net.hostsharing.hsadminng.hs.booking.item; package net.hostsharing.hsadminng.hs.booking.item;
import io.micrometer.core.annotation.Timed; import io.micrometer.core.annotation.Timed;
import org.springframework.context.annotation.Profile;
import org.springframework.data.repository.Repository; import org.springframework.data.repository.Repository;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@Profile("!only-office")
public interface HsBookingItemRbacRepository extends HsBookingItemRepository<HsBookingItemRbacEntity>, public interface HsBookingItemRbacRepository extends HsBookingItemRepository<HsBookingItemRbacEntity>,
Repository<HsBookingItemRbacEntity, UUID> { Repository<HsBookingItemRbacEntity, UUID> {

View File

@ -1,12 +1,14 @@
package net.hostsharing.hsadminng.hs.booking.item; package net.hostsharing.hsadminng.hs.booking.item;
import io.micrometer.core.annotation.Timed; import io.micrometer.core.annotation.Timed;
import org.springframework.context.annotation.Profile;
import org.springframework.data.repository.Repository; import org.springframework.data.repository.Repository;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@Profile("!only-office")
public interface HsBookingItemRealRepository extends HsBookingItemRepository<HsBookingItemRealEntity>, public interface HsBookingItemRealRepository extends HsBookingItemRepository<HsBookingItemRealEntity>,
Repository<HsBookingItemRealEntity, UUID> { Repository<HsBookingItemRealEntity, UUID> {

View File

@ -1,10 +1,13 @@
package net.hostsharing.hsadminng.hs.booking.item; package net.hostsharing.hsadminng.hs.booking.item;
import org.springframework.context.annotation.Profile;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@Profile("!only-office")
public interface HsBookingItemRepository<E extends HsBookingItem> { public interface HsBookingItemRepository<E extends HsBookingItem> {
Optional<E> findByUuid(final UUID bookingItemUuid); Optional<E> findByUuid(final UUID bookingItemUuid);

View File

@ -7,8 +7,9 @@ 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.StandardMapper; import net.hostsharing.hsadminng.mapper.StrictMapper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
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;
@ -20,13 +21,14 @@ import java.util.UUID;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
@RestController @RestController
@Profile("!only-office")
public class HsBookingProjectController implements HsBookingProjectsApi { public class HsBookingProjectController implements HsBookingProjectsApi {
@Autowired @Autowired
private Context context; private Context context;
@Autowired @Autowired
private StandardMapper mapper; private StrictMapper mapper;
@Autowired @Autowired
private HsBookingProjectRbacRepository bookingProjectRepo; private HsBookingProjectRbacRepository bookingProjectRepo;

View File

@ -6,30 +6,30 @@ 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.generator.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL; import net.hostsharing.hsadminng.rbac.generator.RbacSpec.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.generator.RbacView.Column.dependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingCase; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingCase;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.GLOBAL;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NOT_NULL; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.DELETE; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.DELETE;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.INSERT; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.SELECT; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.UPDATE; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.UPDATE;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.AGENT; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.AGENT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.OWNER; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.OWNER;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.TENANT; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.TENANT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.fetchedBySql; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.fetchedBySql;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
@Entity @Entity
@Table(schema = "hs_booking", name = "project_rv") @Table(schema = "hs_booking", name = "project_rv")
@ -39,7 +39,7 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
@NoArgsConstructor @NoArgsConstructor
public class HsBookingProjectRbacEntity extends HsBookingProject { public class HsBookingProjectRbacEntity extends HsBookingProject {
public static RbacView rbac() { public static RbacSpec rbac() {
return rbacViewFor("project", HsBookingProjectRbacEntity.class) return rbacViewFor("project", HsBookingProjectRbacEntity.class)
.withIdentityView(SQL.query(""" .withIdentityView(SQL.query("""
SELECT bookingProject.uuid as uuid, debitorIV.idName || '-' || base.cleanIdentifier(bookingProject.caption) as idName SELECT bookingProject.uuid as uuid, debitorIV.idName || '-' || base.cleanIdentifier(bookingProject.caption) as idName

View File

@ -1,12 +1,14 @@
package net.hostsharing.hsadminng.hs.booking.project; package net.hostsharing.hsadminng.hs.booking.project;
import io.micrometer.core.annotation.Timed; import io.micrometer.core.annotation.Timed;
import org.springframework.context.annotation.Profile;
import org.springframework.data.repository.Repository; import org.springframework.data.repository.Repository;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@Profile("!only-office")
public interface HsBookingProjectRbacRepository extends HsBookingProjectRepository<HsBookingProjectRbacEntity>, public interface HsBookingProjectRbacRepository extends HsBookingProjectRepository<HsBookingProjectRbacEntity>,
Repository<HsBookingProjectRbacEntity, UUID> { Repository<HsBookingProjectRbacEntity, UUID> {

View File

@ -1,12 +1,14 @@
package net.hostsharing.hsadminng.hs.booking.project; package net.hostsharing.hsadminng.hs.booking.project;
import io.micrometer.core.annotation.Timed; import io.micrometer.core.annotation.Timed;
import org.springframework.context.annotation.Profile;
import org.springframework.data.repository.Repository; import org.springframework.data.repository.Repository;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@Profile("!only-office")
public interface HsBookingProjectRealRepository extends HsBookingProjectRepository<HsBookingProjectRealEntity>, public interface HsBookingProjectRealRepository extends HsBookingProjectRepository<HsBookingProjectRealEntity>,
Repository<HsBookingProjectRealEntity, UUID> { Repository<HsBookingProjectRealEntity, UUID> {

View File

@ -1,11 +1,13 @@
package net.hostsharing.hsadminng.hs.booking.project; package net.hostsharing.hsadminng.hs.booking.project;
import io.micrometer.core.annotation.Timed; import io.micrometer.core.annotation.Timed;
import org.springframework.context.annotation.Profile;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@Profile("!only-office")
public interface HsBookingProjectRepository<E extends HsBookingProject> { public interface HsBookingProjectRepository<E extends HsBookingProject> {
@Timed("app.booking.projects.repo.findByUuid") @Timed("app.booking.projects.repo.findByUuid")

View File

@ -89,7 +89,7 @@ public abstract class HsHostingAsset implements Stringifyable, BaseEntity<HsHost
@JoinColumn(name = "alarmcontactuuid") @JoinColumn(name = "alarmcontactuuid")
private HsOfficeContactRealEntity alarmContact; private HsOfficeContactRealEntity alarmContact;
@OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true, fetch = FetchType.LAZY) @OneToMany(cascade = { CascadeType.PERSIST, CascadeType.REFRESH }, orphanRemoval = true, fetch = FetchType.LAZY)
@JoinColumn(name = "parentassetuuid", referencedColumnName = "uuid") @JoinColumn(name = "parentassetuuid", referencedColumnName = "uuid")
private List<HsHostingAssetRealEntity> subHostingAssets; private List<HsHostingAssetRealEntity> subHostingAssets;

View File

@ -12,9 +12,10 @@ 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.StandardMapper; import net.hostsharing.hsadminng.mapper.StrictMapper;
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.context.annotation.Profile;
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;
@ -27,6 +28,7 @@ import java.util.UUID;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
@RestController @RestController
@Profile("!only-office")
public class HsHostingAssetController implements HsHostingAssetsApi { public class HsHostingAssetController implements HsHostingAssetsApi {
@Autowired @Autowired
@ -36,7 +38,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
private Context context; private Context context;
@Autowired @Autowired
private StandardMapper mapper; private StrictMapper mapper;
@Autowired @Autowired
private HsHostingAssetRbacRepository rbacAssetRepo; private HsHostingAssetRbacRepository rbacAssetRepo;

View File

@ -4,6 +4,7 @@ import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntityValidatorRegistry; import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntityValidatorRegistry;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetPropsApi; import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetPropsApi;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource; import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource;
import org.springframework.context.annotation.Profile;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@ -12,6 +13,7 @@ import java.util.Map;
@RestController @RestController
@Profile("!only-office")
public class HsHostingAssetPropsController implements HsHostingAssetPropsApi { public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
@Override @Override

View File

@ -6,31 +6,31 @@ import lombok.Setter;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRbacEntity; 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.generator.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL; import net.hostsharing.hsadminng.rbac.generator.RbacSpec.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.generator.RbacView.CaseDef.inCaseOf; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.CaseDef.inCaseOf;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Column.dependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.GLOBAL;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NULLABLE; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Nullable.NULLABLE;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.DELETE; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.DELETE;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.INSERT; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.SELECT; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.UPDATE; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.UPDATE;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.RbacSubjectReference.UserRole.CREATOR;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.AGENT; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.AGENT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.GUEST; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.GUEST;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.OWNER; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.OWNER;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.REFERRER; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.REFERRER;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.TENANT; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.TENANT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
@Entity @Entity
@Table(schema = "hs_hosting", name = "asset_rv") @Table(schema = "hs_hosting", name = "asset_rv")
@ -40,7 +40,7 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
@NoArgsConstructor @NoArgsConstructor
public class HsHostingAssetRbacEntity extends HsHostingAsset { public class HsHostingAssetRbacEntity extends HsHostingAsset {
public static RbacView rbac() { public static RbacSpec rbac() {
return rbacViewFor("asset", HsHostingAssetRbacEntity.class) return rbacViewFor("asset", HsHostingAssetRbacEntity.class)
.withIdentityView(SQL.projection("identifier")) .withIdentityView(SQL.projection("identifier"))
.withRestrictedViewOrderBy(SQL.expression("identifier")) .withRestrictedViewOrderBy(SQL.expression("identifier"))

View File

@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.hs.hosting.asset; package net.hostsharing.hsadminng.hs.hosting.asset;
import io.micrometer.core.annotation.Timed; import io.micrometer.core.annotation.Timed;
import org.springframework.context.annotation.Profile;
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;
@ -8,7 +9,7 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@Profile("!only-office")
public interface HsHostingAssetRbacRepository extends HsHostingAssetRepository<HsHostingAssetRbacEntity>, Repository<HsHostingAssetRbacEntity, UUID> { public interface HsHostingAssetRbacRepository extends HsHostingAssetRepository<HsHostingAssetRbacEntity>, Repository<HsHostingAssetRbacEntity, UUID> {
@Timed("app.hostingAsset.repo.findByUuid.rbac") @Timed("app.hostingAsset.repo.findByUuid.rbac")

View File

@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.hs.hosting.asset; package net.hostsharing.hsadminng.hs.hosting.asset;
import io.micrometer.core.annotation.Timed; import io.micrometer.core.annotation.Timed;
import org.springframework.context.annotation.Profile;
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;
@ -9,6 +10,7 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@Profile("!only-office")
public interface HsHostingAssetRealRepository extends HsHostingAssetRepository<HsHostingAssetRealEntity>, Repository<HsHostingAssetRealEntity, UUID> { public interface HsHostingAssetRealRepository extends HsHostingAssetRepository<HsHostingAssetRealEntity>, Repository<HsHostingAssetRealEntity, UUID> {
@Timed("app.hostingAsset.repo.findByUuid.real") @Timed("app.hostingAsset.repo.findByUuid.real")

View File

@ -1,11 +1,13 @@
package net.hostsharing.hsadminng.hs.hosting.asset; package net.hostsharing.hsadminng.hs.hosting.asset;
import io.micrometer.core.annotation.Timed; import io.micrometer.core.annotation.Timed;
import org.springframework.context.annotation.Profile;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@Profile("!only-office")
public interface HsHostingAssetRepository<E extends HsHostingAsset> { public interface HsHostingAssetRepository<E extends HsHostingAsset> {
@Timed("app.hosting.assets.repo.findByUuid") @Timed("app.hosting.assets.repo.findByUuid")

View File

@ -9,7 +9,7 @@ import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
import net.hostsharing.hsadminng.lambda.Reducer; import net.hostsharing.hsadminng.lambda.Reducer;
import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.mapper.ToStringConverter; import net.hostsharing.hsadminng.mapper.ToStringConverter;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
@ -31,8 +31,8 @@ public class DomainSetupHostingAssetFactory extends HostingAssetFactory {
final EntityManagerWrapper emw, final EntityManagerWrapper emw,
final HsBookingItemRealEntity newBookingItemRealEntity, final HsBookingItemRealEntity newBookingItemRealEntity,
final HsHostingAssetAutoInsertResource asset, final HsHostingAssetAutoInsertResource asset,
final StandardMapper standardMapper) { final StrictMapper StrictMapper) {
super(emw, newBookingItemRealEntity, asset, standardMapper); super(emw, newBookingItemRealEntity, asset, StrictMapper);
} }
@Override @Override

View File

@ -6,7 +6,7 @@ import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAsse
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntitySaveProcessor; import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntitySaveProcessor;
import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
@ -16,7 +16,7 @@ abstract class HostingAssetFactory {
final EntityManagerWrapper emw; final EntityManagerWrapper emw;
final HsBookingItemRealEntity fromBookingItem; final HsBookingItemRealEntity fromBookingItem;
final HsHostingAssetAutoInsertResource asset; final HsHostingAssetAutoInsertResource asset;
final StandardMapper standardMapper; final StrictMapper StrictMapper;
protected abstract HsHostingAsset create(); protected abstract HsHostingAsset create();

View File

@ -9,13 +9,15 @@ import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAsse
import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedAppEvent; import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedAppEvent;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.mapper.StrictMapper;
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.context.ApplicationListener; import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
@Profile("!only-office")
public class HsBookingItemCreatedListener implements ApplicationListener<BookingItemCreatedAppEvent> { public class HsBookingItemCreatedListener implements ApplicationListener<BookingItemCreatedAppEvent> {
@Autowired @Autowired
@ -25,7 +27,7 @@ public class HsBookingItemCreatedListener implements ApplicationListener<Booking
private ObjectMapper jsonMapper; private ObjectMapper jsonMapper;
@Autowired @Autowired
private StandardMapper standardMapper; private StrictMapper StrictMapper;
@Override @Override
@SneakyThrows @SneakyThrows
@ -44,9 +46,9 @@ public class HsBookingItemCreatedListener implements ApplicationListener<Booking
final var asset = jsonMapper.readValue(event.getEntity().getAssetJson(), HsHostingAssetAutoInsertResource.class); final var asset = jsonMapper.readValue(event.getEntity().getAssetJson(), HsHostingAssetAutoInsertResource.class);
final var factory = switch (newBookingItemRealEntity.getType()) { final var factory = switch (newBookingItemRealEntity.getType()) {
case PRIVATE_CLOUD, CLOUD_SERVER, MANAGED_SERVER -> case PRIVATE_CLOUD, CLOUD_SERVER, MANAGED_SERVER ->
forNowNoAutomaticHostingAssetCreationPossible(emw, newBookingItemRealEntity, asset, standardMapper); forNowNoAutomaticHostingAssetCreationPossible(emw, newBookingItemRealEntity, asset, StrictMapper);
case MANAGED_WEBSPACE -> new ManagedWebspaceHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper); case MANAGED_WEBSPACE -> new ManagedWebspaceHostingAssetFactory(emw, newBookingItemRealEntity, asset, StrictMapper);
case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper); case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(emw, newBookingItemRealEntity, asset, StrictMapper);
}; };
if (factory != null) { if (factory != null) {
final var statusMessage = factory.createAndPersist(); final var statusMessage = factory.createAndPersist();
@ -62,9 +64,9 @@ public class HsBookingItemCreatedListener implements ApplicationListener<Booking
final EntityManagerWrapper emw, final EntityManagerWrapper emw,
final HsBookingItemRealEntity fromBookingItem, final HsBookingItemRealEntity fromBookingItem,
final HsHostingAssetAutoInsertResource asset, final HsHostingAssetAutoInsertResource asset,
final StandardMapper standardMapper final StrictMapper StrictMapper
) { ) {
return new HostingAssetFactory(emw, fromBookingItem, asset, standardMapper) { return new HostingAssetFactory(emw, fromBookingItem, asset, StrictMapper) {
@Override @Override
protected HsHostingAsset create() { protected HsHostingAsset create() {

View File

@ -5,7 +5,7 @@ import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAsse
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity;
import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import jakarta.validation.ValidationException; import jakarta.validation.ValidationException;
@ -19,8 +19,8 @@ public class ManagedWebspaceHostingAssetFactory extends HostingAssetFactory {
final EntityManagerWrapper emw, final EntityManagerWrapper emw,
final HsBookingItemRealEntity newBookingItemRealEntity, final HsBookingItemRealEntity newBookingItemRealEntity,
final HsHostingAssetAutoInsertResource asset, final HsHostingAssetAutoInsertResource asset,
final StandardMapper standardMapper) { final StrictMapper StrictMapper) {
super(emw, newBookingItemRealEntity, asset, standardMapper); super(emw, newBookingItemRealEntity, asset, StrictMapper);
} }
@Override @Override
@ -32,7 +32,7 @@ public class ManagedWebspaceHostingAssetFactory extends HostingAssetFactory {
.map(Enum::name) .map(Enum::name)
.orElse(null)); .orElse(null));
} }
final var managedWebspaceHostingAsset = standardMapper.map(asset, HsHostingAssetRealEntity.class); final var managedWebspaceHostingAsset = StrictMapper.map(asset, HsHostingAssetRealEntity.class);
managedWebspaceHostingAsset.setBookingItem(fromBookingItem); managedWebspaceHostingAsset.setBookingItem(fromBookingItem);
emw.createQuery( emw.createQuery(
"SELECT asset FROM HsHostingAssetRealEntity asset WHERE asset.bookingItem.uuid=:bookingItemUuid", "SELECT asset FROM HsHostingAssetRealEntity asset WHERE asset.bookingItem.uuid=:bookingItemUuid",

View File

@ -5,7 +5,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.StandardMapper; import net.hostsharing.hsadminng.mapper.StrictMapper;
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;
@ -25,7 +25,7 @@ public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
private Context context; private Context context;
@Autowired @Autowired
private StandardMapper mapper; private StrictMapper mapper;
@Autowired @Autowired
private HsOfficeBankAccountRepository bankAccountRepo; private HsOfficeBankAccountRepository bankAccountRepo;

View File

@ -4,7 +4,7 @@ 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.persistence.BaseEntity; import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.generator.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
import net.hostsharing.hsadminng.repr.Stringify; import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable; import net.hostsharing.hsadminng.repr.Stringifyable;
@ -12,10 +12,10 @@ 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.generator.RbacView.*; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.RbacSubjectReference.UserRole.CREATOR;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.*;
import static net.hostsharing.hsadminng.repr.Stringify.stringify; import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@Entity @Entity
@ -57,7 +57,7 @@ public class HsOfficeBankAccountEntity implements BaseEntity<HsOfficeBankAccount
return holder; return holder;
} }
public static RbacView rbac() { public static RbacSpec rbac() {
return rbacViewFor("bankAccount", HsOfficeBankAccountEntity.class) return rbacViewFor("bankAccount", HsOfficeBankAccountEntity.class)
.withIdentityView(SQL.projection("iban")) .withIdentityView(SQL.projection("iban"))
.withUpdatableColumns("holder", "iban", "bic") .withUpdatableColumns("holder", "iban", "bic")

View File

@ -12,6 +12,7 @@ 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.persistence.BaseEntity; import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.role.WithRoleId;
import net.hostsharing.hsadminng.repr.Stringify; import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable; import net.hostsharing.hsadminng.repr.Stringifyable;
import org.hibernate.annotations.GenericGenerator; import org.hibernate.annotations.GenericGenerator;
@ -37,7 +38,7 @@ import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@SuperBuilder(toBuilder = true) @SuperBuilder(toBuilder = true)
@FieldNameConstants @FieldNameConstants
@DisplayAs("Contact") @DisplayAs("Contact")
public class HsOfficeContact implements Stringifyable, BaseEntity<HsOfficeContact> { public class HsOfficeContact implements Stringifyable, BaseEntity<HsOfficeContact>, WithRoleId {
private static Stringify<HsOfficeContact> toString = stringify(HsOfficeContact.class, "contact") private static Stringify<HsOfficeContact> toString = stringify(HsOfficeContact.class, "contact")
.withProp(Fields.caption, HsOfficeContact::getCaption) .withProp(Fields.caption, HsOfficeContact::getCaption)

View File

@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.hs.office.contact; package net.hostsharing.hsadminng.hs.office.contact;
import io.micrometer.core.annotation.Timed; import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.mapper.StrictMapper;
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;
@ -28,7 +28,7 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
private Context context; private Context context;
@Autowired @Autowired
private StandardMapper mapper; private StrictMapper mapper;
@Autowired @Autowired
private HsOfficeContactRbacRepository contactRepo; private HsOfficeContactRbacRepository contactRepo;
@ -131,6 +131,7 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
final BiConsumer<HsOfficeContactInsertResource, HsOfficeContactRbacEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { final BiConsumer<HsOfficeContactInsertResource, HsOfficeContactRbacEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
entity.putPostalAddress(from(resource.getPostalAddress()));
entity.putEmailAddresses(from(resource.getEmailAddresses())); entity.putEmailAddresses(from(resource.getEmailAddresses()));
entity.putPhoneNumbers(from(resource.getPhoneNumbers())); entity.putPhoneNumbers(from(resource.getPhoneNumbers()));
}; };

View File

@ -3,17 +3,17 @@ 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.generator.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL; import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.io.IOException; import java.io.IOException;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.GLOBAL;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.RbacSubjectReference.UserRole.CREATOR;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
@Entity @Entity
@Table(schema = "hs_office", name = "contact_rv") @Table(schema = "hs_office", name = "contact_rv")
@ -24,7 +24,7 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
@DisplayAs("RbacContact") @DisplayAs("RbacContact")
public class HsOfficeContactRbacEntity extends HsOfficeContact { public class HsOfficeContactRbacEntity extends HsOfficeContact {
public static RbacView rbac() { public static RbacSpec rbac() {
return rbacViewFor("contact", HsOfficeContactRbacEntity.class) return rbacViewFor("contact", HsOfficeContactRbacEntity.class)
.withIdentityView(SQL.projection("caption")) .withIdentityView(SQL.projection("caption"))
.withUpdatableColumns("caption", "postalAddress", "emailAddresses", "phoneNumbers") .withUpdatableColumns("caption", "postalAddress", "emailAddresses", "phoneNumbers")

View File

@ -1,4 +1,3 @@
package net.hostsharing.hsadminng.hs.office.coopassets; package net.hostsharing.hsadminng.hs.office.coopassets;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@ -9,28 +8,39 @@ 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.persistence.BaseEntity; import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.generator.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
import net.hostsharing.hsadminng.repr.Stringify; import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable; import net.hostsharing.hsadminng.repr.Stringifyable;
import org.hibernate.annotations.GenericGenerator;
import jakarta.persistence.*; import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.UUID; import java.util.UUID;
import static java.util.Optional.ofNullable; import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Column.dependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NOT_NULL; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.INSERT; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.SELECT; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.UPDATE; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.UPDATE;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.AGENT; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.AGENT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
import static net.hostsharing.hsadminng.repr.Stringify.stringify; import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@Entity @Entity
@ -57,8 +67,7 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, BaseE
.quotedValues(false); .quotedValues(false);
@Id @Id
@GeneratedValue(generator = "UUID") @GeneratedValue
@GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
private UUID uuid; private UUID uuid;
@Version @Version
@ -122,15 +131,15 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, BaseE
return this; return this;
} }
public String getTaggedMemberNumber() {
return ofNullable(membership).map(HsOfficeMembershipEntity::toShortString).orElse("M-???????");
}
@Override @Override
public String toString() { public String toString() {
return stringify.apply(this); return stringify.apply(this);
} }
public String getTaggedMemberNumber() {
return ofNullable(membership).map(HsOfficeMembershipEntity::toShortString).orElse("M-???????");
}
@Override @Override
public String toShortString() { public String toShortString() {
return "%s:%.3s:%+1.2f".formatted( return "%s:%.3s:%+1.2f".formatted(
@ -139,9 +148,9 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, BaseE
ofNullable(assetValue).orElse(BigDecimal.ZERO)); ofNullable(assetValue).orElse(BigDecimal.ZERO));
} }
public static RbacView rbac() { public static RbacSpec rbac() {
return rbacViewFor("coopAssetsTransaction", HsOfficeCoopAssetsTransactionEntity.class) return rbacViewFor("coopAssetsTransaction", HsOfficeCoopAssetsTransactionEntity.class)
.withIdentityView(RbacView.SQL.projection("reference")) .withIdentityView(SQL.projection("reference"))
.withUpdatableColumns("comment") .withUpdatableColumns("comment")
.importEntityAlias("membership", HsOfficeMembershipEntity.class, usingDefaultCase(), .importEntityAlias("membership", HsOfficeMembershipEntity.class, usingDefaultCase(),
dependsOnColumn("membershipUuid"), dependsOnColumn("membershipUuid"),

View File

@ -1,14 +1,13 @@
package net.hostsharing.hsadminng.hs.office.coopshares; package net.hostsharing.hsadminng.hs.office.coopshares;
import jakarta.persistence.EntityNotFoundException;
import io.micrometer.core.annotation.Timed; import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.errors.MultiValidationException;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopSharesApi; import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopSharesApi;
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.hs.office.membership.HsOfficeMembershipRepository;
import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.mapper.StrictMapper;
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;
@ -25,6 +24,7 @@ import java.util.function.BiConsumer;
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionTypeResource.CANCELLATION; import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionTypeResource.CANCELLATION;
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionTypeResource.SUBSCRIPTION; import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionTypeResource.SUBSCRIPTION;
import static net.hostsharing.hsadminng.hs.validation.UuidResolver.resolve;
@RestController @RestController
public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopSharesApi { public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopSharesApi {
@ -33,14 +33,16 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
private Context context; private Context context;
@Autowired @Autowired
private StandardMapper mapper; private StrictMapper mapper;
@Autowired @Autowired
private HsOfficeCoopSharesTransactionRepository coopSharesTransactionRepo; private HsOfficeCoopSharesTransactionRepository coopSharesTransactionRepo;
@Autowired
private HsOfficeMembershipRepository membershipRepo;
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
@Timed("app.office.coopShares.api.getListOfCoopShares") @Timed("app.office.coopShares.api.getListOfCoopShares")
public ResponseEntity<List<HsOfficeCoopSharesTransactionResource>> getListOfCoopShares( public ResponseEntity<List<HsOfficeCoopSharesTransactionResource>> getListOfCoopShares(
final String currentSubject, final String currentSubject,
@ -55,7 +57,10 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
fromValueDate, fromValueDate,
toValueDate); toValueDate);
final var resources = mapper.mapList(entities, HsOfficeCoopSharesTransactionResource.class); final var resources = mapper.mapList(
entities,
HsOfficeCoopSharesTransactionResource.class,
ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(resources); return ResponseEntity.ok(resources);
} }
@ -70,7 +75,10 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
context.define(currentSubject, 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);
final var saved = coopSharesTransactionRepo.save(entityToSave); final var saved = coopSharesTransactionRepo.save(entityToSave);
@ -79,7 +87,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
.path("/api/hs/office/coopsharestransactions/{id}") .path("/api/hs/office/coopsharestransactions/{id}")
.buildAndExpand(saved.getUuid()) .buildAndExpand(saved.getUuid())
.toUri(); .toUri();
final var mapped = mapper.map(saved, HsOfficeCoopSharesTransactionResource.class); final var mapped = mapper.map(saved, HsOfficeCoopSharesTransactionResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.created(uri).body(mapped); return ResponseEntity.created(uri).body(mapped);
} }
@ -95,7 +103,10 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
if (result.isEmpty()) { if (result.isEmpty()) {
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
} }
return ResponseEntity.ok(mapper.map(result.get(), HsOfficeCoopSharesTransactionResource.class)); return ResponseEntity.ok(mapper.map(
result.get(),
HsOfficeCoopSharesTransactionResource.class,
ENTITY_TO_RESOURCE_POSTMAPPER));
} }
@ -137,9 +148,16 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
} }
final BiConsumer<HsOfficeCoopSharesTransactionInsertResource, HsOfficeCoopSharesTransactionEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { final BiConsumer<HsOfficeCoopSharesTransactionInsertResource, HsOfficeCoopSharesTransactionEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
entity.setMembership(resolve("membership.uuid", resource.getMembershipUuid(), membershipRepo::findByUuid));
if (resource.getRevertedShareTxUuid() != null) { if (resource.getRevertedShareTxUuid() != null) {
entity.setRevertedShareTx(coopSharesTransactionRepo.findByUuid(resource.getRevertedShareTxUuid()) entity.setRevertedShareTx(resolve(
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] revertedShareTxUuid %s not found".formatted(resource.getRevertedShareTxUuid())))); "revertedShareTx.uuid",
resource.getRevertedShareTxUuid(),
coopSharesTransactionRepo::findByUuid));
} }
}; };
final BiConsumer<HsOfficeCoopSharesTransactionEntity, HsOfficeCoopSharesTransactionResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
resource.setMembershipUuid(entity.getMembership().getUuid());
};
} }

View File

@ -7,28 +7,38 @@ 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.generator.RbacView;
import net.hostsharing.hsadminng.persistence.BaseEntity; import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL; import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
import net.hostsharing.hsadminng.repr.Stringify; import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable; import net.hostsharing.hsadminng.repr.Stringifyable;
import jakarta.persistence.*; import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import java.io.IOException; import java.io.IOException;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.UUID; import java.util.UUID;
import static java.util.Optional.ofNullable; import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Column.dependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NOT_NULL; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.INSERT; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.SELECT; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.UPDATE; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.UPDATE;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.AGENT; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.AGENT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
import static net.hostsharing.hsadminng.repr.Stringify.stringify; import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@Entity @Entity
@ -123,7 +133,7 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, BaseE
return "%s:%.3s:%+d".formatted(getMemberNumberTagged(), transactionType, shareCount); return "%s:%.3s:%+d".formatted(getMemberNumberTagged(), transactionType, shareCount);
} }
public static RbacView rbac() { public static RbacSpec rbac() {
return rbacViewFor("coopSharesTransaction", HsOfficeCoopSharesTransactionEntity.class) return rbacViewFor("coopSharesTransaction", HsOfficeCoopSharesTransactionEntity.class)
.withIdentityView(SQL.projection("reference")) .withIdentityView(SQL.projection("reference"))
.withUpdatableColumns("comment") .withUpdatableColumns("comment")
@ -132,6 +142,7 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, BaseE
directlyFetchedByDependsOnColumn(), directlyFetchedByDependsOnColumn(),
NOT_NULL) NOT_NULL)
// the membership:ADMIN is not to be confused with the member itself, it's an account manager of the coop
.toRole("membership", ADMIN).grantPermission(INSERT) .toRole("membership", ADMIN).grantPermission(INSERT)
.toRole("membership", ADMIN).grantPermission(UPDATE) .toRole("membership", ADMIN).grantPermission(UPDATE)
.toRole("membership", AGENT).grantPermission(SELECT); .toRole("membership", AGENT).grantPermission(SELECT);

View File

@ -2,14 +2,16 @@ package net.hostsharing.hsadminng.hs.office.debitor;
import io.micrometer.core.annotation.Timed; import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountRepository;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealRepository;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeDebitorsApi; import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeDebitorsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorInsertResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorPatchResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorPatchResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorResource;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealRepository;
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.StandardMapper; import net.hostsharing.hsadminng.mapper.StrictMapper;
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;
@ -26,6 +28,7 @@ import java.util.UUID;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
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.hs.validation.UuidResolver.resolve;
import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag; import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag;
@RestController @RestController
@ -36,16 +39,22 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
private Context context; private Context context;
@Autowired @Autowired
private StandardMapper mapper; private StrictMapper mapper;
@Autowired @Autowired
private HsOfficeDebitorRepository debitorRepo; private HsOfficeDebitorRepository debitorRepo;
@Autowired @Autowired
private HsOfficeRelationRealRepository relrealRepo; private HsOfficeRelationRealRepository realRelRepo;
@Autowired @Autowired
private EntityExistsValidator entityValidator; private HsOfficePersonRealRepository realPersonRepo;
@Autowired
private HsOfficeContactRealRepository realContactRepo;
@Autowired
private HsOfficeBankAccountRepository bankAccountRepo;
@PersistenceContext @PersistenceContext
private EntityManager em; private EntityManager em;
@ -81,34 +90,19 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
context.define(currentSubject, 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().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, RESOURCE_TO_ENTITY_POSTMAPPER);
if (body.getDebitorRel() != null) {
final var debitorRel = mapper.map("debitorRel.", body.getDebitorRel(), HsOfficeRelationRealEntity.class);
debitorRel.setType(DEBITOR);
entityValidator.validateEntityExists("debitorRel.anchorUuid", debitorRel.getAnchor());
entityValidator.validateEntityExists("debitorRel.holderUuid", debitorRel.getHolder());
entityValidator.validateEntityExists("debitorRel.contactUuid", debitorRel.getContact());
entityToSave.setDebitorRel(relrealRepo.save(debitorRel));
} else {
final var debitorRelOptional = relrealRepo.findByUuid(body.getDebitorRelUuid());
debitorRelOptional.ifPresentOrElse(
debitorRel -> {entityToSave.setDebitorRel(relrealRepo.save(debitorRel));},
() -> {
throw new ValidationException(
"Unable to find RealRelation by debitorRelUuid: " + body.getDebitorRelUuid());
});
}
final var savedEntity = debitorRepo.save(entityToSave); final var savedEntity = debitorRepo.save(entityToSave).reload(em);
em.flush();
em.refresh(savedEntity);
final var uri = final var uri =
MvcUriComponentsBuilder.fromController(getClass()) MvcUriComponentsBuilder.fromController(getClass())
@ -181,7 +175,7 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
context.define(currentSubject, assumedRoles); context.define(currentSubject, assumedRoles);
final var current = debitorRepo.findByUuid(debitorUuid).orElseThrow(); final var current = debitorRepo.findByUuid(debitorUuid).orElseThrow().reload(em);
new HsOfficeDebitorEntityPatcher(em, current).apply(body); new HsOfficeDebitorEntityPatcher(em, current).apply(body);
@ -191,7 +185,39 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
return ResponseEntity.ok(mapped); return ResponseEntity.ok(mapped);
} }
final BiConsumer<HsOfficeDebitorInsertResource, HsOfficeDebitorEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
if (resource.getDebitorRel() != null) {
final var debitorRel = realRelRepo.save(HsOfficeRelationRealEntity.builder()
.type(DEBITOR)
.anchor(resolve(
"debitorRel.anchor.uuid", resource.getDebitorRel().getAnchorUuid(), realPersonRepo::findByUuid))
.holder(resolve(
"debitorRel.holder.uuid", resource.getDebitorRel().getHolderUuid(), realPersonRepo::findByUuid))
.contact(resolve(
"debitorRel.contact.uuid", resource.getDebitorRel().getContactUuid(), realContactRepo::findByUuid))
.build());
entity.setDebitorRel(debitorRel);
} else {
final var debitorRelOptional = realRelRepo.findByUuid(resource.getDebitorRelUuid());
debitorRelOptional.ifPresentOrElse(
debitorRel -> {
entity.setDebitorRel(realRelRepo.save(debitorRel));
},
() -> {
throw new ValidationException(
"Unable to find debitorRel.uuid: " + resource.getDebitorRelUuid());
});
}
if (resource.getRefundBankAccountUuid() != null) {
entity.setRefundBankAccount(resolve(
"refundBankAccount.uuid", resource.getRefundBankAccountUuid(), bankAccountRepo::findByUuid));
}
};
final BiConsumer<HsOfficeDebitorEntity, HsOfficeDebitorResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { final BiConsumer<HsOfficeDebitorEntity, HsOfficeDebitorResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
resource.setDebitorNumber(entity.getTaggedDebitorNumber()); resource.setDebitorNumber(entity.getTaggedDebitorNumber());
resource.getPartner().setPartnerNumber(entity.getPartner().getTaggedPartnerNumber());
}; };
} }

View File

@ -7,16 +7,16 @@ 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.bankaccount.HsOfficeBankAccountEntity; import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity; import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartner;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRealEntity;
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.persistence.BaseEntity; import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.generator.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL; import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
import net.hostsharing.hsadminng.repr.Stringify; import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable; import net.hostsharing.hsadminng.repr.Stringifyable;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.JoinFormula; import org.hibernate.annotations.JoinFormula;
import org.hibernate.annotations.NotFound; import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction; import org.hibernate.annotations.NotFoundAction;
@ -40,17 +40,17 @@ 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.generator.RbacView.Column.dependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingCase; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingCase;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.GLOBAL;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NOT_NULL; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NULLABLE; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Nullable.NULLABLE;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.fetchedBySql; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.fetchedBySql;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
import static net.hostsharing.hsadminng.repr.Stringify.stringify; import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@Entity @Entity
@ -75,7 +75,6 @@ public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>,
@Id @Id
@GeneratedValue @GeneratedValue
@GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
private UUID uuid; private UUID uuid;
@Version @Version
@ -87,16 +86,16 @@ 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 partner
JOIN hs_office.relation_rv dRel JOIN hs_office.relation 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 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
) )
""") """)
@NotFound(action = NotFoundAction.IGNORE) // TODO.impl: map a simplified raw-PartnerEntity, just for the partner-number @NotFound(action = NotFoundAction.EXCEPTION) // TODO.impl: map a simplified raw-PartnerEntity, just for the partner-number
private HsOfficePartnerEntity partner; private HsOfficePartnerRealEntity partner;
@Column(name = "debitornumbersuffix", length = 2) @Column(name = "debitornumbersuffix", length = 2)
@Pattern(regexp = TWO_DECIMAL_DIGITS) @Pattern(regexp = TWO_DECIMAL_DIGITS)
@ -132,9 +131,7 @@ public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>,
@Override @Override
public HsOfficeDebitorEntity load() { public HsOfficeDebitorEntity load() {
BaseEntity.super.load(); BaseEntity.super.load();
if (partner != null) {
partner.load(); partner.load();
}
debitorRel.load(); debitorRel.load();
if (refundBankAccount != null) { if (refundBankAccount != null) {
refundBankAccount.load(); refundBankAccount.load();
@ -145,7 +142,7 @@ public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>,
public String getTaggedDebitorNumber() { public String getTaggedDebitorNumber() {
return ofNullable(partner) return ofNullable(partner)
.filter(partner -> debitorNumberSuffix != null) .filter(partner -> debitorNumberSuffix != null)
.map(HsOfficePartnerEntity::getPartnerNumber) .map(HsOfficePartner::getPartnerNumber)
.map(partnerNumber -> DEBITOR_NUMBER_TAG + partnerNumber + debitorNumberSuffix) .map(partnerNumber -> DEBITOR_NUMBER_TAG + partnerNumber + debitorNumberSuffix)
.orElse(null); .orElse(null);
} }
@ -160,7 +157,7 @@ public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>,
return getTaggedDebitorNumber(); return getTaggedDebitorNumber();
} }
public static RbacView rbac() { public static RbacSpec rbac() {
return rbacViewFor("debitor", HsOfficeDebitorEntity.class) return rbacViewFor("debitor", HsOfficeDebitorEntity.class)
.withIdentityView(SQL.query(""" .withIdentityView(SQL.query("""
SELECT debitor.uuid AS uuid, SELECT debitor.uuid AS uuid,

View File

@ -19,7 +19,7 @@ public interface HsOfficeDebitorRepository extends Repository<HsOfficeDebitorEnt
@Query(""" @Query("""
SELECT debitor FROM HsOfficeDebitorEntity debitor SELECT debitor FROM HsOfficeDebitorEntity debitor
JOIN HsOfficePartnerEntity partner JOIN HsOfficePartnerRealEntity partner
ON partner.partnerRel.holder = debitor.debitorRel.anchor ON partner.partnerRel.holder = debitor.debitorRel.anchor
AND partner.partnerRel.type = 'PARTNER' AND debitor.debitorRel.type = 'DEBITOR' AND partner.partnerRel.type = 'PARTNER' AND debitor.debitorRel.type = 'DEBITOR'
WHERE partner.partnerNumber = :partnerNumber WHERE partner.partnerNumber = :partnerNumber
@ -42,7 +42,7 @@ public interface HsOfficeDebitorRepository extends Repository<HsOfficeDebitorEnt
@Query(""" @Query("""
SELECT debitor FROM HsOfficeDebitorEntity debitor SELECT debitor FROM HsOfficeDebitorEntity debitor
JOIN HsOfficePartnerEntity partner JOIN HsOfficePartnerRealEntity partner
ON partner.partnerRel.holder = debitor.debitorRel.anchor ON partner.partnerRel.holder = debitor.debitorRel.anchor
AND partner.partnerRel.type = 'PARTNER' AND debitor.debitorRel.type = 'DEBITOR' AND partner.partnerRel.type = 'PARTNER' AND debitor.debitorRel.type = 'DEBITOR'
JOIN HsOfficePersonRealEntity person JOIN HsOfficePersonRealEntity person

View File

@ -6,14 +6,16 @@ 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.hs.office.partner.HsOfficePartnerEntity; import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRbacEntity;
import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRealRepository;
import net.hostsharing.hsadminng.mapper.StrictMapper;
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;
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.EntityNotFoundException;
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;
@ -28,7 +30,10 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
private Context context; private Context context;
@Autowired @Autowired
private StandardMapper mapper; private StrictMapper mapper;
@Autowired
private HsOfficePartnerRealRepository partnerRepo;
@Autowired @Autowired
private HsOfficeMembershipRepository membershipRepo; private HsOfficeMembershipRepository membershipRepo;
@ -47,7 +52,7 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
final var entities = partnerNumber != null final var entities = partnerNumber != null
? membershipRepo.findMembershipsByPartnerNumber( ? membershipRepo.findMembershipsByPartnerNumber(
cropTag(HsOfficePartnerEntity.PARTNER_NUMBER_TAG, partnerNumber)) cropTag(HsOfficePartnerRbacEntity.PARTNER_NUMBER_TAG, partnerNumber))
: partnerUuid != null : partnerUuid != null
? membershipRepo.findMembershipsByPartnerUuid(partnerUuid) ? membershipRepo.findMembershipsByPartnerUuid(partnerUuid)
: membershipRepo.findAll(); : membershipRepo.findAll();
@ -68,7 +73,7 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
context.define(currentSubject, assumedRoles); context.define(currentSubject, assumedRoles);
final var entityToSave = mapper.map(body, HsOfficeMembershipEntity.class); final var entityToSave = mapper.map(body, HsOfficeMembershipEntity.class, SEPA_MANDATE_RESOURCE_TO_ENTITY_POSTMAPPER);
final var saved = membershipRepo.save(entityToSave); final var saved = membershipRepo.save(entityToSave);
@ -164,5 +169,12 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
if (entity.getValidity().hasUpperBound()) { if (entity.getValidity().hasUpperBound()) {
resource.setValidTo(entity.getValidity().upper().minusDays(1)); resource.setValidTo(entity.getValidity().upper().minusDays(1));
} }
resource.getPartner().setPartnerNumber(entity.getPartner().getTaggedPartnerNumber()); // TODO.refa: use partner mapper?
};
final BiConsumer<HsOfficeMembershipInsertResource, HsOfficeMembershipEntity> SEPA_MANDATE_RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
entity.setPartner(partnerRepo.findByUuid(resource.getPartnerUuid())
.orElseThrow(() -> new EntityNotFoundException(
"ERROR: [400] partnerUuid %s not found".formatted(resource.getPartnerUuid()))));
}; };
} }

View File

@ -8,11 +8,12 @@ import lombok.Getter;
import lombok.NoArgsConstructor; 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.partner.HsOfficePartnerRealEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
import net.hostsharing.hsadminng.persistence.BaseEntity; import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity; import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
import net.hostsharing.hsadminng.rbac.generator.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL; import net.hostsharing.hsadminng.rbac.role.WithRoleId;
import net.hostsharing.hsadminng.repr.Stringify; import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable; import net.hostsharing.hsadminng.repr.Stringifyable;
import org.hibernate.annotations.Type; import org.hibernate.annotations.Type;
@ -38,21 +39,21 @@ 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.generator.RbacView.Column.dependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.GLOBAL;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NOT_NULL; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.DELETE; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.DELETE;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.INSERT; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.SELECT; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.UPDATE; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.UPDATE;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.RbacSubjectReference.UserRole.CREATOR;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.AGENT; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.AGENT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.OWNER; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.OWNER;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.TENANT; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.TENANT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.fetchedBySql; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.fetchedBySql;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
import static net.hostsharing.hsadminng.repr.Stringify.stringify; import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@Entity @Entity
@ -63,7 +64,7 @@ import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@DisplayAs("Membership") @DisplayAs("Membership")
public class HsOfficeMembershipEntity implements BaseEntity<HsOfficeMembershipEntity>, Stringifyable { public class HsOfficeMembershipEntity implements BaseEntity<HsOfficeMembershipEntity>, Stringifyable, WithRoleId {
public static final String MEMBER_NUMBER_TAG = "M-"; public static final String MEMBER_NUMBER_TAG = "M-";
public static final String TWO_DECIMAL_DIGITS = "^([0-9]{2})$"; public static final String TWO_DECIMAL_DIGITS = "^([0-9]{2})$";
@ -84,7 +85,7 @@ public class HsOfficeMembershipEntity implements BaseEntity<HsOfficeMembershipEn
@ManyToOne(fetch = FetchType.LAZY) @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "partneruuid") @JoinColumn(name = "partneruuid")
private HsOfficePartnerEntity partner; private HsOfficePartnerRealEntity partner;
@Column(name = "membernumbersuffix", length = 2) @Column(name = "membernumbersuffix", length = 2)
@Pattern(regexp = TWO_DECIMAL_DIGITS) @Pattern(regexp = TWO_DECIMAL_DIGITS)
@ -160,7 +161,7 @@ public class HsOfficeMembershipEntity implements BaseEntity<HsOfficeMembershipEn
} }
} }
public static RbacView rbac() { public static RbacSpec rbac() {
return rbacViewFor("membership", HsOfficeMembershipEntity.class) return rbacViewFor("membership", HsOfficeMembershipEntity.class)
.withIdentityView(SQL.query(""" .withIdentityView(SQL.query("""
SELECT m.uuid AS uuid, SELECT m.uuid AS uuid,

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.StandardMapper;
import net.hostsharing.hsadminng.mapper.OptionalFromJson; import net.hostsharing.hsadminng.mapper.OptionalFromJson;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import java.util.Optional; import java.util.Optional;
public class HsOfficeMembershipEntityPatcher implements EntityPatcher<HsOfficeMembershipPatchResource> { public class HsOfficeMembershipEntityPatcher implements EntityPatcher<HsOfficeMembershipPatchResource> {
private final StandardMapper mapper; private final StrictMapper mapper;
private final HsOfficeMembershipEntity entity; private final HsOfficeMembershipEntity entity;
public HsOfficeMembershipEntityPatcher( public HsOfficeMembershipEntityPatcher(
final StandardMapper mapper, final StrictMapper mapper,
final HsOfficeMembershipEntity entity) { final HsOfficeMembershipEntity entity) {
this.mapper = mapper; this.mapper = mapper;
this.entity = entity; this.entity = entity;

View File

@ -0,0 +1,103 @@
package net.hostsharing.hsadminng.hs.office.partner;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContact;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePerson;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelation;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable;
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
import jakarta.persistence.Column;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.Version;
import java.util.UUID;
import static jakarta.persistence.CascadeType.DETACH;
import static jakarta.persistence.CascadeType.MERGE;
import static jakarta.persistence.CascadeType.PERSIST;
import static jakarta.persistence.CascadeType.REFRESH;
import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@MappedSuperclass
@Getter
@Setter
@SuperBuilder(toBuilder = true)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@DisplayAs("Partner")
public class HsOfficePartner<T extends HsOfficePartner<?>> implements Stringifyable, BaseEntity<T> {
public static final String PARTNER_NUMBER_TAG = "P-";
protected static Stringify<HsOfficePartner> stringify = stringify(HsOfficePartner.class, "partner")
.withIdProp(HsOfficePartner::toShortString)
.withProp(p -> ofNullable(p.getPartnerRel())
.map(HsOfficeRelation::getHolder)
.map(HsOfficePerson::toShortString)
.orElse(null))
.withProp(p -> ofNullable(p.getPartnerRel())
.map(HsOfficeRelation::getContact)
.map(HsOfficeContact::toShortString)
.orElse(null))
.quotedValues(false);
@Id
@GeneratedValue
private UUID uuid;
@Version
private int version;
@Column(name = "partnernumber", columnDefinition = "numeric(5) not null")
private Integer partnerNumber;
@ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "partnerreluuid", nullable = false)
private HsOfficeRelationRealEntity partnerRel;
@ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = true, fetch = FetchType.LAZY)
@JoinColumn(name = "detailsuuid")
@NotFound(action = NotFoundAction.IGNORE)
private HsOfficePartnerDetailsEntity details;
@Override
public T load() {
BaseEntity.super.load();
partnerRel.load();
if (details != null) {
details.load();
}
//noinspection unchecked
return (T) this;
}
public String getTaggedPartnerNumber() {
return PARTNER_NUMBER_TAG + partnerNumber;
}
@Override
public String toString() {
return stringify.apply(this);
}
@Override
public String toShortString() {
return getTaggedPartnerNumber();
}
}

View File

@ -13,7 +13,7 @@ import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
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.StandardMapper; import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.persistence.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;
@ -26,6 +26,7 @@ import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceContext;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.function.BiConsumer;
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.EX_PARTNER; import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.EX_PARTNER;
import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag; import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag;
@ -38,10 +39,10 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
private Context context; private Context context;
@Autowired @Autowired
private StandardMapper mapper; private StrictMapper mapper;
@Autowired @Autowired
private HsOfficePartnerRepository partnerRepo; private HsOfficePartnerRbacRepository partnerRepo;
@Autowired @Autowired
private HsOfficeRelationRealRepository relationRepo; private HsOfficeRelationRealRepository relationRepo;
@ -60,7 +61,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
final var entities = partnerRepo.findPartnerByOptionalNameLike(name); final var entities = partnerRepo.findPartnerByOptionalNameLike(name);
final var resources = mapper.mapList(entities, HsOfficePartnerResource.class); final var resources = mapper.mapList(entities, HsOfficePartnerResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(resources); return ResponseEntity.ok(resources);
} }
@ -83,7 +84,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
.path("/api/hs/office/partners/{id}") .path("/api/hs/office/partners/{id}")
.buildAndExpand(saved.getUuid()) .buildAndExpand(saved.getUuid())
.toUri(); .toUri();
final var mapped = mapper.map(saved, HsOfficePartnerResource.class); final var mapped = mapper.map(saved, HsOfficePartnerResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.created(uri).body(mapped); return ResponseEntity.created(uri).body(mapped);
} }
@ -101,7 +102,8 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
if (result.isEmpty()) { if (result.isEmpty()) {
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
} }
return ResponseEntity.ok(mapper.map(result.get(), HsOfficePartnerResource.class)); final var mapped = mapper.map(result.get(), HsOfficePartnerResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(mapped);
} }
@Override @Override
@ -118,7 +120,8 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
if (result.isEmpty()) { if (result.isEmpty()) {
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
} }
return ResponseEntity.ok(mapper.map(result.get(), HsOfficePartnerResource.class)); final var mapped = mapper.map(result.get(), HsOfficePartnerResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(mapped);
} }
@Override @Override
@ -161,20 +164,20 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
final var saved = partnerRepo.save(current); final var saved = partnerRepo.save(current);
optionallyCreateExPartnerRelation(saved, previousPartnerRel); optionallyCreateExPartnerRelation(saved, previousPartnerRel);
final var mapped = mapper.map(saved, HsOfficePartnerResource.class); final var mapped = mapper.map(saved, HsOfficePartnerResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(mapped); return ResponseEntity.ok(mapped);
} }
private void optionallyCreateExPartnerRelation(final HsOfficePartnerEntity saved, final HsOfficeRelationRealEntity previousPartnerRel) { private void optionallyCreateExPartnerRelation(final HsOfficePartnerRbacEntity saved, final HsOfficeRelationRealEntity previousPartnerRel) {
if (!saved.getPartnerRel().getUuid().equals(previousPartnerRel.getUuid())) { if (!saved.getPartnerRel().getUuid().equals(previousPartnerRel.getUuid())) {
// TODO.impl: we also need to use the new partner-person as the anchor // TODO.impl: we also need to use the new partner-person as the anchor
relationRepo.save(previousPartnerRel.toBuilder().uuid(null).type(EX_PARTNER).build()); relationRepo.save(previousPartnerRel.toBuilder().uuid(null).type(EX_PARTNER).build());
} }
} }
private HsOfficePartnerEntity createPartnerEntity(final HsOfficePartnerInsertResource body) { private HsOfficePartnerRbacEntity createPartnerEntity(final HsOfficePartnerInsertResource body) {
final var entityToSave = new HsOfficePartnerEntity(); final var entityToSave = new HsOfficePartnerRbacEntity();
entityToSave.setPartnerNumber(cropTag(HsOfficePartnerEntity.PARTNER_NUMBER_TAG, body.getPartnerNumber())); entityToSave.setPartnerNumber(cropTag(HsOfficePartnerRbacEntity.PARTNER_NUMBER_TAG, body.getPartnerNumber()));
entityToSave.setPartnerRel(persistPartnerRel(body.getPartnerRel())); entityToSave.setPartnerRel(persistPartnerRel(body.getPartnerRel()));
entityToSave.setDetails(mapper.map(body.getDetails(), HsOfficePartnerDetailsEntity.class)); entityToSave.setDetails(mapper.map(body.getDetails(), HsOfficePartnerDetailsEntity.class));
return entityToSave; return entityToSave;
@ -197,4 +200,8 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
throw new ReferenceNotFoundException(entityClass, uuid, exc); throw new ReferenceNotFoundException(entityClass, uuid, exc);
} }
} }
final BiConsumer<HsOfficePartnerRbacEntity, HsOfficePartnerResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
resource.setPartnerNumber(entity.getTaggedPartnerNumber());
};
} }

View File

@ -3,8 +3,8 @@ 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.persistence.BaseEntity; import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.generator.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL; import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
import net.hostsharing.hsadminng.repr.Stringify; import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable; import net.hostsharing.hsadminng.repr.Stringifyable;
@ -13,10 +13,10 @@ 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.generator.RbacView.GLOBAL; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.GLOBAL;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
import static net.hostsharing.hsadminng.repr.Stringify.stringify; import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@Entity @Entity
@ -67,7 +67,7 @@ public class HsOfficePartnerDetailsEntity implements BaseEntity<HsOfficePartnerD
} }
public static RbacView rbac() { public static RbacSpec rbac() {
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

View File

@ -1,128 +0,0 @@
package net.hostsharing.hsadminng.hs.office.partner;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContact;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePerson;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelation;
import net.hostsharing.hsadminng.rbac.generator.RbacView;
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable;
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
import jakarta.persistence.*;
import java.io.IOException;
import java.util.UUID;
import static jakarta.persistence.CascadeType.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
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 net.hostsharing.hsadminng.repr.Stringify.stringify;
@Entity
@Table(schema = "hs_office", name = "partner_rv")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@DisplayAs("Partner")
public class HsOfficePartnerEntity implements Stringifyable, BaseEntity<HsOfficePartnerEntity> {
public static final String PARTNER_NUMBER_TAG = "P-";
private static Stringify<HsOfficePartnerEntity> stringify = stringify(HsOfficePartnerEntity.class, "partner")
.withIdProp(HsOfficePartnerEntity::toShortString)
.withProp(p -> ofNullable(p.getPartnerRel())
.map(HsOfficeRelation::getHolder)
.map(HsOfficePerson::toShortString)
.orElse(null))
.withProp(p -> ofNullable(p.getPartnerRel())
.map(HsOfficeRelation::getContact)
.map(HsOfficeContact::toShortString)
.orElse(null))
.quotedValues(false);
@Id
@GeneratedValue
private UUID uuid;
@Version
private int version;
@Column(name = "partnernumber", columnDefinition = "numeric(5) not null")
private Integer partnerNumber;
@ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "partnerreluuid", nullable = false)
private HsOfficeRelationRealEntity partnerRel;
@ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = true, fetch = FetchType.LAZY)
@JoinColumn(name = "detailsuuid")
@NotFound(action = NotFoundAction.IGNORE)
private HsOfficePartnerDetailsEntity details;
@Override
public HsOfficePartnerEntity load() {
BaseEntity.super.load();
partnerRel.load();
details.load();
return this;
}
public String getTaggedPartnerNumber() {
return PARTNER_NUMBER_TAG + partnerNumber;
}
@Override
public String toString() {
return stringify.apply(this);
}
@Override
public String toShortString() {
return getTaggedPartnerNumber();
}
public static RbacView rbac() {
return rbacViewFor("partner", HsOfficePartnerEntity.class)
.withIdentityView(SQL.projection("'P-' || partnerNumber"))
.withUpdatableColumns("partnerRelUuid")
.toRole(GLOBAL, ADMIN).grantPermission(INSERT)
.importRootEntityAliasProxy("partnerRel", HsOfficeRelationRbacEntity.class,
usingDefaultCase(),
directlyFetchedByDependsOnColumn(),
dependsOnColumn("partnerRelUuid"))
.createPermission(DELETE).grantedTo("partnerRel", OWNER)
.createPermission(UPDATE).grantedTo("partnerRel", ADMIN)
.createPermission(SELECT).grantedTo("partnerRel", TENANT)
.importSubEntityAlias("partnerDetails", HsOfficePartnerDetailsEntity.class,
directlyFetchedByDependsOnColumn(),
dependsOnColumn("detailsUuid"))
.createPermission("partnerDetails", DELETE).grantedTo("partnerRel", OWNER)
.createPermission("partnerDetails", UPDATE).grantedTo("partnerRel", AGENT)
.createPermission("partnerDetails", SELECT).grantedTo("partnerRel", AGENT); // not TENANT!
}
public static void main(String[] args) throws IOException {
rbac().generateWithBaseFileName("5-hs-office/504-partner/5043-hs-office-partner-rbac");
}
}

View File

@ -9,10 +9,10 @@ import jakarta.persistence.EntityManager;
class HsOfficePartnerEntityPatcher implements EntityPatcher<HsOfficePartnerPatchResource> { class HsOfficePartnerEntityPatcher implements EntityPatcher<HsOfficePartnerPatchResource> {
private final EntityManager em; private final EntityManager em;
private final HsOfficePartnerEntity entity; private final HsOfficePartnerRbacEntity entity;
HsOfficePartnerEntityPatcher( HsOfficePartnerEntityPatcher(
final EntityManager em, final EntityManager em,
final HsOfficePartnerEntity entity) { final HsOfficePartnerRbacEntity entity) {
this.em = em; this.em = em;
this.entity = entity; this.entity = entity;
} }

View File

@ -0,0 +1,59 @@
package net.hostsharing.hsadminng.hs.office.partner;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
import jakarta.persistence.*;
import java.io.IOException;
import static jakarta.persistence.CascadeType.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.GLOBAL;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
@Entity
@Table(schema = "hs_office", name = "partner_rv")
@Getter
@Setter
@SuperBuilder(toBuilder = true)
@NoArgsConstructor
@DisplayAs("RbacPartner")
public class HsOfficePartnerRbacEntity extends HsOfficePartner<HsOfficePartnerRbacEntity> {
public static RbacSpec rbac() {
return rbacViewFor("partner", HsOfficePartnerRbacEntity.class)
.withIdentityView(SQL.projection("'P-' || partnerNumber"))
.withUpdatableColumns("partnerRelUuid")
.toRole(GLOBAL, ADMIN).grantPermission(INSERT)
.importRootEntityAliasProxy("partnerRel", HsOfficeRelationRbacEntity.class,
usingDefaultCase(),
directlyFetchedByDependsOnColumn(),
dependsOnColumn("partnerRelUuid"))
.createPermission(DELETE).grantedTo("partnerRel", OWNER)
.createPermission(UPDATE).grantedTo("partnerRel", ADMIN)
.createPermission(SELECT).grantedTo("partnerRel", TENANT)
.importSubEntityAlias("partnerDetails", HsOfficePartnerDetailsEntity.class,
directlyFetchedByDependsOnColumn(),
dependsOnColumn("detailsUuid"))
.createPermission("partnerDetails", DELETE).grantedTo("partnerRel", OWNER)
.createPermission("partnerDetails", UPDATE).grantedTo("partnerRel", AGENT)
.createPermission("partnerDetails", SELECT).grantedTo("partnerRel", AGENT); // not TENANT!
}
public static void main(String[] args) throws IOException {
rbac().generateWithBaseFileName("5-hs-office/504-partner/5043-hs-office-partner-rbac");
}
}

View File

@ -0,0 +1,47 @@
package net.hostsharing.hsadminng.hs.office.partner;
import io.micrometer.core.annotation.Timed;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface HsOfficePartnerRbacRepository extends Repository<HsOfficePartnerRbacEntity, UUID> {
@Timed("app.office.partners.repo.findByUuid.rbac")
Optional<HsOfficePartnerRbacEntity> findByUuid(UUID id);
@Timed("app.office.partners.repo.findAll.rbac")
List<HsOfficePartnerRbacEntity> findAll(); // TODO.refa: move to a repo in test sources
@Query(value = """
select partner.uuid, partner.detailsuuid, partner.partnernumber, partner.partnerreluuid, partner.version
from hs_office.partner_rv partner
join hs_office.relation partnerRel on partnerRel.uuid = partner.partnerreluuid
join hs_office.contact contact on contact.uuid = partnerRel.contactuuid
join hs_office.person partnerPerson on partnerPerson.uuid = partnerRel.holderuuid
left join hs_office.partner_details_rv partnerDetails on partnerDetails.uuid = partner.detailsuuid
where :name is null
or (partnerDetails.uuid is not null and partnerDetails.birthname like (cast(:name as text) || '%') escape '')
or contact.caption like (cast(:name as text) || '%') escape ''
or partnerPerson.tradename like (cast(:name as text) || '%') escape ''
or partnerPerson.givenname like (cast(:name as text) || '%') escape ''
or partnerPerson.familyname like (cast(:name as text) || '%') escape ''
""", nativeQuery = true)
@Timed("app.office.partners.repo.findPartnerByOptionalNameLike.rbac")
List<HsOfficePartnerRbacEntity> findPartnerByOptionalNameLike(String name);
@Timed("app.office.partners.repo.findPartnerByPartnerNumber.rbac")
Optional<HsOfficePartnerRbacEntity> findPartnerByPartnerNumber(Integer partnerNumber);
@Timed("app.office.partners.repo.save.rbac")
HsOfficePartnerRbacEntity save(final HsOfficePartnerRbacEntity entity);
@Timed("app.office.partners.repo.count.rbac")
long count();
@Timed("app.office.partners.repo.deleteByUuid.rbac")
int deleteByUuid(UUID uuid);
}

View File

@ -0,0 +1,21 @@
package net.hostsharing.hsadminng.hs.office.partner;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.errors.DisplayAs;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
@Entity
@Table(schema = "hs_office", name = "partner")
@Getter
@Setter
@SuperBuilder(toBuilder = true)
@NoArgsConstructor
@DisplayAs("RealPartner")
public class HsOfficePartnerRealEntity extends HsOfficePartner<HsOfficePartnerRealEntity> {
}

View File

@ -0,0 +1,41 @@
package net.hostsharing.hsadminng.hs.office.partner;
import io.micrometer.core.annotation.Timed;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface HsOfficePartnerRealRepository extends Repository<HsOfficePartnerRealEntity, UUID> {
@Timed("app.office.partners.repo.findByUuid.real")
Optional<HsOfficePartnerRealEntity> findByUuid(UUID id);
@Timed("app.office.partners.repo.findAll.real")
List<HsOfficePartnerRbacEntity> findAll(); // TODO.refa: move to a repo in test sources
@Query(value = """
select partner.uuid, partner.detailsuuid, partner.partnernumber, partner.partnerreluuid, partner.version
from hs_office.partner partner
join hs_office.relation partnerRel on partnerRel.uuid = partner.partnerreluuid
join hs_office.contact contact on contact.uuid = partnerRel.contactuuid
join hs_office.person partnerPerson on partnerPerson.uuid = partnerRel.holderuuid
left join hs_office.partner_details_rv partnerDetails on partnerDetails.uuid = partner.detailsuuid
where :name is null
or (partnerDetails.uuid is not null and partnerDetails.birthname like (cast(:name as text) || '%') escape '')
or contact.caption like (cast(:name as text) || '%') escape ''
or partnerPerson.tradename like (cast(:name as text) || '%') escape ''
or partnerPerson.givenname like (cast(:name as text) || '%') escape ''
or partnerPerson.familyname like (cast(:name as text) || '%') escape ''
""", nativeQuery = true)
@Timed("app.office.partners.repo.findPartnerByOptionalNameLike.real")
List<HsOfficePartnerRealEntity> findPartnerByOptionalNameLike(String name);
@Timed("app.office.partners.repo.findPartnerByPartnerNumber.real")
Optional<HsOfficePartnerRealEntity> findPartnerByPartnerNumber(Integer partnerNumber);
@Timed("app.office.partners.repo.save.real")
HsOfficePartnerRealEntity save(final HsOfficePartnerRealEntity entity);
}

View File

@ -1,45 +0,0 @@
package net.hostsharing.hsadminng.hs.office.partner;
import io.micrometer.core.annotation.Timed;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface HsOfficePartnerRepository extends Repository<HsOfficePartnerEntity, UUID> {
@Timed("app.office.partners.repo.findByUuid")
Optional<HsOfficePartnerEntity> findByUuid(UUID id);
@Timed("app.office.partners.repo.findAll")
List<HsOfficePartnerEntity> findAll(); // TODO.refa: move to a repo in test sources
@Query("""
SELECT partner FROM HsOfficePartnerEntity partner
JOIN HsOfficeRelationRealEntity rel ON rel.uuid = partner.partnerRel.uuid
JOIN HsOfficeContactRealEntity contact ON contact.uuid = rel.contact.uuid
JOIN HsOfficePersonRealEntity person ON person.uuid = rel.holder.uuid
WHERE :name is null
OR partner.details.birthName like concat(cast(:name as text), '%')
OR contact.caption like concat(cast(:name as text), '%')
OR person.tradeName like concat(cast(:name as text), '%')
OR person.givenName like concat(cast(:name as text), '%')
OR person.familyName like concat(cast(:name as text), '%')
""")
@Timed("app.office.partners.repo.findPartnerByOptionalNameLike")
List<HsOfficePartnerEntity> findPartnerByOptionalNameLike(String name);
@Timed("app.office.partners.repo.findPartnerByPartnerNumber")
Optional<HsOfficePartnerEntity> findPartnerByPartnerNumber(Integer partnerNumber);
@Timed("app.office.partners.repo.save")
HsOfficePartnerEntity save(final HsOfficePartnerEntity entity);
@Timed("app.office.partners.repo.count")
long count();
@Timed("app.office.partners.repo.deleteByUuid")
int deleteByUuid(UUID uuid);
}

View File

@ -9,6 +9,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.persistence.BaseEntity; import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.role.WithRoleId;
import net.hostsharing.hsadminng.repr.Stringify; import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable; import net.hostsharing.hsadminng.repr.Stringifyable;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@ -30,7 +31,7 @@ import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@SuperBuilder(toBuilder = true) @SuperBuilder(toBuilder = true)
@FieldNameConstants @FieldNameConstants
@DisplayAs("Person") @DisplayAs("Person")
public class HsOfficePerson<T extends HsOfficePerson<?> & BaseEntity<?>> implements BaseEntity<T>, Stringifyable { public class HsOfficePerson<T extends HsOfficePerson<?> & BaseEntity<?>> implements BaseEntity<T>, Stringifyable, WithRoleId {
private static Stringify<HsOfficePerson> toString = stringify(HsOfficePerson.class, "person") private static Stringify<HsOfficePerson> toString = stringify(HsOfficePerson.class, "person")
.withProp(Fields.personType, HsOfficePerson::getPersonType) .withProp(Fields.personType, HsOfficePerson::getPersonType)

View File

@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.hs.office.person; package net.hostsharing.hsadminng.hs.office.person;
import io.micrometer.core.annotation.Timed; import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.mapper.StrictMapper;
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;
@ -24,7 +24,7 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
private Context context; private Context context;
@Autowired @Autowired
private StandardMapper mapper; private StrictMapper mapper;
@Autowired @Autowired
private HsOfficePersonRbacRepository personRepo; private HsOfficePersonRbacRepository personRepo;

View File

@ -1,20 +1,19 @@
package net.hostsharing.hsadminng.hs.office.person; package net.hostsharing.hsadminng.hs.office.person;
import lombok.*; import lombok.*;
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.rbac.generator.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL; import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.io.IOException; import java.io.IOException;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.GLOBAL;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.RbacSubjectReference.UserRole.CREATOR;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
@Entity @Entity
@Table(schema = "hs_office", name = "person_rv") @Table(schema = "hs_office", name = "person_rv")
@ -22,11 +21,10 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@SuperBuilder(toBuilder = true) @SuperBuilder(toBuilder = true)
@FieldNameConstants
@DisplayAs("RbacPerson") @DisplayAs("RbacPerson")
public class HsOfficePersonRbacEntity extends HsOfficePerson<HsOfficePersonRbacEntity> { public class HsOfficePersonRbacEntity extends HsOfficePerson<HsOfficePersonRbacEntity> {
public static RbacView rbac() { public static RbacSpec rbac() {
return rbacViewFor("person", HsOfficePersonRbacEntity.class) return rbacViewFor("person", HsOfficePersonRbacEntity.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")

View File

@ -3,7 +3,6 @@ package net.hostsharing.hsadminng.hs.office.person;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import lombok.experimental.FieldNameConstants;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.errors.DisplayAs; import net.hostsharing.hsadminng.errors.DisplayAs;
@ -17,7 +16,6 @@ import jakarta.persistence.Table;
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@SuperBuilder(toBuilder = true) @SuperBuilder(toBuilder = true)
@FieldNameConstants
@DisplayAs("RealPerson") @DisplayAs("RealPerson")
public class HsOfficePersonRealEntity extends HsOfficePerson<HsOfficePersonRealEntity> { public class HsOfficePersonRealEntity extends HsOfficePerson<HsOfficePersonRealEntity> {
} }

View File

@ -4,6 +4,7 @@ public enum HsOfficePersonType {
UNKNOWN_PERSON_TYPE("??"), UNKNOWN_PERSON_TYPE("??"),
NATURAL_PERSON("NP"), // a human being NATURAL_PERSON("NP"), // a human being
LEGAL_PERSON("LP"), // incorporated legal entity like A/S, GmbH, e.K., eG, e.V. LEGAL_PERSON("LP"), // incorporated legal entity like A/S, GmbH, e.K., eG, e.V.
ORGANIZATIONAL_UNIT("OU"), // groups of persons within an organization, e.g. "Admin-Team", "Buchhaltung"
INCORPORATED_FIRM("IF"), // registered business partnership like OHG, Partnerschaftsgesellschaft INCORPORATED_FIRM("IF"), // registered business partnership like OHG, Partnerschaftsgesellschaft
UNINCORPORATED_FIRM("UF"), // unregistered partnership, association etc. like GbR, ARGE, community of heirs UNINCORPORATED_FIRM("UF"), // unregistered partnership, association etc. like GbR, ARGE, community of heirs
PUBLIC_INSTITUTION("PI"); // entities under public law like government entities, KdöR, AöR PUBLIC_INSTITUTION("PI"); // entities under public law like government entities, KdöR, AöR

View File

@ -6,6 +6,7 @@ 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.HsOfficePersonRealEntity; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
import net.hostsharing.hsadminng.persistence.BaseEntity; import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.role.WithRoleId;
import net.hostsharing.hsadminng.repr.Stringify; import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable; import net.hostsharing.hsadminng.repr.Stringifyable;
@ -22,7 +23,7 @@ import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@Setter @Setter
@SuperBuilder(toBuilder = true) @SuperBuilder(toBuilder = true)
@FieldNameConstants @FieldNameConstants
public class HsOfficeRelation implements BaseEntity<HsOfficeRelation>, Stringifyable { public class HsOfficeRelation implements BaseEntity<HsOfficeRelation>, Stringifyable, WithRoleId {
private static Stringify<HsOfficeRelation> toString = stringify(HsOfficeRelation.class, "rel") private static Stringify<HsOfficeRelation> toString = stringify(HsOfficeRelation.class, "rel")
.withProp(Fields.anchor, HsOfficeRelation::getAnchor) .withProp(Fields.anchor, HsOfficeRelation::getAnchor)

View File

@ -9,7 +9,7 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeRelation
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.HsOfficePersonRealEntity; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealRepository; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealRepository;
import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.mapper.StrictMapper;
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;
@ -32,7 +32,7 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
private Context context; private Context context;
@Autowired @Autowired
private StandardMapper mapper; private StrictMapper mapper;
@Autowired @Autowired
private HsOfficeRelationRbacRepository rbacRelationRepo; private HsOfficeRelationRbacRepository rbacRelationRepo;
@ -185,6 +185,7 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
final BiConsumer<HsOfficeContactInsertResource, HsOfficeContactRealEntity> CONTACT_RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { final BiConsumer<HsOfficeContactInsertResource, HsOfficeContactRealEntity> CONTACT_RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
entity.putPostalAddress(from(resource.getPostalAddress()));
entity.putEmailAddresses(from(resource.getEmailAddresses())); entity.putEmailAddresses(from(resource.getEmailAddresses()));
entity.putPhoneNumbers(from(resource.getPhoneNumbers())); entity.putPhoneNumbers(from(resource.getPhoneNumbers()));
}; };

View File

@ -7,31 +7,31 @@ 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.HsOfficePersonRbacEntity; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacEntity;
import net.hostsharing.hsadminng.rbac.generator.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL; import net.hostsharing.hsadminng.rbac.generator.RbacSpec.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.generator.RbacView.CaseDef.inCaseOf; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.CaseDef.inCaseOf;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.CaseDef.inOtherCases; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.CaseDef.inOtherCases;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Column.dependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.GLOBAL;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NOT_NULL; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.DELETE; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.DELETE;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.INSERT; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.SELECT; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.UPDATE; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.UPDATE;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.RbacSubjectReference.UserRole.CREATOR;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.AGENT; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.AGENT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.OWNER; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.OWNER;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.REFERRER; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.REFERRER;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.TENANT; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.TENANT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
@Entity @Entity
@Table(schema = "hs_office", name = "relation_rv") @Table(schema = "hs_office", name = "relation_rv")
@ -42,7 +42,7 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
@DisplayAs("RbacRelation") @DisplayAs("RbacRelation")
public class HsOfficeRelationRbacEntity extends HsOfficeRelation { public class HsOfficeRelationRbacEntity extends HsOfficeRelation {
public static RbacView rbac() { public static RbacSpec 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)

View File

@ -42,22 +42,22 @@ public interface HsOfficeRelationRbacRepository extends Repository<HsOfficeRelat
toSqlLikeOperand(mark), toSqlLikeOperand(personData), toSqlLikeOperand(contactData)); toSqlLikeOperand(mark), toSqlLikeOperand(personData), toSqlLikeOperand(contactData));
} }
// TODO: use ELIKE instead of lower(...) LIKE ...? Or use jsonb_path with RegEx like emailAddressRegEx in ContactRepo? // TODO: Or use jsonb_path with RegEx like emailAddressRegEx in ContactRepo?
@Query(value = """ @Query(value = """
SELECT rel FROM HsOfficeRelationRbacEntity AS rel SELECT rel FROM HsOfficeRelationRbacEntity AS rel
WHERE (:relationType IS NULL OR CAST(rel.type AS String) = :relationType) WHERE (:relationType IS NULL OR CAST(rel.type AS String) = :relationType)
AND ( :personUuid IS NULL AND ( :personUuid IS NULL
OR rel.anchor.uuid = :personUuid OR rel.holder.uuid = :personUuid ) OR rel.anchor.uuid = :personUuid OR rel.holder.uuid = :personUuid )
AND ( :mark IS NULL OR lower(rel.mark) LIKE :mark ) AND ( :mark IS NULL OR rel.mark ILIKE :mark )
AND ( :personData IS NULL AND ( :personData IS NULL
OR lower(rel.anchor.tradeName) LIKE :personData OR lower(rel.holder.tradeName) LIKE :personData OR rel.anchor.tradeName ILIKE :personData OR rel.holder.tradeName ILIKE :personData
OR lower(rel.anchor.familyName) LIKE :personData OR lower(rel.holder.familyName) LIKE :personData OR rel.anchor.familyName ILIKE :personData OR rel.holder.familyName ILIKE :personData
OR lower(rel.anchor.givenName) LIKE :personData OR lower(rel.holder.givenName) LIKE :personData ) OR rel.anchor.givenName ILIKE :personData OR rel.holder.givenName ILIKE :personData )
AND ( :contactData IS NULL AND ( :contactData IS NULL
OR lower(rel.contact.caption) LIKE :contactData OR rel.contact.caption ILIKE :contactData
OR lower(CAST(rel.contact.postalAddress AS String)) LIKE :contactData OR CAST(rel.contact.postalAddress AS String) ILIKE :contactData
OR lower(CAST(rel.contact.emailAddresses AS String)) LIKE :contactData OR CAST(rel.contact.emailAddresses AS String) ILIKE :contactData
OR lower(CAST(rel.contact.phoneNumbers AS String)) LIKE :contactData ) OR CAST(rel.contact.phoneNumbers AS String) ILIKE :contactData )
""") """)
@Timed("app.office.relations.repo.findRelationRelatedToPersonUuidRelationByTypeMarkPersonAndContactDataImpl.rbac") @Timed("app.office.relations.repo.findRelationRelatedToPersonUuidRelationByTypeMarkPersonAndContactDataImpl.rbac")
List<HsOfficeRelationRbacEntity> findRelationRelatedToPersonUuidRelationByTypeMarkPersonAndContactDataImpl( List<HsOfficeRelationRbacEntity> findRelationRelatedToPersonUuidRelationByTypeMarkPersonAndContactDataImpl(

View File

@ -14,10 +14,6 @@ public interface HsOfficeRelationRealRepository extends Repository<HsOfficeRelat
@Timed("app.repo.relations.findByUuid.real") @Timed("app.repo.relations.findByUuid.real")
Optional<HsOfficeRelationRealEntity> findByUuid(UUID id); Optional<HsOfficeRelationRealEntity> findByUuid(UUID id);
default List<HsOfficeRelationRealEntity> findRelationRelatedToPersonUuidAndRelationType(@NotNull UUID personUuid, HsOfficeRelationType relationType) {
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
@ -25,13 +21,51 @@ public interface HsOfficeRelationRealRepository extends Repository<HsOfficeRelat
@Timed("app.repo.relations.findRelationRelatedToPersonUuid.real") @Timed("app.repo.relations.findRelationRelatedToPersonUuid.real")
List<HsOfficeRelationRealEntity> findRelationRelatedToPersonUuid(@NotNull UUID personUuid); List<HsOfficeRelationRealEntity> 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 mark the mark (use '%' for wildcard), case ignored
* @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<HsOfficeRelationRealEntity> findRelationRelatedToPersonUuidRelationTypeMarkPersonAndContactData(
final UUID personUuid,
final HsOfficeRelationType relationType,
final String mark,
final String personData,
final String contactData) {
return findRelationRelatedToPersonUuidRelationByTypeMarkPersonAndContactDataImpl(
personUuid, toStringOrNull(relationType),
toSqlLikeOperand(mark), toSqlLikeOperand(personData), toSqlLikeOperand(contactData));
}
// TODO: Or use jsonb_path with RegEx like emailAddressRegEx in ContactRepo?
@Query(value = """ @Query(value = """
SELECT p.* FROM hs_office.relation AS p SELECT rel FROM HsOfficeRelationRealEntity AS rel
WHERE (:relationType IS NULL OR p.type = cast(:relationType AS hs_office.RelationType)) WHERE (:relationType IS NULL OR CAST(rel.type AS String) = :relationType)
AND ( p.anchorUuid = :personUuid OR p.holderUuid = :personUuid) AND ( :personUuid IS NULL
""", nativeQuery = true) OR rel.anchor.uuid = :personUuid OR rel.holder.uuid = :personUuid )
@Timed("app.repo.relations.findRelationRelatedToPersonUuidAndRelationTypeString.real") AND ( :mark IS NULL OR rel.mark ILIKE :mark )
List<HsOfficeRelationRealEntity> findRelationRelatedToPersonUuidAndRelationTypeString(@NotNull UUID personUuid, String relationType); AND ( :personData IS NULL
OR rel.anchor.tradeName ILIKE :personData OR rel.holder.tradeName ILIKE :personData
OR rel.anchor.familyName ILIKE :personData OR rel.holder.familyName ILIKE :personData
OR rel.anchor.givenName ILIKE :personData OR rel.holder.givenName ILIKE :personData )
AND ( :contactData IS NULL
OR rel.contact.caption ILIKE :contactData
OR CAST(rel.contact.postalAddress AS String) ILIKE :contactData
OR CAST(rel.contact.emailAddresses AS String) ILIKE :contactData
OR CAST(rel.contact.phoneNumbers AS String) ILIKE :contactData )
""")
@Timed("app.office.relations.repo.findRelationRelatedToPersonUuidRelationByTypeMarkPersonAndContactDataImpl.real")
List<HsOfficeRelationRealEntity> findRelationRelatedToPersonUuidRelationByTypeMarkPersonAndContactDataImpl(
final UUID personUuid,
final String relationType,
final String mark,
final String personData,
final String contactData);
@Timed("app.repo.relations.save.real") @Timed("app.repo.relations.save.real")
HsOfficeRelationRealEntity save(final HsOfficeRelationRealEntity entity); HsOfficeRelationRealEntity save(final HsOfficeRelationRealEntity entity);
@ -41,4 +75,11 @@ public interface HsOfficeRelationRealRepository extends Repository<HsOfficeRelat
@Timed("app.repo.relations.deleteByUuid.real") @Timed("app.repo.relations.deleteByUuid.real")
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

@ -2,11 +2,14 @@ package net.hostsharing.hsadminng.hs.office.sepamandate;
import io.micrometer.core.annotation.Timed; import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountRepository;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeSepaMandatesApi; import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeSepaMandatesApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorResource;
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.StandardMapper; import net.hostsharing.hsadminng.mapper.StrictMapper;
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;
@ -15,6 +18,7 @@ import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBui
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceContext;
import jakarta.validation.ValidationException;
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;
@ -29,7 +33,13 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
private Context context; private Context context;
@Autowired @Autowired
private StandardMapper mapper; private StrictMapper mapper;
@Autowired
private HsOfficeDebitorRepository debitorRepo;
@Autowired
private HsOfficeBankAccountRepository bankAccountRepo;
@Autowired @Autowired
private HsOfficeSepaMandateRepository sepaMandateRepo; private HsOfficeSepaMandateRepository sepaMandateRepo;
@ -137,10 +147,22 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
if (entity.getValidity().hasUpperBound()) { if (entity.getValidity().hasUpperBound()) {
resource.setValidTo(entity.getValidity().upper().minusDays(1)); resource.setValidTo(entity.getValidity().upper().minusDays(1));
} }
resource.setDebitor(mapper.map(entity.getDebitor(), HsOfficeDebitorResource.class));
resource.getDebitor().setDebitorNumber(entity.getDebitor().getTaggedDebitorNumber()); resource.getDebitor().setDebitorNumber(entity.getDebitor().getTaggedDebitorNumber());
resource.getDebitor().getPartner().setPartnerNumber(entity.getDebitor().getPartner().getTaggedPartnerNumber());
}; };
final BiConsumer<HsOfficeSepaMandateInsertResource, HsOfficeSepaMandateEntity> SEPA_MANDATE_RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { final BiConsumer<HsOfficeSepaMandateInsertResource, HsOfficeSepaMandateEntity> SEPA_MANDATE_RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
entity.setValidity(toPostgresDateRange(resource.getValidFrom(), resource.getValidTo())); entity.setValidity(toPostgresDateRange(resource.getValidFrom(), resource.getValidTo()));
entity.setDebitor(debitorRepo.findByUuid(resource.getDebitorUuid()).orElseThrow( () ->
new ValidationException(
"debitor.uuid='" + resource.getDebitorUuid() + "' not found or not accessible"
)
));
entity.setBankAccount(bankAccountRepo.findByUuid(resource.getBankAccountUuid()).orElseThrow( () ->
new ValidationException(
"bankAccount.uuid='" + resource.getBankAccountUuid() + "' not found or not accessible"
)
));
}; };
} }

View File

@ -8,7 +8,7 @@ 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.persistence.BaseEntity; import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.generator.RbacView; import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
import net.hostsharing.hsadminng.repr.Stringify; import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable; import net.hostsharing.hsadminng.repr.Stringifyable;
import org.hibernate.annotations.Type; import org.hibernate.annotations.Type;
@ -20,16 +20,16 @@ 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.generator.RbacView.Column.dependsOnColumn; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingCase; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingCase;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.GLOBAL;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NOT_NULL; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.RbacSubjectReference.UserRole.CREATOR;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.*; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
import static net.hostsharing.hsadminng.repr.Stringify.stringify; import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@Entity @Entity
@ -100,7 +100,7 @@ public class HsOfficeSepaMandateEntity implements Stringifyable, BaseEntity<HsOf
return reference; return reference;
} }
public static RbacView rbac() { public static RbacSpec rbac() {
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

View File

@ -0,0 +1,17 @@
package net.hostsharing.hsadminng.hs.validation;
import lombok.experimental.UtilityClass;
import jakarta.validation.ValidationException;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Function;
@UtilityClass
public class UuidResolver {
public static <T> T resolve(final String jsonPath, final UUID uuid, final Function<UUID, Optional<T>> findByUuid) {
return findByUuid.apply(uuid)
.orElseThrow(() -> new ValidationException("Unable to find " + jsonPath + ": " + uuid));
}
}

View File

@ -1,17 +0,0 @@
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

@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.persistence;
import org.hibernate.Hibernate; import org.hibernate.Hibernate;
import jakarta.persistence.EntityManager;
import java.util.UUID; import java.util.UUID;
public interface BaseEntity<T extends BaseEntity<?>> { public interface BaseEntity<T extends BaseEntity<?>> {
@ -15,4 +16,10 @@ public interface BaseEntity<T extends BaseEntity<?>> {
//noinspection unchecked //noinspection unchecked
return (T) this; return (T) this;
}; };
default T reload(final EntityManager em) {
em.flush();
em.refresh(this);
return load();
}
} }

View File

@ -1,38 +0,0 @@
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,16 +1,22 @@
package net.hostsharing.hsadminng.ping; package net.hostsharing.hsadminng.ping;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import jakarta.validation.constraints.NotNull;
@Controller @Controller
public class PingController { public class PingController {
@ResponseBody @ResponseBody
@RequestMapping(value = "/api/ping", method = RequestMethod.GET) @RequestMapping(value = "/api/ping", method = RequestMethod.GET)
public String ping() { public String ping(
return "pong\n"; @RequestHeader(name = "current-subject") @NotNull String currentSubject,
@RequestHeader(name = "assumed-roles", required = false) String assumedRoles
) {
return "pong " + currentSubject + "\n";
} }
} }

View File

@ -7,20 +7,20 @@ 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.generator.PostgresTriggerReference.NEW; import static net.hostsharing.hsadminng.rbac.generator.PostgresTriggerReference.NEW;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.INSERT; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacGrantDefinition.GrantType.PERM_TO_ROLE; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.RbacGrantDefinition.GrantType.PERM_TO_ROLE;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.GUEST; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.GUEST;
import static net.hostsharing.hsadminng.rbac.generator.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;
public class InsertTriggerGenerator { public class InsertTriggerGenerator {
private final RbacView rbacDef; private final RbacSpec rbacDef;
private final String liquibaseTagPrefix; private final String liquibaseTagPrefix;
public InsertTriggerGenerator(final RbacView rbacDef, final String liqibaseTagPrefix) { public InsertTriggerGenerator(final RbacSpec rbacDef, final String liqibaseTagPrefix) {
this.rbacDef = rbacDef; this.rbacDef = rbacDef;
this.liquibaseTagPrefix = liqibaseTagPrefix; this.liquibaseTagPrefix = liqibaseTagPrefix;
} }
@ -203,8 +203,8 @@ public class InsertTriggerGenerator {
plPgSql.chopEmptyLines(); plPgSql.chopEmptyLines();
} }
private void generateInsertPermissionChecksForSingleGrant(final StringWriter plPgSql, final RbacView.RbacGrantDefinition g) { private void generateInsertPermissionChecksForSingleGrant(final StringWriter plPgSql, final RbacSpec.RbacGrantDefinition g) {
final RbacView.EntityAlias superRoleEntityAlias = g.getSuperRoleDef().getEntityAlias(); final RbacSpec.EntityAlias superRoleEntityAlias = g.getSuperRoleDef().getEntityAlias();
final var caseCondition = g.isConditional() final var caseCondition = g.isConditional()
? ("NEW.type in (" + toStringList(g.getForCases()) + ") and ") ? ("NEW.type in (" + toStringList(g.getForCases()) + ") and ")
@ -275,15 +275,15 @@ public class InsertTriggerGenerator {
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName())); with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()));
} }
private String toStringList(final Set<RbacView.CaseDef> cases) { private String toStringList(final Set<RbacSpec.CaseDef> cases) {
return cases.stream().map(c -> "'" + c.value + "'").collect(joining(", ")); return cases.stream().map(c -> "'" + c.value + "'").collect(joining(", "));
} }
private boolean isGrantToADifferentTable(final RbacView.RbacGrantDefinition g) { private boolean isGrantToADifferentTable(final RbacSpec.RbacGrantDefinition g) {
return !rbacDef.getRootEntityAlias().getRawTableNameWithSchema().equals(g.getSuperRoleDef().getEntityAlias().getRawTableNameWithSchema()); return !rbacDef.getRootEntityAlias().getRawTableNameWithSchema().equals(g.getSuperRoleDef().getEntityAlias().getRawTableNameWithSchema());
} }
private Stream<RbacView.RbacGrantDefinition> getInsertGrants() { private Stream<RbacSpec.RbacGrantDefinition> getInsertGrants() {
return rbacDef.getGrantDefs().stream() return rbacDef.getGrantDefs().stream()
.filter(g -> g.grantType() == PERM_TO_ROLE) .filter(g -> g.grantType() == PERM_TO_ROLE)
.filter(g -> g.getPermDef().toCreate && g.getPermDef().getPermission() == INSERT); .filter(g -> g.getPermDef().toCreate && g.getPermDef().getPermission() == INSERT);
@ -298,14 +298,14 @@ public class InsertTriggerGenerator {
g.getSuperRoleDef().getEntityAlias().isGlobal() && g.getSuperRoleDef().getRole() == GUEST); g.getSuperRoleDef().getEntityAlias().isGlobal() && g.getSuperRoleDef().getRole() == GUEST);
} }
private Optional<RbacView.RbacGrantDefinition> getOptionalInsertGrant() { private Optional<RbacSpec.RbacGrantDefinition> getOptionalInsertGrant() {
return getInsertGrants() return getInsertGrants()
.reduce(singleton()); .reduce(singleton());
} }
private Optional<RbacView.RbacRoleDefinition> getOptionalInsertSuperRole() { private Optional<RbacSpec.RbacRoleDefinition> getOptionalInsertSuperRole() {
return getInsertGrants() return getInsertGrants()
.map(RbacView.RbacGrantDefinition::getSuperRoleDef) .map(RbacSpec.RbacGrantDefinition::getSuperRoleDef)
.reduce(singleton()); .reduce(singleton());
} }
@ -319,12 +319,12 @@ public class InsertTriggerGenerator {
}; };
} }
private static String toVar(final RbacView.RbacRoleDefinition roleDef) { private static String toVar(final RbacSpec.RbacRoleDefinition roleDef) {
return uncapitalize(roleDef.getEntityAlias().simpleName()) + capitalize(roleDef.getRole().name()); return uncapitalize(roleDef.getEntityAlias().simpleName()) + capitalize(roleDef.getRole().name());
} }
private String toRoleDescriptor(final RbacView.RbacRoleDefinition roleDef, final String ref) { private String toRoleDescriptor(final RbacSpec.RbacRoleDefinition roleDef, final String ref) {
final var functionName = roleDef.descriptorFunctionName(); final var functionName = roleDef.descriptorFunctionName();
if (roleDef.getEntityAlias().isGlobal()) { if (roleDef.getEntityAlias().isGlobal()) {
return functionName + "()"; return functionName + "()";

View File

@ -3,12 +3,12 @@ package net.hostsharing.hsadminng.rbac.generator;
import static net.hostsharing.hsadminng.rbac.generator.StringWriter.with; import static net.hostsharing.hsadminng.rbac.generator.StringWriter.with;
public class RbacIdentityViewGenerator { public class RbacIdentityViewGenerator {
private final RbacView rbacDef; private final RbacSpec rbacDef;
private final String liquibaseTagPrefix; private final String liquibaseTagPrefix;
private final String simpleEntityVarName; private final String simpleEntityVarName;
private final String rawTableName; private final String rawTableName;
public RbacIdentityViewGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) { public RbacIdentityViewGenerator(final RbacSpec rbacDef, final String liquibaseTagPrefix) {
this.rbacDef = rbacDef; this.rbacDef = rbacDef;
this.liquibaseTagPrefix = liquibaseTagPrefix; this.liquibaseTagPrefix = liquibaseTagPrefix;
this.simpleEntityVarName = rbacDef.getRootEntityAlias().simpleName(); this.simpleEntityVarName = rbacDef.getRootEntityAlias().simpleName();

View File

@ -7,7 +7,7 @@ public class RbacObjectGenerator {
private final String liquibaseTagPrefix; private final String liquibaseTagPrefix;
private final String rawTableName; private final String rawTableName;
public RbacObjectGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) { public RbacObjectGenerator(final RbacSpec rbacDef, final String liquibaseTagPrefix) {
this.liquibaseTagPrefix = liquibaseTagPrefix; this.liquibaseTagPrefix = liquibaseTagPrefix;
this.rawTableName = rbacDef.getRootEntityAlias().getRawTableNameWithSchema(); this.rawTableName = rbacDef.getRootEntityAlias().getRawTableNameWithSchema();
} }

View File

@ -0,0 +1,80 @@
package net.hostsharing.hsadminng.rbac.generator;
import net.hostsharing.hsadminng.rbac.generator.RbacSpec.RbacGrantDefinition;
import java.util.HashSet;
import java.util.Set;
import static net.hostsharing.hsadminng.rbac.generator.StringWriter.with;
class RbacRbacSystemRebuildGenerator {
private final RbacSpec rbacDef;
private final Set<RbacGrantDefinition> rbacGrants = new HashSet<>();
private final String liquibaseTagPrefix;
private final String rawTableName;
RbacRbacSystemRebuildGenerator(final RbacSpec rbacDef, final String liquibaseTagPrefix) {
this.rbacDef = rbacDef;
this.liquibaseTagPrefix = liquibaseTagPrefix;
this.rawTableName = rbacDef.getRootEntityAlias().getRawTableNameWithSchema();
}
void generateTo(final StringWriter plPgSql) {
plPgSql.writeLn("""
-- ============================================================================
--changeset RbacRbacSystemRebuildGenerator:${liquibaseTagPrefix}-rbac-rebuild endDelimiter:--//
-- ----------------------------------------------------------------------------
-- HOWTO: Rebuild RBAC-system for table ${rawTableName} after changing its RBAC specification.
--
-- begin transaction;
-- call base.defineContext('re-creating RBAC for table ${rawTableName}', null, <<insert executing global admin user here>>);
-- call ${rawTableName}_rebuild_rbac_system();
-- commit;
--
-- How it works:
-- 1. All grants previously created from the RBAC specification of this table will be deleted.
-- These grants are identified by `${rawTableName}.grantedByTriggerOf IS NOT NULL`.
-- User-induced grants (`${rawTableName}.grantedByTriggerOf IS NULL`) are NOT deleted.
-- 2. New role types will be created, but existing role types which are not specified anymore,
-- will NOT be deleted!
-- 3. All newly specified grants will be created.
--
-- IMPORTANT:
-- Make sure not to skip any previously defined role-types or you might break indirect grants!
-- E.g. If, in an updated version of the RBAC system for a table, you remove the AGENT role type
-- and now directly grant the TENANT role to the ADMIN role, all external grants to the AGENT role
-- of this table would be in a dead end.
create or replace procedure ${rawTableName}_rebuild_rbac_system()
language plpgsql as $$
DECLARE
DECLARE
row ${rawTableName};
grantsAfter numeric;
grantsBefore numeric;
BEGIN
SELECT count(*) INTO grantsBefore FROM rbac.grant;
FOR row IN SELECT * FROM ${rawTableName} LOOP
-- first delete all generated grants for this row from the previously defined RBAC system
DELETE FROM rbac.grant g
WHERE g.grantedbytriggerof = row.uuid;
-- then build the grants according to the currently defined RBAC rules
CALL ${rawTableName}_build_rbac_system(row);
END LOOP;
select count(*) into grantsAfter from rbac.grant;
-- print how the total count of grants has changed
raise notice 'total grant count before -> after: % -> %', grantsBefore, grantsAfter;
END;
$$;
--//
""",
with("liquibaseTagPrefix", liquibaseTagPrefix),
with("rawTableName", rawTableName));
}
}

View File

@ -6,11 +6,11 @@ import static net.hostsharing.hsadminng.rbac.generator.StringWriter.indented;
import static net.hostsharing.hsadminng.rbac.generator.StringWriter.with; import static net.hostsharing.hsadminng.rbac.generator.StringWriter.with;
public class RbacRestrictedViewGenerator { public class RbacRestrictedViewGenerator {
private final RbacView rbacDef; private final RbacSpec rbacDef;
private final String liquibaseTagPrefix; private final String liquibaseTagPrefix;
private final String rawTableName; private final String rawTableName;
public RbacRestrictedViewGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) { public RbacRestrictedViewGenerator(final RbacSpec rbacDef, final String liquibaseTagPrefix) {
this.rbacDef = rbacDef; this.rbacDef = rbacDef;
this.liquibaseTagPrefix = liquibaseTagPrefix; this.liquibaseTagPrefix = liquibaseTagPrefix;
this.rawTableName = rbacDef.getRootEntityAlias().getRawTableNameWithSchema(); this.rawTableName = rbacDef.getRootEntityAlias().getRawTableNameWithSchema();

View File

@ -8,7 +8,7 @@ public class RbacRoleDescriptorsGenerator {
private final String simpleEntityVarName; private final String simpleEntityVarName;
private final String rawTableName; private final String rawTableName;
public RbacRoleDescriptorsGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) { public RbacRoleDescriptorsGenerator(final RbacSpec rbacDef, final String liquibaseTagPrefix) {
this.liquibaseTagPrefix = liquibaseTagPrefix; this.liquibaseTagPrefix = liquibaseTagPrefix;
this.simpleEntityVarName = rbacDef.getRootEntityAlias().simpleName(); this.simpleEntityVarName = rbacDef.getRootEntityAlias().simpleName();
this.rawTableName = rbacDef.getRootEntityAlias().getRawTableNameWithSchema(); this.rawTableName = rbacDef.getRootEntityAlias().getRawTableNameWithSchema();

View File

@ -22,19 +22,18 @@ 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.generator.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NOT_NULL; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacGrantDefinition.GrantType.PERM_TO_ROLE; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.RbacGrantDefinition.GrantType.PERM_TO_ROLE;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacGrantDefinition.GrantType.ROLE_TO_ROLE; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.RbacGrantDefinition.GrantType.ROLE_TO_ROLE;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.RbacSubjectReference.UserRole.CREATOR;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.Part.AUTO_FETCH; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.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.capitalize;
import static org.apache.commons.lang3.StringUtils.uncapitalize; import static org.apache.commons.lang3.StringUtils.uncapitalize;
@Getter @Getter
// TODO.refa: rename to RbacDSL public class RbacSpec {
public class RbacView {
public static final String GLOBAL = "rbac.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";
@ -90,11 +89,11 @@ 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<?>> RbacSpec rbacViewFor(final String alias, final Class<E> entityClass) {
return new RbacView(alias, entityClass); return new RbacSpec(alias, entityClass);
} }
RbacView(final String alias, final Class<? extends BaseEntity<?>> entityClass) { RbacSpec(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 RbacSubjectReference(CREATOR); new RbacSubjectReference(CREATOR);
@ -110,7 +109,7 @@ public class RbacView {
* @return * @return
* the `this` instance itself to allow chained calls. * the `this` instance itself to allow chained calls.
*/ */
public RbacView withUpdatableColumns(final String... columnNames) { public RbacSpec withUpdatableColumns(final String... columnNames) {
Collections.addAll(updatableColumns, columnNames); Collections.addAll(updatableColumns, columnNames);
verifyVersionColumnExists(); verifyVersionColumnExists();
return this; return this;
@ -134,7 +133,7 @@ public class RbacView {
* @return * @return
* the `this` instance itself to allow chained calls. * the `this` instance itself to allow chained calls.
*/ */
public RbacView withIdentityView(final SQL sqlExpression) { public RbacSpec withIdentityView(final SQL sqlExpression) {
this.identityViewSqlQuery = sqlExpression; this.identityViewSqlQuery = sqlExpression;
return this; return this;
} }
@ -150,7 +149,7 @@ public class RbacView {
* @return * @return
* the `this` instance itself to allow chained calls. * the `this` instance itself to allow chained calls.
*/ */
public RbacView withRestrictedViewOrderBy(final SQL orderBySqlExpression) { public RbacSpec withRestrictedViewOrderBy(final SQL orderBySqlExpression) {
this.orderBySqlExpression = orderBySqlExpression; this.orderBySqlExpression = orderBySqlExpression;
return this; return this;
} }
@ -166,7 +165,7 @@ public class RbacView {
* @return * @return
* the `this` instance itself to allow chained calls. * the `this` instance itself to allow chained calls.
*/ */
public RbacView createRole(final Role role, final Consumer<RbacRoleDefinition> with) { public RbacSpec createRole(final Role role, final Consumer<RbacRoleDefinition> with) {
final RbacRoleDefinition newRoleDef = findRbacRole(rootEntityAlias, role).toCreate(); final RbacRoleDefinition newRoleDef = findRbacRole(rootEntityAlias, role).toCreate();
with.accept(newRoleDef); with.accept(newRoleDef);
previousRoleDef = newRoleDef; previousRoleDef = newRoleDef;
@ -182,7 +181,7 @@ public class RbacView {
* @return * @return
* the `this` instance itself to allow chained calls. * the `this` instance itself to allow chained calls.
*/ */
public RbacView createSubRole(final Role role) { public RbacSpec createSubRole(final Role role) {
final RbacRoleDefinition newRoleDef = findRbacRole(rootEntityAlias, role).toCreate(); final RbacRoleDefinition newRoleDef = findRbacRole(rootEntityAlias, role).toCreate();
findOrCreateGrantDef(newRoleDef, previousRoleDef).toCreate(); findOrCreateGrantDef(newRoleDef, previousRoleDef).toCreate();
previousRoleDef = newRoleDef; previousRoleDef = newRoleDef;
@ -202,7 +201,7 @@ public class RbacView {
* @return * @return
* the `this` instance itself to allow chained calls. * the `this` instance itself to allow chained calls.
*/ */
public RbacView createSubRole(final Role role, final Consumer<RbacRoleDefinition> with) { public RbacSpec createSubRole(final Role role, final Consumer<RbacRoleDefinition> with) {
final RbacRoleDefinition newRoleDef = findRbacRole(rootEntityAlias, role).toCreate(); final RbacRoleDefinition newRoleDef = findRbacRole(rootEntityAlias, role).toCreate();
findOrCreateGrantDef(newRoleDef, previousRoleDef).toCreate(); findOrCreateGrantDef(newRoleDef, previousRoleDef).toCreate();
with.accept(newRoleDef); with.accept(newRoleDef);
@ -254,7 +253,7 @@ public class RbacView {
.orElseGet(() -> new RbacPermissionDefinition(entityAlias, permission, null, true)); .orElseGet(() -> new RbacPermissionDefinition(entityAlias, permission, null, true));
} }
public <EC extends BaseEntity> RbacView declarePlaceholderEntityAliases(final String... aliasNames) { public <EC extends BaseEntity> RbacSpec declarePlaceholderEntityAliases(final String... aliasNames) {
for (String alias : aliasNames) { for (String alias : aliasNames) {
entityAliases.put(alias, new EntityAlias(alias)); entityAliases.put(alias, new EntityAlias(alias));
} }
@ -287,7 +286,7 @@ 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<?>> RbacSpec importRootEntityAliasProxy(
final String aliasName, final String aliasName,
final Class<? extends BaseEntity<?>> entityClass, final Class<? extends BaseEntity<?>> entityClass,
final ColumnValue forCase, final ColumnValue forCase,
@ -312,7 +311,7 @@ public class RbacView {
* @param <EC> * @param <EC>
* a JPA entity class extending RbacObject * a JPA entity class extending RbacObject
*/ */
public RbacView importSubEntityAlias( public RbacSpec 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);
@ -349,7 +348,7 @@ public class RbacView {
* @param <EC> * @param <EC>
* a JPA entity class extending RbacObject * a JPA entity class extending RbacObject
*/ */
public RbacView importEntityAlias( public RbacSpec 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);
@ -379,12 +378,12 @@ public class RbacView {
return entityAlias; return entityAlias;
} }
private static RbacView rbacDefinition(final Class<? extends BaseEntity> entityClass) private static RbacSpec rbacDefinition(final Class<? extends BaseEntity> entityClass)
throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
return (RbacView) entityClass.getMethod("rbac").invoke(null); return (RbacSpec) entityClass.getMethod("rbac").invoke(null);
} }
private RbacView importAsAlias(final String aliasName, final RbacView importedRbacView, final ColumnValue forCase, final boolean asSubEntity) { private RbacSpec importAsAlias(final String aliasName, final RbacSpec importedRbacView, final ColumnValue forCase, final boolean asSubEntity) {
final var mapper = new AliasNameMapper(importedRbacView, aliasName, final var mapper = new AliasNameMapper(importedRbacView, aliasName,
asSubEntity ? entityAliases.keySet() : null); asSubEntity ? entityAliases.keySet() : null);
copyOf(importedRbacView.getEntityAliases().values()).stream() copyOf(importedRbacView.getEntityAliases().values()).stream()
@ -416,7 +415,7 @@ public class RbacView {
return this; return this;
} }
public RbacView switchOnColumn(final String discriminatorColumName, final CaseDef... caseDefs) { public RbacSpec switchOnColumn(final String discriminatorColumName, final CaseDef... caseDefs) {
this.discriminatorColumName = discriminatorColumName; this.discriminatorColumName = discriminatorColumName;
allCases.addAll(stream(caseDefs).toList()); allCases.addAll(stream(caseDefs).toList());
@ -511,7 +510,7 @@ public class RbacView {
new RbacViewPostgresGenerator(this).generateToChangeLog(Path.of(OUTPUT_BASEDIR, baseFileName + ".sql")); new RbacViewPostgresGenerator(this).generateToChangeLog(Path.of(OUTPUT_BASEDIR, baseFileName + ".sql"));
} }
public RbacView limitDiagramTo(final String... aliasNames) { public RbacSpec limitDiagramTo(final String... aliasNames) {
this.limitDiagramToAliasNames = Set.of(aliasNames); this.limitDiagramToAliasNames = Set.of(aliasNames);
return this; return this;
} }
@ -542,15 +541,15 @@ public class RbacView {
this.superRoleDef = findRbacRole(entityAlias, role); this.superRoleDef = findRbacRole(entityAlias, role);
} }
public RbacView grantRole(final String entityAlias, final Role role) { public RbacSpec grantRole(final String entityAlias, final Role role) {
findOrCreateGrantDef(findRbacRole(entityAlias, role), superRoleDef).toCreate(); findOrCreateGrantDef(findRbacRole(entityAlias, role), superRoleDef).toCreate();
return RbacView.this; return RbacSpec.this;
} }
public RbacView grantPermission(final Permission perm) { public RbacSpec grantPermission(final Permission perm) {
final var forTable = rootEntityAlias.getRawTableNameWithSchema(); final var forTable = rootEntityAlias.getRawTableNameWithSchema();
findOrCreateGrantDef(findRbacPerm(rootEntityAlias, perm, forTable), superRoleDef).toCreate(); findOrCreateGrantDef(findRbacPerm(rootEntityAlias, perm, forTable), superRoleDef).toCreate();
return RbacView.this; return RbacSpec.this;
} }
} }
@ -698,10 +697,10 @@ public class RbacView {
this.subRole = role; this.subRole = role;
} }
public RbacView wouldBeGrantedTo(final String entityAlias, final Role role) { public RbacSpec wouldBeGrantedTo(final String entityAlias, final Role role) {
this.superRoleEntity = findEntityAlias(entityAlias); this.superRoleEntity = findEntityAlias(entityAlias);
this.superRole = role; this.superRole = role;
return RbacView.this; return RbacSpec.this;
} }
} }
@ -733,9 +732,9 @@ public class RbacView {
* @return * @return
* The RbacView specification to which this permission definition belongs. * The RbacView specification to which this permission definition belongs.
*/ */
public RbacView grantedTo(final String entityAlias, final Role role) { public RbacSpec grantedTo(final String entityAlias, final Role role) {
findOrCreateGrantDef(this, findRbacRole(entityAlias, role)).toCreate(); findOrCreateGrantDef(this, findRbacRole(entityAlias, role)).toCreate();
return RbacView.this; return RbacSpec.this;
} }
@Override @Override
@ -1186,12 +1185,12 @@ public class RbacView {
private static class AliasNameMapper { private static class AliasNameMapper {
private final RbacView importedRbacView; private final RbacSpec importedRbacView;
private final String outerAliasName; private final String outerAliasName;
private final Set<String> outerAliasNames; private final Set<String> outerAliasNames;
AliasNameMapper(final RbacView importedRbacView, final String outerAliasName, final Set<String> outerAliasNames) { AliasNameMapper(final RbacSpec importedRbacView, final String outerAliasName, final Set<String> outerAliasNames) {
this.importedRbacView = importedRbacView; this.importedRbacView = importedRbacView;
this.outerAliasName = outerAliasName; this.outerAliasName = outerAliasName;
this.outerAliasNames = (outerAliasNames == null) ? Collections.emptySet() : outerAliasNames; this.outerAliasNames = (outerAliasNames == null) ? Collections.emptySet() : outerAliasNames;
@ -1210,19 +1209,19 @@ public class RbacView {
public static class CaseDef extends ColumnValue { public static class CaseDef extends ColumnValue {
final Consumer<RbacView> def; final Consumer<RbacSpec> def;
private CaseDef(final String discriminatorColumnValue, final Consumer<RbacView> def) { private CaseDef(final String discriminatorColumnValue, final Consumer<RbacSpec> def) {
super(discriminatorColumnValue); super(discriminatorColumnValue);
this.def = def; this.def = def;
} }
public static CaseDef inCaseOf(final String discriminatorColumnValue, final Consumer<RbacView> def) { public static CaseDef inCaseOf(final String discriminatorColumnValue, final Consumer<RbacSpec> def) {
return new CaseDef(discriminatorColumnValue, def); return new CaseDef(discriminatorColumnValue, def);
} }
public static CaseDef inOtherCases(final Consumer<RbacView> def) { public static CaseDef inOtherCases(final Consumer<RbacSpec> def) {
return new CaseDef(null, def); return new CaseDef(null, def);
} }
@ -1281,7 +1280,7 @@ public class RbacView {
.filter(c -> stream(c.getDeclaredMethods()) .filter(c -> stream(c.getDeclaredMethods())
.anyMatch(m -> m.getName().equals("rbac") && isStatic(m.getModifiers())) .anyMatch(m -> m.getName().equals("rbac") && isStatic(m.getModifiers()))
) )
.map(RbacView::castToSubclassOfBaseEntity) .map(RbacSpec::castToSubclassOfBaseEntity)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
return rbacEntityClasses; return rbacEntityClasses;
} }
@ -1296,6 +1295,6 @@ public class RbacView {
*/ */
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
findRbacEntityClasses("net.hostsharing.hsadminng") findRbacEntityClasses("net.hostsharing.hsadminng")
.forEach(RbacView::generateRbacView); .forEach(RbacSpec::generateRbacView);
} }
} }

View File

@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.rbac.generator; package net.hostsharing.hsadminng.rbac.generator;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import net.hostsharing.hsadminng.rbac.generator.RbacView.CaseDef; import net.hostsharing.hsadminng.rbac.generator.RbacSpec.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.generator.RbacView.RbacGrantDefinition.GrantType.*; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.RbacGrantDefinition.GrantType.*;
public class RbacViewMermaidFlowchartGenerator { public class RbacViewMermaidFlowchartGenerator {
@ -20,14 +20,14 @@ public class RbacViewMermaidFlowchartGenerator {
public static final String HOSTSHARING_LIGHT_ORANGE = "#feb28c"; public static final String HOSTSHARING_LIGHT_ORANGE = "#feb28c";
public static final String HOSTSHARING_DARK_BLUE = "#274d6e"; public static final String HOSTSHARING_DARK_BLUE = "#274d6e";
public static final String HOSTSHARING_LIGHT_BLUE = "#99bcdb"; public static final String HOSTSHARING_LIGHT_BLUE = "#99bcdb";
private final RbacView rbacDef; private final RbacSpec rbacDef;
private final List<RbacView.EntityAlias> usedEntityAliases; private final List<RbacSpec.EntityAlias> usedEntityAliases;
private final CaseDef forCase; private final CaseDef forCase;
private final StringWriter flowchart = new StringWriter(); private final StringWriter flowchart = new StringWriter();
public RbacViewMermaidFlowchartGenerator(final RbacView rbacDef, final CaseDef forCase) { public RbacViewMermaidFlowchartGenerator(final RbacSpec rbacDef, final CaseDef forCase) {
this.rbacDef = rbacDef; this.rbacDef = rbacDef;
this.forCase = forCase; this.forCase = forCase;
@ -37,7 +37,7 @@ public class RbacViewMermaidFlowchartGenerator {
g.getSubRoleDef() != null ? g.getSubRoleDef().getEntityAlias() : null, g.getSubRoleDef() != null ? g.getSubRoleDef().getEntityAlias() : null,
g.getPermDef() != null ? g.getPermDef().getEntityAlias() : null)) g.getPermDef() != null ? g.getPermDef().getEntityAlias() : null))
.filter(Objects::nonNull) .filter(Objects::nonNull)
.sorted(comparing(RbacView.EntityAlias::aliasName)) .sorted(comparing(RbacSpec.EntityAlias::aliasName))
.distinct() .distinct()
.filter(rbacDef::renderInDiagram) .filter(rbacDef::renderInDiagram)
.collect(Collectors.toList()); .collect(Collectors.toList());
@ -50,7 +50,7 @@ public class RbacViewMermaidFlowchartGenerator {
renderGrants(); renderGrants();
} }
public RbacViewMermaidFlowchartGenerator(final RbacView rbacDef) { public RbacViewMermaidFlowchartGenerator(final RbacSpec rbacDef) {
this(rbacDef, null); this(rbacDef, null);
} }
private void renderEntitySubgraphs() { private void renderEntitySubgraphs() {
@ -61,7 +61,7 @@ public class RbacViewMermaidFlowchartGenerator {
.forEach(this::renderEntitySubgraph); .forEach(this::renderEntitySubgraph);
} }
private void renderEntitySubgraph(final RbacView.EntityAlias entity) { private void renderEntitySubgraph(final RbacSpec.EntityAlias entity) {
if (!rbacDef.renderInDiagram(entity)) { if (!rbacDef.renderInDiagram(entity)) {
return; return;
} }
@ -128,7 +128,7 @@ public class RbacViewMermaidFlowchartGenerator {
renderGrants(PERM_TO_ROLE, "%% granting permissions to roles"); renderGrants(PERM_TO_ROLE, "%% granting permissions to roles");
} }
private void renderGrants(final RbacView.RbacGrantDefinition.GrantType grantType, final String comment) { private void renderGrants(final RbacSpec.RbacGrantDefinition.GrantType grantType, final String comment) {
final var grantsOfRequestedType = rbacDef.getGrantDefs().stream() final var grantsOfRequestedType = rbacDef.getGrantDefs().stream()
.filter(g -> g.grantType() == grantType) .filter(g -> g.grantType() == grantType)
.filter(rbacDef::renderInDiagram) .filter(rbacDef::renderInDiagram)
@ -141,7 +141,7 @@ public class RbacViewMermaidFlowchartGenerator {
} }
} }
private boolean isToBeRenderedForThisCase(final RbacView.RbacGrantDefinition g) { private boolean isToBeRenderedForThisCase(final RbacSpec.RbacGrantDefinition g) {
if ( g.grantType() == ROLE_TO_USER ) if ( g.grantType() == ROLE_TO_USER )
return true; return true;
if ( forCase == null && !g.isConditional() ) if ( forCase == null && !g.isConditional() )
@ -150,7 +150,7 @@ public class RbacViewMermaidFlowchartGenerator {
return isToBeRenderedInThisGraph; return isToBeRenderedInThisGraph;
} }
private String grantDef(final RbacView.RbacGrantDefinition grant) { private String grantDef(final RbacSpec.RbacGrantDefinition grant) {
final var arrow = (grant.isToCreate() ? " ==>" : " -.->") final var arrow = (grant.isToCreate() ? " ==>" : " -.->")
+ (grant.isAssumed() ? " " : "|XX| "); + (grant.isAssumed() ? " " : "|XX| ");
final var grantDef = switch (grant.grantType()) { final var grantDef = switch (grant.grantType()) {
@ -164,19 +164,19 @@ public class RbacViewMermaidFlowchartGenerator {
return grantDef; return grantDef;
} }
private String permDef(final RbacView.RbacPermissionDefinition perm) { private String permDef(final RbacSpec.RbacPermissionDefinition perm) {
return permId(perm) + "{{" + perm.getEntityAlias().aliasName() + perm.getPermission() + "}}"; return permId(perm) + "{{" + perm.getEntityAlias().aliasName() + perm.getPermission() + "}}";
} }
private static String permId(final RbacView.RbacPermissionDefinition permDef) { private static String permId(final RbacSpec.RbacPermissionDefinition permDef) {
return "perm:" + permDef.getEntityAlias().aliasName() + permDef.getPermission(); return "perm:" + permDef.getEntityAlias().aliasName() + permDef.getPermission();
} }
private String roleDef(final RbacView.RbacRoleDefinition roleDef) { private String roleDef(final RbacSpec.RbacRoleDefinition roleDef) {
return roleId(roleDef) + "[[" + roleDef.getEntityAlias().aliasName() + roleDef.getRole() + "]]"; return roleId(roleDef) + "[[" + roleDef.getEntityAlias().aliasName() + roleDef.getRole() + "]]";
} }
private static String roleId(final RbacView.RbacRoleDefinition r) { private static String roleId(final RbacSpec.RbacRoleDefinition r) {
return "role:" + r.getEntityAlias().aliasName() + r.getRole(); return "role:" + r.getEntityAlias().aliasName() + r.getRole();
} }

View File

@ -11,11 +11,11 @@ import static net.hostsharing.hsadminng.rbac.generator.StringWriter.with;
public class RbacViewPostgresGenerator { public class RbacViewPostgresGenerator {
private final RbacView rbacDef; private final RbacSpec rbacDef;
private final String liqibaseTagPrefix; private final String liqibaseTagPrefix;
private final StringWriter plPgSql = new StringWriter(); private final StringWriter plPgSql = new StringWriter();
public RbacViewPostgresGenerator(final RbacView forRbacDef) { public RbacViewPostgresGenerator(final RbacSpec forRbacDef) {
rbacDef = forRbacDef; rbacDef = forRbacDef;
liqibaseTagPrefix = rbacDef.getRootEntityAlias().getRawTableNameWithSchema().replace("_", "-").replace(".", "-"); liqibaseTagPrefix = rbacDef.getRootEntityAlias().getRawTableNameWithSchema().replace("_", "-").replace(".", "-");
plPgSql.writeLn(""" plPgSql.writeLn("""
@ -31,6 +31,7 @@ public class RbacViewPostgresGenerator {
new InsertTriggerGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql); new InsertTriggerGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql);
new RbacIdentityViewGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql); new RbacIdentityViewGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql);
new RbacRestrictedViewGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql); new RbacRestrictedViewGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql);
new RbacRbacSystemRebuildGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql);
} }
@Override @Override

View File

@ -1,8 +1,8 @@
package net.hostsharing.hsadminng.rbac.generator; package net.hostsharing.hsadminng.rbac.generator;
import net.hostsharing.hsadminng.rbac.generator.RbacView.CaseDef; import net.hostsharing.hsadminng.rbac.generator.RbacSpec.CaseDef;
import net.hostsharing.hsadminng.rbac.generator.RbacView.RbacGrantDefinition; import net.hostsharing.hsadminng.rbac.generator.RbacSpec.RbacGrantDefinition;
import net.hostsharing.hsadminng.rbac.generator.RbacView.RbacPermissionDefinition; import net.hostsharing.hsadminng.rbac.generator.RbacSpec.RbacPermissionDefinition;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -15,22 +15,22 @@ import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toSet; import static java.util.stream.Collectors.toSet;
import static net.hostsharing.hsadminng.rbac.generator.PostgresTriggerReference.NEW; import static net.hostsharing.hsadminng.rbac.generator.PostgresTriggerReference.NEW;
import static net.hostsharing.hsadminng.rbac.generator.PostgresTriggerReference.OLD; import static net.hostsharing.hsadminng.rbac.generator.PostgresTriggerReference.OLD;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.INSERT; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacGrantDefinition.GrantType.*; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.RbacGrantDefinition.GrantType.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*; import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.*;
import static net.hostsharing.hsadminng.rbac.generator.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;
class RolesGrantsAndPermissionsGenerator { class RolesGrantsAndPermissionsGenerator {
private final RbacView rbacDef; private final RbacSpec rbacDef;
private final Set<RbacGrantDefinition> rbacGrants = new HashSet<>(); private final Set<RbacGrantDefinition> rbacGrants = new HashSet<>();
private final String liquibaseTagPrefix; private final String liquibaseTagPrefix;
private final String simpleEntityName; private final String simpleEntityName;
private final String simpleEntityVarName; private final String simpleEntityVarName;
private final String qualifiedRawTableName; private final String qualifiedRawTableName;
RolesGrantsAndPermissionsGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) { RolesGrantsAndPermissionsGenerator(final RbacSpec rbacDef, final String liquibaseTagPrefix) {
this.rbacDef = rbacDef; this.rbacDef = rbacDef;
this.rbacGrants.addAll(rbacDef.getGrantDefs().stream() this.rbacGrants.addAll(rbacDef.getGrantDefs().stream()
.filter(RbacGrantDefinition::isToCreate) .filter(RbacGrantDefinition::isToCreate)
@ -95,7 +95,7 @@ class RolesGrantsAndPermissionsGenerator {
private void generateSimplifiedUpdateTriggerFunction(final StringWriter plPgSql) { private void generateSimplifiedUpdateTriggerFunction(final StringWriter plPgSql) {
final var updateConditions = updatableEntityAliases() final var updateConditions = updatableEntityAliases()
.map(RbacView.EntityAlias::dependsOnColumName) .map(RbacSpec.EntityAlias::dependsOnColumName)
.distinct() .distinct()
.map(columnName -> "NEW." + columnName + " is distinct from OLD." + columnName) .map(columnName -> "NEW." + columnName + " is distinct from OLD." + columnName)
.collect(joining( "\n or ")); .collect(joining( "\n or "));
@ -112,7 +112,7 @@ class RolesGrantsAndPermissionsGenerator {
begin begin
if ${updateConditions} then if ${updateConditions} then
delete from rbac.grants g where g.grantedbytriggerof = OLD.uuid; delete from rbac.grant g where g.grantedbytriggerof = OLD.uuid;
call ${rawTableQualifiedName}_build_rbac_system(NEW); call ${rawTableQualifiedName}_build_rbac_system(NEW);
end if; end if;
end; $$; end; $$;
@ -166,7 +166,7 @@ class RolesGrantsAndPermissionsGenerator {
private boolean hasAnyUpdatableAndNullableEntityAliases() { private boolean hasAnyUpdatableAndNullableEntityAliases() {
return updatableEntityAliases() return updatableEntityAliases()
.filter(ea -> ea.nullable() == RbacView.Nullable.NULLABLE) .filter(ea -> ea.nullable() == RbacSpec.Nullable.NULLABLE)
.anyMatch(e -> true); .anyMatch(e -> true);
} }
@ -210,7 +210,7 @@ class RolesGrantsAndPermissionsGenerator {
generateGrants(plPgSql, PERM_TO_ROLE); generateGrants(plPgSql, PERM_TO_ROLE);
} }
private Stream<RbacView.EntityAlias> referencedEntityAliases() { private Stream<RbacSpec.EntityAlias> referencedEntityAliases() {
return rbacDef.getEntityAliases().values().stream() return rbacDef.getEntityAliases().values().stream()
.filter(ea -> !rbacDef.isRootEntityAlias(ea)) .filter(ea -> !rbacDef.isRootEntityAlias(ea))
.filter(ea -> ea.dependsOnColum() != null) .filter(ea -> ea.dependsOnColum() != null)
@ -218,7 +218,7 @@ class RolesGrantsAndPermissionsGenerator {
.filter(ea -> ea.fetchSql() != null); .filter(ea -> ea.fetchSql() != null);
} }
private Stream<RbacView.EntityAlias> updatableEntityAliases() { private Stream<RbacSpec.EntityAlias> updatableEntityAliases() {
return referencedEntityAliases() return referencedEntityAliases()
.filter(ea -> rbacDef.getUpdatableColumns().contains(ea.dependsOnColum().column)); .filter(ea -> rbacDef.getUpdatableColumns().contains(ea.dependsOnColum().column));
} }
@ -234,7 +234,7 @@ class RolesGrantsAndPermissionsGenerator {
}); });
updatableEntityAliases() updatableEntityAliases()
.map(RbacView.EntityAlias::dependsOnColum) .map(RbacSpec.EntityAlias::dependsOnColum)
.map(c -> c.column) .map(c -> c.column)
.sorted() .sorted()
.distinct() .distinct()
@ -250,18 +250,19 @@ class RolesGrantsAndPermissionsGenerator {
private void generateFetchedVars( private void generateFetchedVars(
final StringWriter plPgSql, final StringWriter plPgSql,
final RbacView.EntityAlias ea, final RbacSpec.EntityAlias ea,
final PostgresTriggerReference old) { final PostgresTriggerReference old) {
plPgSql.writeLn( plPgSql.writeLn(
ea.fetchSql().sql + " INTO " + entityRefVar(old, ea) + ";", ea.fetchSql().sql + " INTO " + entityRefVar(old, ea) + ";",
with("columns", ea.aliasName() + ".*"), with("columns", ea.aliasName() + ".*"),
with("ref", old.name())); with("ref", old.name()));
if (ea.nullable() == RbacView.Nullable.NOT_NULL) { if (ea.nullable() == RbacSpec.Nullable.NOT_NULL) {
plPgSql.writeLn( plPgSql.writeLn(
"assert ${entityRefVar}.uuid is not null, format('${entityRefVar} must not be null for ${REF}.${dependsOnColumn} = %s', ${REF}.${dependsOnColumn});", "assert ${entityRefVar}.uuid is not null, format('${entityRefVar} must not be null for ${REF}.${dependsOnColumn} = %s of ${rawTable}', ${REF}.${dependsOnColumn});",
with("entityRefVar", entityRefVar(old, ea)), with("entityRefVar", entityRefVar(old, ea)),
with("dependsOnColumn", ea.dependsOnColumName()), with("dependsOnColumn", ea.dependsOnColumName()),
with("ref", old.name())); with("ref", old.name()),
with("rawTable", qualifiedRawTableName));
plPgSql.writeLn(); plPgSql.writeLn();
} }
} }
@ -352,11 +353,11 @@ class RolesGrantsAndPermissionsGenerator {
.replace("${perm}", permDef.permission.name()); .replace("${perm}", permDef.permission.name());
} }
private String refVarName(final PostgresTriggerReference ref, final RbacView.EntityAlias entityAlias) { private String refVarName(final PostgresTriggerReference ref, final RbacSpec.EntityAlias entityAlias) {
return ref.name().toLowerCase() + capitalize(entityAlias.aliasName()); return ref.name().toLowerCase() + capitalize(entityAlias.aliasName());
} }
private String roleRef(final PostgresTriggerReference rootRefVar, final RbacView.RbacRoleDefinition roleDef) { private String roleRef(final PostgresTriggerReference rootRefVar, final RbacSpec.RbacRoleDefinition roleDef) {
if (roleDef == null) { if (roleDef == null) {
System.out.println("null"); System.out.println("null");
} }
@ -369,17 +370,17 @@ class RolesGrantsAndPermissionsGenerator {
private String entityRefVar( private String entityRefVar(
final PostgresTriggerReference rootRefVar, final PostgresTriggerReference rootRefVar,
final RbacView.EntityAlias entityAlias) { final RbacSpec.EntityAlias entityAlias) {
return rbacDef.isRootEntityAlias(entityAlias) return rbacDef.isRootEntityAlias(entityAlias)
? rootRefVar.name() ? rootRefVar.name()
: rootRefVar.name().toLowerCase() + capitalize(entityAlias.aliasName()); : rootRefVar.name().toLowerCase() + capitalize(entityAlias.aliasName());
} }
private void createRolesWithGrantsSql(final StringWriter plPgSql, final RbacView.Role role) { private void createRolesWithGrantsSql(final StringWriter plPgSql, final RbacSpec.Role role) {
final var isToCreate = rbacDef.getRoleDefs().stream() final var isToCreate = rbacDef.getRoleDefs().stream()
.filter(roleDef -> rbacDef.isRootEntityAlias(roleDef.getEntityAlias()) && roleDef.getRole() == role) .filter(roleDef -> rbacDef.isRootEntityAlias(roleDef.getEntityAlias()) && roleDef.getRole() == role)
.findFirst().map(RbacView.RbacRoleDefinition::isToCreate).orElse(false); .findFirst().map(RbacSpec.RbacRoleDefinition::isToCreate).orElse(false);
if (!isToCreate) { if (!isToCreate) {
return; return;
} }
@ -403,7 +404,7 @@ class RolesGrantsAndPermissionsGenerator {
plPgSql.writeLn(");"); plPgSql.writeLn(");");
} }
private void generateUserGrantsForRole(final StringWriter plPgSql, final RbacView.Role role) { private void generateUserGrantsForRole(final StringWriter plPgSql, final RbacSpec.Role role) {
final var grantsToUsers = findGrantsToUserForRole(rbacDef.getRootEntityAlias(), role); final var grantsToUsers = findGrantsToUserForRole(rbacDef.getRootEntityAlias(), role);
if (!grantsToUsers.isEmpty()) { if (!grantsToUsers.isEmpty()) {
final var arrayElements = grantsToUsers.stream() final var arrayElements = grantsToUsers.stream()
@ -416,13 +417,13 @@ class RolesGrantsAndPermissionsGenerator {
} }
} }
private void generatePermissionsForRole(final StringWriter plPgSql, final RbacView.Role role) { private void generatePermissionsForRole(final StringWriter plPgSql, final RbacSpec.Role role) {
final var permissionGrantsForRole = findPermissionsGrantsForRole(rbacDef.getRootEntityAlias(), role); final var permissionGrantsForRole = findPermissionsGrantsForRole(rbacDef.getRootEntityAlias(), role);
if (!permissionGrantsForRole.isEmpty()) { if (!permissionGrantsForRole.isEmpty()) {
final var arrayElements = permissionGrantsForRole.stream() final var arrayElements = permissionGrantsForRole.stream()
.map(RbacGrantDefinition::getPermDef) .map(RbacGrantDefinition::getPermDef)
.map(RbacPermissionDefinition::getPermission) .map(RbacPermissionDefinition::getPermission)
.map(RbacView.Permission::name) .map(RbacSpec.Permission::name)
.map(p -> "'" + p + "'") .map(p -> "'" + p + "'")
.sorted() .sorted()
.toList(); .toList();
@ -432,7 +433,7 @@ class RolesGrantsAndPermissionsGenerator {
} }
} }
private void generateIncomingSuperRolesForRole(final StringWriter plPgSql, final RbacView.Role role) { private void generateIncomingSuperRolesForRole(final StringWriter plPgSql, final RbacSpec.Role role) {
final var unconditionalIncomingGrants = findIncomingSuperRolesForRole(rbacDef.getRootEntityAlias(), role).stream() final var unconditionalIncomingGrants = findIncomingSuperRolesForRole(rbacDef.getRootEntityAlias(), role).stream()
.filter(g -> !g.isConditional()) .filter(g -> !g.isConditional())
.toList(); .toList();
@ -446,7 +447,7 @@ class RolesGrantsAndPermissionsGenerator {
} }
} }
private void generateOutgoingSubRolesForRole(final StringWriter plPgSql, final RbacView.Role role) { private void generateOutgoingSubRolesForRole(final StringWriter plPgSql, final RbacSpec.Role role) {
final var unconditionalOutgoingGrants = findOutgoingSuperRolesForRole(rbacDef.getRootEntityAlias(), role).stream() final var unconditionalOutgoingGrants = findOutgoingSuperRolesForRole(rbacDef.getRootEntityAlias(), role).stream()
.filter(g -> !g.isConditional()) .filter(g -> !g.isConditional())
.toList(); .toList();
@ -467,8 +468,8 @@ class RolesGrantsAndPermissionsGenerator {
} }
private Set<RbacGrantDefinition> findPermissionsGrantsForRole( private Set<RbacGrantDefinition> findPermissionsGrantsForRole(
final RbacView.EntityAlias entityAlias, final RbacSpec.EntityAlias entityAlias,
final RbacView.Role role) { final RbacSpec.Role role) {
final var roleDef = rbacDef.findRbacRole(entityAlias, role); final var roleDef = rbacDef.findRbacRole(entityAlias, role);
return rbacGrants.stream() return rbacGrants.stream()
.filter(g -> g.grantType() == PERM_TO_ROLE && g.getSuperRoleDef() == roleDef) .filter(g -> g.grantType() == PERM_TO_ROLE && g.getSuperRoleDef() == roleDef)
@ -476,8 +477,8 @@ class RolesGrantsAndPermissionsGenerator {
} }
private Set<RbacGrantDefinition> findGrantsToUserForRole( private Set<RbacGrantDefinition> findGrantsToUserForRole(
final RbacView.EntityAlias entityAlias, final RbacSpec.EntityAlias entityAlias,
final RbacView.Role role) { final RbacSpec.Role role) {
final var roleDef = rbacDef.findRbacRole(entityAlias, role); final var roleDef = rbacDef.findRbacRole(entityAlias, role);
return rbacGrants.stream() return rbacGrants.stream()
.filter(g -> g.grantType() == ROLE_TO_USER && g.getSubRoleDef() == roleDef) .filter(g -> g.grantType() == ROLE_TO_USER && g.getSubRoleDef() == roleDef)
@ -485,8 +486,8 @@ class RolesGrantsAndPermissionsGenerator {
} }
private Set<RbacGrantDefinition> findIncomingSuperRolesForRole( private Set<RbacGrantDefinition> findIncomingSuperRolesForRole(
final RbacView.EntityAlias entityAlias, final RbacSpec.EntityAlias entityAlias,
final RbacView.Role role) { final RbacSpec.Role role) {
final var roleDef = rbacDef.findRbacRole(entityAlias, role); final var roleDef = rbacDef.findRbacRole(entityAlias, role);
return rbacGrants.stream() return rbacGrants.stream()
.filter(g -> g.grantType() == ROLE_TO_ROLE && g.getSubRoleDef() == roleDef) .filter(g -> g.grantType() == ROLE_TO_ROLE && g.getSubRoleDef() == roleDef)
@ -494,8 +495,8 @@ class RolesGrantsAndPermissionsGenerator {
} }
private Set<RbacGrantDefinition> findOutgoingSuperRolesForRole( private Set<RbacGrantDefinition> findOutgoingSuperRolesForRole(
final RbacView.EntityAlias entityAlias, final RbacSpec.EntityAlias entityAlias,
final RbacView.Role role) { final RbacSpec.Role role) {
final var roleDef = rbacDef.findRbacRole(entityAlias, role); final var roleDef = rbacDef.findRbacRole(entityAlias, role);
return rbacGrants.stream() return rbacGrants.stream()
.filter(g -> g.grantType() == ROLE_TO_ROLE && g.getSuperRoleDef() == roleDef) .filter(g -> g.grantType() == ROLE_TO_ROLE && g.getSuperRoleDef() == roleDef)
@ -579,7 +580,7 @@ class RolesGrantsAndPermissionsGenerator {
plPgSql.writeLn(); plPgSql.writeLn();
} }
private String toPlPgSqlReference(final RbacView.RbacSubjectReference userRef) { private String toPlPgSqlReference(final RbacSpec.RbacSubjectReference userRef) {
return switch (userRef.role) { return switch (userRef.role) {
case CREATOR -> "rbac.currentSubjectUuid()"; case CREATOR -> "rbac.currentSubjectUuid()";
default -> throw new IllegalArgumentException("unknown user role: " + userRef); default -> throw new IllegalArgumentException("unknown user role: " + userRef);
@ -588,7 +589,7 @@ class RolesGrantsAndPermissionsGenerator {
private String toPlPgSqlReference( private String toPlPgSqlReference(
final PostgresTriggerReference triggerRef, final PostgresTriggerReference triggerRef,
final RbacView.RbacRoleDefinition roleDef, final RbacSpec.RbacRoleDefinition roleDef,
final boolean assumed) { final boolean assumed) {
final var assumedArg = assumed ? "" : ", rbac.unassumed()"; final var assumedArg = assumed ? "" : ", rbac.unassumed()";
return roleDef.descriptorFunctionName() + return roleDef.descriptorFunctionName() +
@ -599,7 +600,7 @@ class RolesGrantsAndPermissionsGenerator {
private static String toTriggerReference( private static String toTriggerReference(
final PostgresTriggerReference triggerRef, final PostgresTriggerReference triggerRef,
final RbacView.EntityAlias entityAlias) { final RbacSpec.EntityAlias entityAlias) {
return triggerRef.name().toLowerCase() + capitalize(entityAlias.aliasName()); return triggerRef.name().toLowerCase() + capitalize(entityAlias.aliasName());
} }
} }

View File

@ -12,7 +12,7 @@ import java.util.List;
import java.util.UUID; import java.util.UUID;
@Entity @Entity
@Table(schema = "rbac", name = "grants_ev") @Table(schema = "rbac", name = "grant_ev")
@Getter @Getter
@Setter @Setter
@Builder @Builder

View File

@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.rbac.grant;
import io.micrometer.core.annotation.Timed; import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.mapper.StandardMapper; import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacGrantsApi; import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacGrantsApi;
import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacGrantResource; import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacGrantResource;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -23,7 +23,7 @@ public class RbacGrantController implements RbacGrantsApi {
private Context context; private Context context;
@Autowired @Autowired
private StandardMapper mapper; private StrictMapper mapper;
@Autowired @Autowired
private RbacGrantRepository rbacGrantRepository; private RbacGrantRepository rbacGrantRepository;

View File

@ -8,7 +8,7 @@ import jakarta.persistence.*;
import java.util.UUID; import java.util.UUID;
@Entity @Entity
@Table(schema = "rbac", name = "grants_rv") @Table(schema = "rbac", name = "grant_rv")
@IdClass(RbacGrantId.class) @IdClass(RbacGrantId.class)
@Getter @Getter
@Setter @Setter

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