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)
# and amend them according to your external DB.
@ -71,7 +71,6 @@ function importLegacyData() {
./gradlew $target --rerun
fi
}
alias gw-importOfficeData='importLegacyData importOfficeData'
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'
@ -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 gw-spotless='./gradlew spotlessApply -x pitest -x test -x :processResources'
alias gw-test='. .aliases; ./gradlew test'
alias gw-check='. .aliases; gw test check -x pitest'
alias gw-check='. .aliases; . .tc-environment; 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
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 up -d && sleep 10 &&
time gw-importHostingAssets'
@ -104,5 +146,6 @@ if [ ! -f .environment ]; then
fi
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'

View File

@ -7,6 +7,7 @@
<entry key="HSADMINNG_POSTGRES_ADMIN_USERNAME" value="postgres" />
<entry key="HSADMINNG_POSTGRES_JDBC_URL" value="jdbc:postgresql://localhost:5432/postgres" />
<entry key="HSADMINNG_POSTGRES_RESTRICTED_USERNAME" value="restricted" />
<entry key="HSADMINNG_MIGRATION_DATA_PATH" value="migration" />
</map>
</option>
<option name="executionName" />
@ -34,4 +35,4 @@
<RunAsTest>true</RunAsTest>
<method v="2" />
</configuration>
</component>
</component>

View File

@ -3,9 +3,9 @@
<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" />
<entry key="HSADMINNG_SUPERUSER" value="import-superuser@hostsharing.net" />
</map>
</option>
<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
export HSADMINNG_POSTGRES_ADMIN_USERNAME=admin
export HSADMINNG_POSTGRES_ADMIN_PASSWORD=
source .unset-environment
export HSADMINNG_POSTGRES_RESTRICTED_USERNAME=restricted
export HSADMINNG_SUPERUSER=superuser-alex@hostsharing.net
export HSADMINNG_MIGRATION_DATA_PATH=migration
export LIQUIBASE_CONTEXT=
export HSADMINNG_POSTGRES_ADMIN_USERNAME=admin
export HSADMINNG_SUPERUSER=import-superuser@hostsharing.net
export LANG=en_US.UTF-8

View File

@ -4,5 +4,5 @@ unset HSADMINNG_POSTGRES_ADMIN_PASSWORD
unset HSADMINNG_POSTGRES_RESTRICTED_USERNAME
unset HSADMINNG_SUPERUSER
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') {
parallel {
stage('Unit-/Integration/Acceptance-Tests') {
stage('Unit-Tests') {
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 {
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') {
steps {
sh './gradlew scenarioTests --no-daemon'
sh './gradlew scenarioTest --no-daemon'
}
}
}

214
README.md
View File

@ -5,41 +5,48 @@ For architecture consider the files in the `doc` and `adr` folder.
<!-- generated TOC begin: -->
- [Setting up the Development Environment](#setting-up-the-development-environment)
- [PostgreSQL Server](#postgresql-server)
- [Markdown](#markdown)
- [Render Markdown embedded PlantUML](#render-markdown-embedded-plantuml)
- [Render Markdown Embedded Mermaid Diagrams](#render-markdown-embedded-mermaid-diagrams)
- [IDE Specific Settings](#ide-specific-settings)
- [IntelliJ IDEA](#intellij-idea)
- [Other Tools](#other-tools)
- [PostgreSQL Server](#postgresql-server)
- [Markdown](#markdown)
- [Render Markdown embedded PlantUML](#render-markdown-embedded-plantuml)
- [Render Markdown Embedded Mermaid Diagrams](#render-markdown-embedded-mermaid-diagrams)
- [IDE Specific Settings](#ide-specific-settings)
- [IntelliJ IDEA](#intellij-idea)
- [Other Tools](#other-tools)
- [Running the SQL files](#running-the-sql-files)
- [For RBAC](#for-rbac)
- [For Historization](#for-historization)
- [For RBAC](#for-rbac)
- [For Historization](#for-historization)
- [Coding Guidelines](#coding-guidelines)
- [Directory and Package Structure](#directory-and-package-structure)
- [General Directory Structure](#general-directory-structure)
- [Source Code Package Structure](#source-code-package-structure)
- [Run Tests from Command Line](#run-tests-from-command-line)
- [Spotless Code Formatting](#spotless-code-formatting)
- [JaCoCo Test Code Coverage Check](#jacoco-test-code-coverage-check)
- [PiTest Mutation Testing](#pitest-mutation-testing)
- [Remark](#remark)
- [OWASP Security Vulnerability Check](#owasp-security-vulnerability-check)
- [Dependency-License-Compatibility](#dependency-license-compatibility)
- [Dependency Version Upgrade](#dependency-version-upgrade)
- [Directory and Package Structure](#directory-and-package-structure)
- [General Directory Structure](#general-directory-structure)
- [Source Code Package Structure](#source-code-package-structure)
- [Run Tests from Command Line](#run-tests-from-command-line)
- [Spotless Code Formatting](#spotless-code-formatting)
- [JaCoCo Test Code Coverage Check](#jacoco-test-code-coverage-check)
- [PiTest Mutation Testing](#pitest-mutation-testing)
- [Remark](#remark)
- [OWASP Security Vulnerability Check](#owasp-security-vulnerability-check)
- [Dependency-License-Compatibility](#dependency-license-compatibility)
- [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 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?)
- [Install and Run Podman](#install-and-run-podman)
- [Use the Command Line to Run the Tests Against the Podman Daemon ](#use-the-command-line-to-run-the-tests-against-the-podman-daemon-)
- [Use IntelliJ IDEA Run the Tests Against the Podman Daemon](#use-intellij-idea-run-the-tests-against-the-podman-daemon)
- [~/.testcontainers.properties](#~/.testcontainers.properties)
- [How to Run the Tests Against a Remote Podman or Docker Daemon?](#how-to-run-the-tests-against-a-remote-podman-or-docker-daemon?)
- [How to Run the Application on a Different Port?](#how-to-run-the-application-on-a-different-port?)
- [How to Use a Persistent Database for Integration Tests?](#how-to-use-a-persistent-database-for-integration-tests?)
- [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 Generate Database Table Diagrams?](#how-to-generate-database-table-diagrams?)
- [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 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)
- [Use the Command Line to Run the Tests Against the Podman Daemon ](#use-the-command-line-to-run-the-tests-against-the-podman-daemon-)
- [Use IntelliJ IDEA Run the Tests Against the Podman Daemon](#use-intellij-idea-run-the-tests-against-the-podman-daemon)
- [~/.testcontainers.properties](#~/.testcontainers.properties)
- [How to Run the Tests Against a Remote Podman or Docker Daemon?](#how-to-run-the-tests-against-a-remote-podman-or-docker-daemon?)
- [How to Run the Application on a Different Port?](#how-to-run-the-application-on-a-different-port?)
- [How to Use a Persistent Database for Integration Tests?](#how-to-use-a-persistent-database-for-integration-tests?)
- [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 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)
<!-- 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:
- 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)
- 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*.
- 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:
@ -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 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:
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:
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":
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:
curl -f\
curl -f -s\
-H 'current-subject: superuser-alex@hostsharing.net' \
http://localhost:8080/api/test/customers \
| 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:
curl -f\
curl -f -s\
-H 'current-subject: superuser-alex@hostsharing.net' -H 'assumed-roles: rbactest.customer#yyy:ADMIN' \
http://localhost:8080/api/test/packages \
| jq
# add a new customer
curl -f\
curl -f -s\
-H 'current-subject: superuser-alex@hostsharing.net' -H "Content-Type: application/json" \
-d '{ "prefix":"ttt", "reference":80001, "adminUserName":"admin@ttt.example.com" }' \
-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.
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.
@ -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:
```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:
@ -419,36 +450,42 @@ Some of these rules are checked with *ArchUnit* unit tests.
### 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
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:
```shell
gw cleanTest test
gw-test --rerun
```
To find more options about running tests, try `howto test`.
### Spotless Code Formatting
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:
```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
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 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
In this project, there is little business logic in *Java* code;
most business code is in *plsql*
and *Java* ist mostly used for mapping REST calls to database queries.
In this project, there is a large amount of code is in *plsql*, especially for RBAC.
*Java* ist mostly used for mapping and validating REST calls to database queries.
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.
@ -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.
### Dependency-License-Compatibility
### How to Check Dependency-License-Compatibility
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.
@ -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).
### Dependency Version Upgrade
### How to Upgrade Versions of Dependencies
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.
We would also reduce the depth of the expensive recursive CTE-query.
This has to be explored further.
For now, we just keep it in mind and
This has to be explored further. For now, we just keep it in mind and avoid roles+grants
which would not fit into a simplified system with a fixed role-type-system.
### 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
```
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?
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.
### 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
- 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 {
id 'java'
id 'org.springframework.boot' version '3.3.4'
id 'io.spring.dependency-management' version '1.1.6'
id 'io.openapiprocessor.openapi-processor' version '2023.2'
id 'com.github.jk1.dependency-license-report' version '2.9'
id "org.owasp.dependencycheck" version "10.0.4"
id "com.diffplug.spotless" version "6.25.0"
id 'jacoco'
id 'info.solidsoft.pitest' version '1.15.0'
id 'se.patrikerdes.use-latest-versions' version '0.2.18'
id 'com.github.ben-manes.versions' version '0.51.0'
id 'org.springframework.boot' version '3.4.1'
id 'io.spring.dependency-management' version '1.1.7' // manages implicit dependencies
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' // checks dependency-license compatibility
id "org.owasp.dependencycheck" version "12.0.1" // checks dependencies for known vulnerabilities
id "com.diffplug.spotless" version "7.0.2" // formats + checks formatting for source-code
id 'jacoco' // determines code-coverage of tests
id 'info.solidsoft.pitest' version '1.15.0' // performs mutation testing
id 'se.patrikerdes.use-latest-versions' version '0.2.18' // updates module and plugin versions
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'
version = '0.0.1-SNAPSHOT'
@ -20,6 +23,9 @@ wrapper {
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 {
compileOnly {
extendsFrom annotationProcessor
@ -60,25 +66,25 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2'
implementation 'org.springdoc:springdoc-openapi:2.6.0'
implementation 'org.postgresql:postgresql:42.7.4'
implementation 'org.liquibase:liquibase-core:4.29.2'
implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.8.3'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.0'
implementation 'com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.10.0'
implementation 'org.springdoc:springdoc-openapi:2.8.3'
implementation 'org.postgresql:postgresql'
implementation 'org.liquibase:liquibase-core'
implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.9.0'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
implementation 'org.openapitools:jackson-databind-nullable:0.2.6'
implementation 'org.apache.commons:commons-text:1.12.0'
implementation 'net.java.dev.jna:jna:5.15.0'
implementation 'org.modelmapper:modelmapper:3.2.1'
implementation 'org.apache.commons:commons-text:1.13.0'
implementation 'net.java.dev.jna:jna:5.16.0'
implementation 'org.modelmapper:modelmapper:3.2.2'
implementation 'org.iban4j:iban4j:3.2.10-RELEASE'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'
implementation 'org.webjars:swagger-ui:5.17.14'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.3'
implementation 'org.reflections:reflections:0.10.2'
compileOnly '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'
testAnnotationProcessor 'org.projectlombok:lombok'
@ -90,9 +96,10 @@ dependencies {
testImplementation 'org.testcontainers:postgresql'
testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
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.junit.jupiter:junit-jupiter-api'
testImplementation 'org.wiremock:wiremock-standalone:3.10.0'
}
dependencyManagement {
@ -173,7 +180,9 @@ openapiProcessor {
}
}
sourceSets.main.java.srcDir 'build/generated/sources/openapi'
abstract class ProcessSpring extends DefaultTask {}
tasks.register('processSpring', ProcessSpring)
['processSpringRoot',
'processSpringRbac',
@ -204,7 +213,7 @@ openApiGenerate.dependsOn processSpring
spotless {
java {
removeUnusedImports()
indentWithSpaces(4)
leadingTabsToSpaces(4)
endWithNewline()
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
project.tasks.spotlessJava.dependsOn(
tasks.generateLicenseReport,
tasks.pitest,
// tasks.pitest, TODO.test: PiTest currently does not work, needs to be fixed
tasks.jacocoTestReport,
tasks.processResources,
tasks.processTestResources)
@ -247,19 +256,21 @@ licenseReport {
}
project.tasks.check.dependsOn(checkLicense)
// JaCoCo Test Code Coverage
jacoco {
toolVersion = "0.8.10"
}
// HOWTO: run all tests except import- and scenario-tests: gw test
test {
finalizedBy jacocoTestReport // generate report after tests
excludes = [
'net.hostsharing.hsadminng.**.generated.**',
]
useJUnitPlatform {
excludeTags 'importOfficeData', 'importHostingData', 'scenarioTest'
excludeTags 'importHostingAssets', 'scenarioTest'
}
}
// JaCoCo Test Code Coverage for unit-tests
jacoco {
toolVersion = "0.8.10"
}
jacocoTestReport {
dependsOn test
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 {
includeTags 'importOfficeData'
excludeTags 'importHostingAssets', 'scenarioTest', 'generalIntegrationTest',
'officeIntegrationTest', 'bookingIntegrationTest', 'hostingIntegrationTest'
}
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
}
@ -346,7 +407,7 @@ tasks.register('importHostingAssets', Test) {
mustRunAfter spotlessJava
}
tasks.register('scenarioTests', Test) {
tasks.register('scenarioTest', Test) {
useJUnitPlatform {
includeTags 'scenarioTest'
}
@ -367,7 +428,7 @@ pitest {
]
targetTests = ['net.hostsharing.hsadminng.**.*UnitTest', 'net.hostsharing.hsadminng.**.*RestTest']
excludedTestClasses = ['**AcceptanceTest*', '**IntegrationTest*']
excludedTestClasses = ['**AcceptanceTest*', '**IntegrationTest*', '**ImportHostingAssets']
pitestVersion = '1.17.0'
junit5PluginVersion = '1.1.0'
@ -382,7 +443,7 @@ pitest {
outputFormats = ['XML', 'HTML']
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!
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
tasks.register('compile') {

View File

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

View File

@ -5,9 +5,23 @@
{ "moduleLicense": "Apache-2.0" },
{ "moduleLicense": "Apache License 2.0" },
{ "moduleLicense": "Apache License v2.0" },
{ "moduleLicense": "Apache License Version 2.0" },
{ "moduleLicense": "Apache License, Version 2.0" },
{ "moduleLicense": "The Apache 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-2-Clause" },
{ "moduleLicense": "BSD-3-Clause" },
@ -46,14 +60,8 @@
{
"moduleLicense": "Public Domain, per Creative Commons CC0",
"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>
<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>
<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>
</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.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@ -22,6 +23,14 @@ public class WebSecurityConfig {
.requestMatchers("/actuator/**").permitAll()
.anyRequest().authenticated()
)
.csrf(AbstractHttpConfigurer::disable)
.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(:currentRequest as text),
cast(:currentSubject as varchar(63)),
cast(:assumedRoles as varchar(1023)));
cast(:assumedRoles as text));
""");
query.setParameter("currentTask", shortenToMaxLength(currentTask, 127));
query.setParameter("currentRequest", currentRequest);

View File

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

View File

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

View File

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

View File

@ -1,10 +1,12 @@
package net.hostsharing.hsadminng.hs.booking.item;
import io.micrometer.core.annotation.Timed;
import org.springframework.context.annotation.Profile;
import org.springframework.data.repository.Repository;
import java.util.UUID;
@Profile("!only-office")
public interface BookingItemCreatedEventRepository extends Repository<BookingItemCreatedEventEntity, UUID> {
@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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Profile;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
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;
@RestController
@Profile("!only-office")
public class HsBookingItemController implements HsBookingItemsApi {
@Autowired

View File

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

View File

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

View File

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

View File

@ -1,10 +1,13 @@
package net.hostsharing.hsadminng.hs.booking.item;
import org.springframework.context.annotation.Profile;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Profile("!only-office")
public interface HsBookingItemRepository<E extends HsBookingItem> {
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.HsBookingProjectPatchResource;
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.context.annotation.Profile;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
@ -20,13 +21,14 @@ import java.util.UUID;
import java.util.function.BiConsumer;
@RestController
@Profile("!only-office")
public class HsBookingProjectController implements HsBookingProjectsApi {
@Autowired
private Context context;
@Autowired
private StandardMapper mapper;
private StrictMapper mapper;
@Autowired
private HsBookingProjectRbacRepository bookingProjectRepo;

View File

@ -6,30 +6,30 @@ import lombok.Setter;
import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
import net.hostsharing.hsadminng.rbac.generator.RbacView;
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import java.io.IOException;
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.RbacView.ColumnValue.usingCase;
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.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.DELETE;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.UPDATE;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.AGENT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.OWNER;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.TENANT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.fetchedBySql;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingCase;
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.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.DELETE;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.UPDATE;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.AGENT;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.OWNER;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.TENANT;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.fetchedBySql;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
@Entity
@Table(schema = "hs_booking", name = "project_rv")
@ -39,7 +39,7 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
@NoArgsConstructor
public class HsBookingProjectRbacEntity extends HsBookingProject {
public static RbacView rbac() {
public static RbacSpec rbac() {
return rbacViewFor("project", HsBookingProjectRbacEntity.class)
.withIdentityView(SQL.query("""
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;
import io.micrometer.core.annotation.Timed;
import org.springframework.context.annotation.Profile;
import org.springframework.data.repository.Repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Profile("!only-office")
public interface HsBookingProjectRbacRepository extends HsBookingProjectRepository<HsBookingProjectRbacEntity>,
Repository<HsBookingProjectRbacEntity, UUID> {

View File

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

View File

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

View File

@ -89,7 +89,7 @@ public abstract class HsHostingAsset implements Stringifyable, BaseEntity<HsHost
@JoinColumn(name = "alarmcontactuuid")
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")
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.HsHostingAssetTypeResource;
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
@ -27,6 +28,7 @@ import java.util.UUID;
import java.util.function.BiConsumer;
@RestController
@Profile("!only-office")
public class HsHostingAssetController implements HsHostingAssetsApi {
@Autowired
@ -36,7 +38,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
private Context context;
@Autowired
private StandardMapper mapper;
private StrictMapper mapper;
@Autowired
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.generated.api.v1.api.HsHostingAssetPropsApi;
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.web.bind.annotation.RestController;
@ -12,6 +13,7 @@ import java.util.Map;
@RestController
@Profile("!only-office")
public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
@Override

View File

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

View File

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

View File

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

View File

@ -1,11 +1,13 @@
package net.hostsharing.hsadminng.hs.hosting.asset;
import io.micrometer.core.annotation.Timed;
import org.springframework.context.annotation.Profile;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Profile("!only-office")
public interface HsHostingAssetRepository<E extends HsHostingAsset> {
@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.office.contact.HsOfficeContactRealEntity;
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.persistence.EntityManagerWrapper;
@ -31,8 +31,8 @@ public class DomainSetupHostingAssetFactory extends HostingAssetFactory {
final EntityManagerWrapper emw,
final HsBookingItemRealEntity newBookingItemRealEntity,
final HsHostingAssetAutoInsertResource asset,
final StandardMapper standardMapper) {
super(emw, newBookingItemRealEntity, asset, standardMapper);
final StrictMapper StrictMapper) {
super(emw, newBookingItemRealEntity, asset, StrictMapper);
}
@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.hosting.asset.HsHostingAsset;
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;
@ -16,7 +16,7 @@ abstract class HostingAssetFactory {
final EntityManagerWrapper emw;
final HsBookingItemRealEntity fromBookingItem;
final HsHostingAssetAutoInsertResource asset;
final StandardMapper standardMapper;
final StrictMapper StrictMapper;
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.HsBookingItemRealEntity;
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
@Component
@Profile("!only-office")
public class HsBookingItemCreatedListener implements ApplicationListener<BookingItemCreatedAppEvent> {
@Autowired
@ -25,7 +27,7 @@ public class HsBookingItemCreatedListener implements ApplicationListener<Booking
private ObjectMapper jsonMapper;
@Autowired
private StandardMapper standardMapper;
private StrictMapper StrictMapper;
@Override
@SneakyThrows
@ -44,9 +46,9 @@ public class HsBookingItemCreatedListener implements ApplicationListener<Booking
final var asset = jsonMapper.readValue(event.getEntity().getAssetJson(), HsHostingAssetAutoInsertResource.class);
final var factory = switch (newBookingItemRealEntity.getType()) {
case PRIVATE_CLOUD, CLOUD_SERVER, MANAGED_SERVER ->
forNowNoAutomaticHostingAssetCreationPossible(emw, newBookingItemRealEntity, asset, standardMapper);
case MANAGED_WEBSPACE -> new ManagedWebspaceHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper);
case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper);
forNowNoAutomaticHostingAssetCreationPossible(emw, newBookingItemRealEntity, asset, StrictMapper);
case MANAGED_WEBSPACE -> new ManagedWebspaceHostingAssetFactory(emw, newBookingItemRealEntity, asset, StrictMapper);
case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(emw, newBookingItemRealEntity, asset, StrictMapper);
};
if (factory != null) {
final var statusMessage = factory.createAndPersist();
@ -62,9 +64,9 @@ public class HsBookingItemCreatedListener implements ApplicationListener<Booking
final EntityManagerWrapper emw,
final HsBookingItemRealEntity fromBookingItem,
final HsHostingAssetAutoInsertResource asset,
final StandardMapper standardMapper
final StrictMapper StrictMapper
) {
return new HostingAssetFactory(emw, fromBookingItem, asset, standardMapper) {
return new HostingAssetFactory(emw, fromBookingItem, asset, StrictMapper) {
@Override
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.hosting.asset.HsHostingAsset;
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 jakarta.validation.ValidationException;
@ -19,8 +19,8 @@ public class ManagedWebspaceHostingAssetFactory extends HostingAssetFactory {
final EntityManagerWrapper emw,
final HsBookingItemRealEntity newBookingItemRealEntity,
final HsHostingAssetAutoInsertResource asset,
final StandardMapper standardMapper) {
super(emw, newBookingItemRealEntity, asset, standardMapper);
final StrictMapper StrictMapper) {
super(emw, newBookingItemRealEntity, asset, StrictMapper);
}
@Override
@ -32,7 +32,7 @@ public class ManagedWebspaceHostingAssetFactory extends HostingAssetFactory {
.map(Enum::name)
.orElse(null));
}
final var managedWebspaceHostingAsset = standardMapper.map(asset, HsHostingAssetRealEntity.class);
final var managedWebspaceHostingAsset = StrictMapper.map(asset, HsHostingAssetRealEntity.class);
managedWebspaceHostingAsset.setBookingItem(fromBookingItem);
emw.createQuery(
"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.model.HsOfficeBankAccountInsertResource;
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.IbanUtil;
import org.springframework.beans.factory.annotation.Autowired;
@ -25,7 +25,7 @@ public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
private Context context;
@Autowired
private StandardMapper mapper;
private StrictMapper mapper;
@Autowired
private HsOfficeBankAccountRepository bankAccountRepo;

View File

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

View File

@ -12,6 +12,7 @@ import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.role.WithRoleId;
import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable;
import org.hibernate.annotations.GenericGenerator;
@ -37,7 +38,7 @@ import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@SuperBuilder(toBuilder = true)
@FieldNameConstants
@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")
.withProp(Fields.caption, HsOfficeContact::getCaption)

View File

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

View File

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

View File

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

View File

@ -1,14 +1,13 @@
package net.hostsharing.hsadminng.hs.office.coopshares;
import jakarta.persistence.EntityNotFoundException;
import io.micrometer.core.annotation.Timed;
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.model.HsOfficeCoopSharesTransactionInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionResource;
import net.hostsharing.hsadminng.errors.MultiValidationException;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipRepository;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
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.SUBSCRIPTION;
import static net.hostsharing.hsadminng.hs.validation.UuidResolver.resolve;
@RestController
public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopSharesApi {
@ -33,14 +33,16 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
private Context context;
@Autowired
private StandardMapper mapper;
private StrictMapper mapper;
@Autowired
private HsOfficeCoopSharesTransactionRepository coopSharesTransactionRepo;
@Autowired
private HsOfficeMembershipRepository membershipRepo;
@Override
@Transactional(readOnly = true)
@Timed("app.office.coopShares.api.getListOfCoopShares")
public ResponseEntity<List<HsOfficeCoopSharesTransactionResource>> getListOfCoopShares(
final String currentSubject,
@ -55,7 +57,10 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
fromValueDate,
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);
}
@ -70,7 +75,10 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
context.define(currentSubject, assumedRoles);
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);
@ -79,7 +87,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
.path("/api/hs/office/coopsharestransactions/{id}")
.buildAndExpand(saved.getUuid())
.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);
}
@ -87,15 +95,18 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
@Transactional(readOnly = true)
@Timed("app.office.coopShares.repo.getSingleCoopShareTransactionByUuid")
public ResponseEntity<HsOfficeCoopSharesTransactionResource> getSingleCoopShareTransactionByUuid(
final String currentSubject, final String assumedRoles, final UUID shareTransactionUuid) {
final String currentSubject, final String assumedRoles, final UUID shareTransactionUuid) {
context.define(currentSubject, assumedRoles);
context.define(currentSubject, assumedRoles);
final var result = coopSharesTransactionRepo.findByUuid(shareTransactionUuid);
if (result.isEmpty()) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(mapper.map(result.get(), HsOfficeCoopSharesTransactionResource.class));
final var result = coopSharesTransactionRepo.findByUuid(shareTransactionUuid);
if (result.isEmpty()) {
return ResponseEntity.notFound().build();
}
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) -> {
if ( resource.getRevertedShareTxUuid() != null ) {
entity.setRevertedShareTx(coopSharesTransactionRepo.findByUuid(resource.getRevertedShareTxUuid())
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] revertedShareTxUuid %s not found".formatted(resource.getRevertedShareTxUuid()))));
entity.setMembership(resolve("membership.uuid", resource.getMembershipUuid(), membershipRepo::findByUuid));
if (resource.getRevertedShareTxUuid() != null) {
entity.setRevertedShareTx(resolve(
"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 net.hostsharing.hsadminng.errors.DisplayAs;
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.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.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.time.LocalDate;
import java.util.UUID;
import static java.util.Optional.ofNullable;
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.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.UPDATE;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.AGENT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
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.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.UPDATE;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.AGENT;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@Entity
@ -123,7 +133,7 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, BaseE
return "%s:%.3s:%+d".formatted(getMemberNumberTagged(), transactionType, shareCount);
}
public static RbacView rbac() {
public static RbacSpec rbac() {
return rbacViewFor("coopSharesTransaction", HsOfficeCoopSharesTransactionEntity.class)
.withIdentityView(SQL.projection("reference"))
.withUpdatableColumns("comment")
@ -132,6 +142,7 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, BaseE
directlyFetchedByDependsOnColumn(),
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(UPDATE)
.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 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.model.HsOfficeDebitorInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorPatchResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorResource;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealRepository;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.persistence.EntityExistsValidator;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import org.apache.commons.lang3.Validate;
import org.hibernate.Hibernate;
import org.springframework.beans.factory.annotation.Autowired;
@ -26,6 +28,7 @@ import java.util.UUID;
import java.util.function.BiConsumer;
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;
@RestController
@ -36,16 +39,22 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
private Context context;
@Autowired
private StandardMapper mapper;
private StrictMapper mapper;
@Autowired
private HsOfficeDebitorRepository debitorRepo;
@Autowired
private HsOfficeRelationRealRepository relrealRepo;
private HsOfficeRelationRealRepository realRelRepo;
@Autowired
private EntityExistsValidator entityValidator;
private HsOfficePersonRealRepository realPersonRepo;
@Autowired
private HsOfficeContactRealRepository realContactRepo;
@Autowired
private HsOfficeBankAccountRepository bankAccountRepo;
@PersistenceContext
private EntityManager em;
@ -63,9 +72,9 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
final var entities = partnerNumber != null
? debitorRepo.findDebitorsByPartnerNumber(cropTag("P-", partnerNumber))
: partnerUuid != null
? debitorRepo.findDebitorsByPartnerUuid(partnerUuid)
: debitorRepo.findDebitorsByOptionalNameLike(name);
: partnerUuid != null
? debitorRepo.findDebitorsByPartnerUuid(partnerUuid)
: debitorRepo.findDebitorsByOptionalNameLike(name);
final var resources = mapper.mapList(entities, HsOfficeDebitorResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(resources);
@ -81,34 +90,19 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
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");
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");
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");
final var entityToSave = mapper.map(body, HsOfficeDebitorEntity.class);
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 entityToSave = mapper.map(body, HsOfficeDebitorEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
final var savedEntity = debitorRepo.save(entityToSave);
em.flush();
em.refresh(savedEntity);
final var savedEntity = debitorRepo.save(entityToSave).reload(em);
final var uri =
MvcUriComponentsBuilder.fromController(getClass())
@ -181,7 +175,7 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
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);
@ -191,7 +185,39 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
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) -> {
resource.setDebitorNumber(entity.getTaggedDebitorNumber());
resource.getPartner().setPartnerNumber(entity.getPartner().getTaggedPartnerNumber());
};
}

View File

@ -7,16 +7,16 @@ import lombok.NoArgsConstructor;
import lombok.Setter;
import net.hostsharing.hsadminng.errors.DisplayAs;
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.HsOfficeRelationRealEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.generator.RbacView;
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.Stringifyable;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.JoinFormula;
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
@ -40,17 +40,17 @@ import static jakarta.persistence.CascadeType.PERSIST;
import static jakarta.persistence.CascadeType.REFRESH;
import static java.util.Optional.ofNullable;
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.RbacView.ColumnValue.usingCase;
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.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NULLABLE;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*;
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.SQL.fetchedBySql;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingCase;
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.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Nullable.NULLABLE;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.*;
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.SQL.fetchedBySql;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@Entity
@ -75,7 +75,6 @@ public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>,
@Id
@GeneratedValue
@GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
private UUID uuid;
@Version
@ -87,16 +86,16 @@ public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>,
value = """
(
SELECT DISTINCT partner.uuid
FROM hs_office.partner_rv partner
JOIN hs_office.relation_rv dRel
ON dRel.uuid = debitorreluuid AND dRel.type = 'DEBITOR'
JOIN hs_office.relation_rv pRel
FROM hs_office.partner partner
JOIN hs_office.relation dRel
ON dRel.uuid = debitorRelUuid AND dRel.type = 'DEBITOR'
JOIN hs_office.relation pRel
ON pRel.uuid = partner.partnerRelUuid AND pRel.type = 'PARTNER'
WHERE pRel.holderUuid = dRel.anchorUuid
)
""")
@NotFound(action = NotFoundAction.IGNORE) // TODO.impl: map a simplified raw-PartnerEntity, just for the partner-number
private HsOfficePartnerEntity partner;
@NotFound(action = NotFoundAction.EXCEPTION) // TODO.impl: map a simplified raw-PartnerEntity, just for the partner-number
private HsOfficePartnerRealEntity partner;
@Column(name = "debitornumbersuffix", length = 2)
@Pattern(regexp = TWO_DECIMAL_DIGITS)
@ -132,9 +131,7 @@ public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>,
@Override
public HsOfficeDebitorEntity load() {
BaseEntity.super.load();
if (partner != null) {
partner.load();
}
partner.load();
debitorRel.load();
if (refundBankAccount != null) {
refundBankAccount.load();
@ -145,7 +142,7 @@ public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>,
public String getTaggedDebitorNumber() {
return ofNullable(partner)
.filter(partner -> debitorNumberSuffix != null)
.map(HsOfficePartnerEntity::getPartnerNumber)
.map(HsOfficePartner::getPartnerNumber)
.map(partnerNumber -> DEBITOR_NUMBER_TAG + partnerNumber + debitorNumberSuffix)
.orElse(null);
}
@ -160,7 +157,7 @@ public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>,
return getTaggedDebitorNumber();
}
public static RbacView rbac() {
public static RbacSpec rbac() {
return rbacViewFor("debitor", HsOfficeDebitorEntity.class)
.withIdentityView(SQL.query("""
SELECT debitor.uuid AS uuid,

View File

@ -19,7 +19,7 @@ public interface HsOfficeDebitorRepository extends Repository<HsOfficeDebitorEnt
@Query("""
SELECT debitor FROM HsOfficeDebitorEntity debitor
JOIN HsOfficePartnerEntity partner
JOIN HsOfficePartnerRealEntity partner
ON partner.partnerRel.holder = debitor.debitorRel.anchor
AND partner.partnerRel.type = 'PARTNER' AND debitor.debitorRel.type = 'DEBITOR'
WHERE partner.partnerNumber = :partnerNumber
@ -42,7 +42,7 @@ public interface HsOfficeDebitorRepository extends Repository<HsOfficeDebitorEnt
@Query("""
SELECT debitor FROM HsOfficeDebitorEntity debitor
JOIN HsOfficePartnerEntity partner
JOIN HsOfficePartnerRealEntity partner
ON partner.partnerRel.holder = debitor.debitorRel.anchor
AND partner.partnerRel.type = 'PARTNER' AND debitor.debitorRel.type = 'DEBITOR'
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.HsOfficeMembershipPatchResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipResource;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRbacEntity;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRealRepository;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import jakarta.persistence.EntityNotFoundException;
import java.util.List;
import java.util.UUID;
import java.util.function.BiConsumer;
@ -28,7 +30,10 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
private Context context;
@Autowired
private StandardMapper mapper;
private StrictMapper mapper;
@Autowired
private HsOfficePartnerRealRepository partnerRepo;
@Autowired
private HsOfficeMembershipRepository membershipRepo;
@ -47,7 +52,7 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
final var entities = partnerNumber != null
? membershipRepo.findMembershipsByPartnerNumber(
cropTag(HsOfficePartnerEntity.PARTNER_NUMBER_TAG, partnerNumber))
cropTag(HsOfficePartnerRbacEntity.PARTNER_NUMBER_TAG, partnerNumber))
: partnerUuid != null
? membershipRepo.findMembershipsByPartnerUuid(partnerUuid)
: membershipRepo.findAll();
@ -68,7 +73,7 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
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);
@ -164,5 +169,12 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
if (entity.getValidity().hasUpperBound()) {
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.Setter;
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.persistence.BaseEntity;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
import net.hostsharing.hsadminng.rbac.generator.RbacView;
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.rbac.role.WithRoleId;
import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable;
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.toPostgresDateRange;
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.RbacView.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.DELETE;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.UPDATE;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.AGENT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.OWNER;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.TENANT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.fetchedBySql;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
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.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.DELETE;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.UPDATE;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.RbacSubjectReference.UserRole.CREATOR;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.AGENT;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.OWNER;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.TENANT;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.fetchedBySql;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@Entity
@ -63,7 +64,7 @@ import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@NoArgsConstructor
@AllArgsConstructor
@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 TWO_DECIMAL_DIGITS = "^([0-9]{2})$";
@ -84,7 +85,7 @@ public class HsOfficeMembershipEntity implements BaseEntity<HsOfficeMembershipEn
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "partneruuid")
private HsOfficePartnerEntity partner;
private HsOfficePartnerRealEntity partner;
@Column(name = "membernumbersuffix", length = 2)
@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)
.withIdentityView(SQL.query("""
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.mapper.EntityPatcher;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import java.util.Optional;
public class HsOfficeMembershipEntityPatcher implements EntityPatcher<HsOfficeMembershipPatchResource> {
private final StandardMapper mapper;
private final StrictMapper mapper;
private final HsOfficeMembershipEntity entity;
public HsOfficeMembershipEntityPatcher(
final StandardMapper mapper,
final StrictMapper mapper,
final HsOfficeMembershipEntity entity) {
this.mapper = mapper;
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.HsOfficeRelationRealRepository;
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
@ -26,6 +26,7 @@ import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.util.List;
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.repr.TaggedNumber.cropTag;
@ -38,10 +39,10 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
private Context context;
@Autowired
private StandardMapper mapper;
private StrictMapper mapper;
@Autowired
private HsOfficePartnerRepository partnerRepo;
private HsOfficePartnerRbacRepository partnerRepo;
@Autowired
private HsOfficeRelationRealRepository relationRepo;
@ -60,7 +61,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
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);
}
@ -83,7 +84,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
.path("/api/hs/office/partners/{id}")
.buildAndExpand(saved.getUuid())
.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);
}
@ -101,7 +102,8 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
if (result.isEmpty()) {
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
@ -118,7 +120,8 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
if (result.isEmpty()) {
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
@ -161,20 +164,20 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
final var saved = partnerRepo.save(current);
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);
}
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())) {
// TODO.impl: we also need to use the new partner-person as the anchor
relationRepo.save(previousPartnerRel.toBuilder().uuid(null).type(EX_PARTNER).build());
}
}
private HsOfficePartnerEntity createPartnerEntity(final HsOfficePartnerInsertResource body) {
final var entityToSave = new HsOfficePartnerEntity();
entityToSave.setPartnerNumber(cropTag(HsOfficePartnerEntity.PARTNER_NUMBER_TAG, body.getPartnerNumber()));
private HsOfficePartnerRbacEntity createPartnerEntity(final HsOfficePartnerInsertResource body) {
final var entityToSave = new HsOfficePartnerRbacEntity();
entityToSave.setPartnerNumber(cropTag(HsOfficePartnerRbacEntity.PARTNER_NUMBER_TAG, body.getPartnerNumber()));
entityToSave.setPartnerRel(persistPartnerRel(body.getPartnerRel()));
entityToSave.setDetails(mapper.map(body.getDetails(), HsOfficePartnerDetailsEntity.class));
return entityToSave;
@ -197,4 +200,8 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
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 net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.generator.RbacView;
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.Stringifyable;
@ -13,10 +13,10 @@ import java.io.IOException;
import java.time.LocalDate;
import java.util.UUID;
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.Role.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
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.Role.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@Entity
@ -67,7 +67,7 @@ public class HsOfficePartnerDetailsEntity implements BaseEntity<HsOfficePartnerD
}
public static RbacView rbac() {
public static RbacSpec rbac() {
return rbacViewFor("partnerDetails", HsOfficePartnerDetailsEntity.class)
.withIdentityView(SQL.query("""
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> {
private final EntityManager em;
private final HsOfficePartnerEntity entity;
private final HsOfficePartnerRbacEntity entity;
HsOfficePartnerEntityPatcher(
final EntityManager em,
final HsOfficePartnerEntity entity) {
final HsOfficePartnerRbacEntity entity) {
this.em = em;
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 net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.role.WithRoleId;
import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable;
import org.apache.commons.lang3.StringUtils;
@ -30,7 +31,7 @@ import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@SuperBuilder(toBuilder = true)
@FieldNameConstants
@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")
.withProp(Fields.personType, HsOfficePerson::getPersonType)

View File

@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.hs.office.person;
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.hs.office.generated.api.v1.api.HsOfficePersonsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonInsertResource;
@ -24,7 +24,7 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
private Context context;
@Autowired
private StandardMapper mapper;
private StrictMapper mapper;
@Autowired
private HsOfficePersonRbacRepository personRepo;

View File

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

View File

@ -4,6 +4,7 @@ public enum HsOfficePersonType {
UNKNOWN_PERSON_TYPE("??"),
NATURAL_PERSON("NP"), // a human being
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
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

View File

@ -6,6 +6,7 @@ import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.role.WithRoleId;
import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable;
@ -22,7 +23,7 @@ import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@Setter
@SuperBuilder(toBuilder = true)
@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")
.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.person.HsOfficePersonRealEntity;
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.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
@ -32,7 +32,7 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
private Context context;
@Autowired
private StandardMapper mapper;
private StrictMapper mapper;
@Autowired
private HsOfficeRelationRbacRepository rbacRelationRepo;
@ -185,6 +185,7 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
@SuppressWarnings("unchecked")
final BiConsumer<HsOfficeContactInsertResource, HsOfficeContactRealEntity> CONTACT_RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
entity.putPostalAddress(from(resource.getPostalAddress()));
entity.putEmailAddresses(from(resource.getEmailAddresses()));
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.hs.office.contact.HsOfficeContactRbacEntity;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacEntity;
import net.hostsharing.hsadminng.rbac.generator.RbacView;
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import java.io.IOException;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.CaseDef.inCaseOf;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.CaseDef.inOtherCases;
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.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.DELETE;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.UPDATE;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.AGENT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.OWNER;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.REFERRER;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.TENANT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.CaseDef.inCaseOf;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.CaseDef.inOtherCases;
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.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.DELETE;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.UPDATE;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.RbacSubjectReference.UserRole.CREATOR;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.AGENT;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.OWNER;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.REFERRER;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.TENANT;
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 = "relation_rv")
@ -42,7 +42,7 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
@DisplayAs("RbacRelation")
public class HsOfficeRelationRbacEntity extends HsOfficeRelation {
public static RbacView rbac() {
public static RbacSpec rbac() {
return rbacViewFor("relation", HsOfficeRelationRbacEntity.class)
.withIdentityView(SQL.projection("""
(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));
}
// 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 = """
SELECT rel FROM HsOfficeRelationRbacEntity AS rel
WHERE (:relationType IS NULL OR CAST(rel.type AS String) = :relationType)
AND ( :personUuid IS NULL
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
OR lower(rel.anchor.tradeName) LIKE :personData OR lower(rel.holder.tradeName) LIKE :personData
OR lower(rel.anchor.familyName) LIKE :personData OR lower(rel.holder.familyName) LIKE :personData
OR lower(rel.anchor.givenName) LIKE :personData OR lower(rel.holder.givenName) LIKE :personData )
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 lower(rel.contact.caption) LIKE :contactData
OR lower(CAST(rel.contact.postalAddress AS String)) LIKE :contactData
OR lower(CAST(rel.contact.emailAddresses AS String)) LIKE :contactData
OR lower(CAST(rel.contact.phoneNumbers AS String)) LIKE :contactData )
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.rbac")
List<HsOfficeRelationRbacEntity> findRelationRelatedToPersonUuidRelationByTypeMarkPersonAndContactDataImpl(

View File

@ -14,24 +14,58 @@ public interface HsOfficeRelationRealRepository extends Repository<HsOfficeRelat
@Timed("app.repo.relations.findByUuid.real")
Optional<HsOfficeRelationRealEntity> findByUuid(UUID id);
default List<HsOfficeRelationRealEntity> findRelationRelatedToPersonUuidAndRelationType(@NotNull UUID personUuid, HsOfficeRelationType relationType) {
return findRelationRelatedToPersonUuidAndRelationTypeString(personUuid, relationType == null ? null : relationType.toString());
}
@Query(value = """
SELECT p.* FROM hs_office.relation AS p
WHERE p.anchorUuid = :personUuid OR p.holderUuid = :personUuid
""", nativeQuery = true)
""", nativeQuery = true)
@Timed("app.repo.relations.findRelationRelatedToPersonUuid.real")
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 = """
SELECT p.* FROM hs_office.relation AS p
WHERE (:relationType IS NULL OR p.type = cast(:relationType AS hs_office.RelationType))
AND ( p.anchorUuid = :personUuid OR p.holderUuid = :personUuid)
""", nativeQuery = true)
@Timed("app.repo.relations.findRelationRelatedToPersonUuidAndRelationTypeString.real")
List<HsOfficeRelationRealEntity> findRelationRelatedToPersonUuidAndRelationTypeString(@NotNull UUID personUuid, String relationType);
SELECT rel FROM HsOfficeRelationRealEntity AS rel
WHERE (:relationType IS NULL OR CAST(rel.type AS String) = :relationType)
AND ( :personUuid IS NULL
OR rel.anchor.uuid = :personUuid OR rel.holder.uuid = :personUuid )
AND ( :mark IS NULL OR rel.mark ILIKE :mark )
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")
HsOfficeRelationRealEntity save(final HsOfficeRelationRealEntity entity);
@ -41,4 +75,11 @@ public interface HsOfficeRelationRealRepository extends Repository<HsOfficeRelat
@Timed("app.repo.relations.deleteByUuid.real")
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 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.model.HsOfficeDebitorResource;
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.HsOfficeSepaMandateResource;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
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.PersistenceContext;
import jakarta.validation.ValidationException;
import java.util.List;
import java.util.UUID;
import java.util.function.BiConsumer;
@ -29,7 +33,13 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
private Context context;
@Autowired
private StandardMapper mapper;
private StrictMapper mapper;
@Autowired
private HsOfficeDebitorRepository debitorRepo;
@Autowired
private HsOfficeBankAccountRepository bankAccountRepo;
@Autowired
private HsOfficeSepaMandateRepository sepaMandateRepo;
@ -137,10 +147,22 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
if (entity.getValidity().hasUpperBound()) {
resource.setValidTo(entity.getValidity().upper().minusDays(1));
}
resource.setDebitor(mapper.map(entity.getDebitor(), HsOfficeDebitorResource.class));
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) -> {
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.relation.HsOfficeRelationRbacEntity;
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.Stringifyable;
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.mapper.PostgresDateRange.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingCase;
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.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingCase;
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.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.RbacSubjectReference.UserRole.CREATOR;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@Entity
@ -100,7 +100,7 @@ public class HsOfficeSepaMandateEntity implements Stringifyable, BaseEntity<HsOf
return reference;
}
public static RbacView rbac() {
public static RbacSpec rbac() {
return rbacViewFor("sepaMandate", HsOfficeSepaMandateEntity.class)
.withIdentityView(query("""
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 jakarta.persistence.EntityManager;
import java.util.UUID;
public interface BaseEntity<T extends BaseEntity<?>> {
@ -15,4 +16,10 @@ public interface BaseEntity<T extends BaseEntity<?>> {
//noinspection unchecked
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;
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.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import jakarta.validation.constraints.NotNull;
@Controller
public class PingController {
@ResponseBody
@RequestMapping(value = "/api/ping", method = RequestMethod.GET)
public String ping() {
return "pong\n";
public String ping(
@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 net.hostsharing.hsadminng.rbac.generator.PostgresTriggerReference.NEW;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacGrantDefinition.GrantType.PERM_TO_ROLE;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.GUEST;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.RbacGrantDefinition.GrantType.PERM_TO_ROLE;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.GUEST;
import static net.hostsharing.hsadminng.rbac.generator.StringWriter.with;
import static org.apache.commons.lang3.StringUtils.capitalize;
import static org.apache.commons.lang3.StringUtils.uncapitalize;
public class InsertTriggerGenerator {
private final RbacView rbacDef;
private final RbacSpec rbacDef;
private final String liquibaseTagPrefix;
public InsertTriggerGenerator(final RbacView rbacDef, final String liqibaseTagPrefix) {
public InsertTriggerGenerator(final RbacSpec rbacDef, final String liqibaseTagPrefix) {
this.rbacDef = rbacDef;
this.liquibaseTagPrefix = liqibaseTagPrefix;
}
@ -203,8 +203,8 @@ public class InsertTriggerGenerator {
plPgSql.chopEmptyLines();
}
private void generateInsertPermissionChecksForSingleGrant(final StringWriter plPgSql, final RbacView.RbacGrantDefinition g) {
final RbacView.EntityAlias superRoleEntityAlias = g.getSuperRoleDef().getEntityAlias();
private void generateInsertPermissionChecksForSingleGrant(final StringWriter plPgSql, final RbacSpec.RbacGrantDefinition g) {
final RbacSpec.EntityAlias superRoleEntityAlias = g.getSuperRoleDef().getEntityAlias();
final var caseCondition = g.isConditional()
? ("NEW.type in (" + toStringList(g.getForCases()) + ") and ")
@ -275,15 +275,15 @@ public class InsertTriggerGenerator {
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(", "));
}
private boolean isGrantToADifferentTable(final RbacView.RbacGrantDefinition g) {
private boolean isGrantToADifferentTable(final RbacSpec.RbacGrantDefinition g) {
return !rbacDef.getRootEntityAlias().getRawTableNameWithSchema().equals(g.getSuperRoleDef().getEntityAlias().getRawTableNameWithSchema());
}
private Stream<RbacView.RbacGrantDefinition> getInsertGrants() {
private Stream<RbacSpec.RbacGrantDefinition> getInsertGrants() {
return rbacDef.getGrantDefs().stream()
.filter(g -> g.grantType() == PERM_TO_ROLE)
.filter(g -> g.getPermDef().toCreate && g.getPermDef().getPermission() == INSERT);
@ -298,14 +298,14 @@ public class InsertTriggerGenerator {
g.getSuperRoleDef().getEntityAlias().isGlobal() && g.getSuperRoleDef().getRole() == GUEST);
}
private Optional<RbacView.RbacGrantDefinition> getOptionalInsertGrant() {
private Optional<RbacSpec.RbacGrantDefinition> getOptionalInsertGrant() {
return getInsertGrants()
.reduce(singleton());
}
private Optional<RbacView.RbacRoleDefinition> getOptionalInsertSuperRole() {
private Optional<RbacSpec.RbacRoleDefinition> getOptionalInsertSuperRole() {
return getInsertGrants()
.map(RbacView.RbacGrantDefinition::getSuperRoleDef)
.map(RbacSpec.RbacGrantDefinition::getSuperRoleDef)
.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());
}
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();
if (roleDef.getEntityAlias().isGlobal()) {
return functionName + "()";

View File

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

View File

@ -7,7 +7,7 @@ public class RbacObjectGenerator {
private final String liquibaseTagPrefix;
private final String rawTableName;
public RbacObjectGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) {
public RbacObjectGenerator(final RbacSpec rbacDef, final String liquibaseTagPrefix) {
this.liquibaseTagPrefix = liquibaseTagPrefix;
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;
public class RbacRestrictedViewGenerator {
private final RbacView rbacDef;
private final RbacSpec rbacDef;
private final String liquibaseTagPrefix;
private final String rawTableName;
public RbacRestrictedViewGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) {
public RbacRestrictedViewGenerator(final RbacSpec rbacDef, final String liquibaseTagPrefix) {
this.rbacDef = rbacDef;
this.liquibaseTagPrefix = liquibaseTagPrefix;
this.rawTableName = rbacDef.getRootEntityAlias().getRawTableNameWithSchema();

View File

@ -8,7 +8,7 @@ public class RbacRoleDescriptorsGenerator {
private final String simpleEntityVarName;
private final String rawTableName;
public RbacRoleDescriptorsGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) {
public RbacRoleDescriptorsGenerator(final RbacSpec rbacDef, final String liquibaseTagPrefix) {
this.liquibaseTagPrefix = liquibaseTagPrefix;
this.simpleEntityVarName = rbacDef.getRootEntityAlias().simpleName();
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.Collections.max;
import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.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.RbacView.RbacSubjectReference.UserRole.CREATOR;
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.Part.AUTO_FETCH;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.RbacGrantDefinition.GrantType.PERM_TO_ROLE;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.RbacGrantDefinition.GrantType.ROLE_TO_ROLE;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.RbacSubjectReference.UserRole.CREATOR;
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.lang3.StringUtils.capitalize;
import static org.apache.commons.lang3.StringUtils.uncapitalize;
@Getter
// TODO.refa: rename to RbacDSL
public class RbacView {
public class RbacSpec {
public static final String GLOBAL = "rbac.global";
public static final String OUTPUT_BASEDIR = "src/main/resources/db/changelog";
@ -90,11 +89,11 @@ public class RbacView {
* @param <E>
* a JPA entity class extending RbacObject
*/
public static <E extends BaseEntity<?>> RbacView rbacViewFor(final String alias, final Class<E> entityClass) {
return new RbacView(alias, entityClass);
public static <E extends BaseEntity<?>> RbacSpec rbacViewFor(final String alias, final Class<E> 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);
entityAliases.put(alias, rootEntityAlias);
new RbacSubjectReference(CREATOR);
@ -110,7 +109,7 @@ public class RbacView {
* @return
* the `this` instance itself to allow chained calls.
*/
public RbacView withUpdatableColumns(final String... columnNames) {
public RbacSpec withUpdatableColumns(final String... columnNames) {
Collections.addAll(updatableColumns, columnNames);
verifyVersionColumnExists();
return this;
@ -134,7 +133,7 @@ public class RbacView {
* @return
* the `this` instance itself to allow chained calls.
*/
public RbacView withIdentityView(final SQL sqlExpression) {
public RbacSpec withIdentityView(final SQL sqlExpression) {
this.identityViewSqlQuery = sqlExpression;
return this;
}
@ -150,7 +149,7 @@ public class RbacView {
* @return
* the `this` instance itself to allow chained calls.
*/
public RbacView withRestrictedViewOrderBy(final SQL orderBySqlExpression) {
public RbacSpec withRestrictedViewOrderBy(final SQL orderBySqlExpression) {
this.orderBySqlExpression = orderBySqlExpression;
return this;
}
@ -166,7 +165,7 @@ public class RbacView {
* @return
* 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();
with.accept(newRoleDef);
previousRoleDef = newRoleDef;
@ -182,7 +181,7 @@ public class RbacView {
* @return
* 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();
findOrCreateGrantDef(newRoleDef, previousRoleDef).toCreate();
previousRoleDef = newRoleDef;
@ -202,7 +201,7 @@ public class RbacView {
* @return
* 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();
findOrCreateGrantDef(newRoleDef, previousRoleDef).toCreate();
with.accept(newRoleDef);
@ -254,7 +253,7 @@ public class RbacView {
.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) {
entityAliases.put(alias, new EntityAlias(alias));
}
@ -287,7 +286,7 @@ public class RbacView {
* @param <EC>
* a JPA entity class extending RbacObject
*/
public <EC extends BaseEntity<?>> RbacView importRootEntityAliasProxy(
public <EC extends BaseEntity<?>> RbacSpec importRootEntityAliasProxy(
final String aliasName,
final Class<? extends BaseEntity<?>> entityClass,
final ColumnValue forCase,
@ -312,7 +311,7 @@ public class RbacView {
* @param <EC>
* a JPA entity class extending RbacObject
*/
public RbacView importSubEntityAlias(
public RbacSpec importSubEntityAlias(
final String aliasName, final Class<? extends BaseEntity<?>> entityClass,
final SQL fetchSql, final Column dependsOnColum) {
importEntityAliasImpl(aliasName, entityClass, usingDefaultCase(), fetchSql, dependsOnColum, true, NOT_NULL);
@ -349,7 +348,7 @@ public class RbacView {
* @param <EC>
* a JPA entity class extending RbacObject
*/
public RbacView importEntityAlias(
public RbacSpec importEntityAlias(
final String aliasName, final Class<? extends BaseEntity<?>> entityClass, final ColumnValue usingCase,
final Column dependsOnColum, final SQL fetchSql, final Nullable nullable) {
importEntityAliasImpl(aliasName, entityClass, usingCase, fetchSql, dependsOnColum, false, nullable);
@ -379,12 +378,12 @@ public class RbacView {
return entityAlias;
}
private static RbacView rbacDefinition(final Class<? extends BaseEntity> entityClass)
private static RbacSpec rbacDefinition(final Class<? extends BaseEntity> entityClass)
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,
asSubEntity ? entityAliases.keySet() : null);
copyOf(importedRbacView.getEntityAliases().values()).stream()
@ -416,7 +415,7 @@ public class RbacView {
return this;
}
public RbacView switchOnColumn(final String discriminatorColumName, final CaseDef... caseDefs) {
public RbacSpec switchOnColumn(final String discriminatorColumName, final CaseDef... caseDefs) {
this.discriminatorColumName = discriminatorColumName;
allCases.addAll(stream(caseDefs).toList());
@ -511,7 +510,7 @@ public class RbacView {
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);
return this;
}
@ -542,15 +541,15 @@ public class RbacView {
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();
return RbacView.this;
return RbacSpec.this;
}
public RbacView grantPermission(final Permission perm) {
public RbacSpec grantPermission(final Permission perm) {
final var forTable = rootEntityAlias.getRawTableNameWithSchema();
findOrCreateGrantDef(findRbacPerm(rootEntityAlias, perm, forTable), superRoleDef).toCreate();
return RbacView.this;
return RbacSpec.this;
}
}
@ -698,10 +697,10 @@ public class RbacView {
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.superRole = role;
return RbacView.this;
return RbacSpec.this;
}
}
@ -733,9 +732,9 @@ public class RbacView {
* @return
* 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();
return RbacView.this;
return RbacSpec.this;
}
@Override
@ -1186,12 +1185,12 @@ public class RbacView {
private static class AliasNameMapper {
private final RbacView importedRbacView;
private final RbacSpec importedRbacView;
private final String outerAliasName;
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.outerAliasName = outerAliasName;
this.outerAliasNames = (outerAliasNames == null) ? Collections.emptySet() : outerAliasNames;
@ -1210,19 +1209,19 @@ public class RbacView {
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);
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);
}
public static CaseDef inOtherCases(final Consumer<RbacView> def) {
public static CaseDef inOtherCases(final Consumer<RbacSpec> def) {
return new CaseDef(null, def);
}
@ -1281,7 +1280,7 @@ public class RbacView {
.filter(c -> stream(c.getDeclaredMethods())
.anyMatch(m -> m.getName().equals("rbac") && isStatic(m.getModifiers()))
)
.map(RbacView::castToSubclassOfBaseEntity)
.map(RbacSpec::castToSubclassOfBaseEntity)
.collect(Collectors.toSet());
return rbacEntityClasses;
}
@ -1296,6 +1295,6 @@ public class RbacView {
*/
public static void main(String[] args) throws Exception {
findRbacEntityClasses("net.hostsharing.hsadminng")
.forEach(RbacView::generateRbacView);
.forEach(RbacSpec::generateRbacView);
}
}

View File

@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.rbac.generator;
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 java.nio.file.*;
@ -12,7 +12,7 @@ import java.util.stream.Stream;
import static java.util.Comparator.comparing;
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 {
@ -20,14 +20,14 @@ public class RbacViewMermaidFlowchartGenerator {
public static final String HOSTSHARING_LIGHT_ORANGE = "#feb28c";
public static final String HOSTSHARING_DARK_BLUE = "#274d6e";
public static final String HOSTSHARING_LIGHT_BLUE = "#99bcdb";
private final RbacView rbacDef;
private final RbacSpec rbacDef;
private final List<RbacView.EntityAlias> usedEntityAliases;
private final List<RbacSpec.EntityAlias> usedEntityAliases;
private final CaseDef forCase;
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.forCase = forCase;
@ -37,7 +37,7 @@ public class RbacViewMermaidFlowchartGenerator {
g.getSubRoleDef() != null ? g.getSubRoleDef().getEntityAlias() : null,
g.getPermDef() != null ? g.getPermDef().getEntityAlias() : null))
.filter(Objects::nonNull)
.sorted(comparing(RbacView.EntityAlias::aliasName))
.sorted(comparing(RbacSpec.EntityAlias::aliasName))
.distinct()
.filter(rbacDef::renderInDiagram)
.collect(Collectors.toList());
@ -50,7 +50,7 @@ public class RbacViewMermaidFlowchartGenerator {
renderGrants();
}
public RbacViewMermaidFlowchartGenerator(final RbacView rbacDef) {
public RbacViewMermaidFlowchartGenerator(final RbacSpec rbacDef) {
this(rbacDef, null);
}
private void renderEntitySubgraphs() {
@ -61,7 +61,7 @@ public class RbacViewMermaidFlowchartGenerator {
.forEach(this::renderEntitySubgraph);
}
private void renderEntitySubgraph(final RbacView.EntityAlias entity) {
private void renderEntitySubgraph(final RbacSpec.EntityAlias entity) {
if (!rbacDef.renderInDiagram(entity)) {
return;
}
@ -128,7 +128,7 @@ public class RbacViewMermaidFlowchartGenerator {
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()
.filter(g -> g.grantType() == grantType)
.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 )
return true;
if ( forCase == null && !g.isConditional() )
@ -150,7 +150,7 @@ public class RbacViewMermaidFlowchartGenerator {
return isToBeRenderedInThisGraph;
}
private String grantDef(final RbacView.RbacGrantDefinition grant) {
private String grantDef(final RbacSpec.RbacGrantDefinition grant) {
final var arrow = (grant.isToCreate() ? " ==>" : " -.->")
+ (grant.isAssumed() ? " " : "|XX| ");
final var grantDef = switch (grant.grantType()) {
@ -164,19 +164,19 @@ public class RbacViewMermaidFlowchartGenerator {
return grantDef;
}
private String permDef(final RbacView.RbacPermissionDefinition perm) {
private String permDef(final RbacSpec.RbacPermissionDefinition perm) {
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();
}
private String roleDef(final RbacView.RbacRoleDefinition roleDef) {
private String roleDef(final RbacSpec.RbacRoleDefinition roleDef) {
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();
}

View File

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

View File

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

View File

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

View File

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

View File

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

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