1
0

Compare commits

...

10 Commits

Author SHA1 Message Date
Michael Hoennig
4994bac101 use @Slf4j (+logback) for logging instead of System.out/err.println (#165)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/165
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
2025-03-19 16:21:55 +01:00
Michael Hoennig
eb9edf1cb1 remove current-subject from OpenAPI header specs, use Authorization instead (#164)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/164
Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
2025-03-18 11:52:29 +01:00
Michael Hoennig
5ca0638319 unauthenticated swagger-ui on- server-port and proper security filter integration into Spring Security (#163)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/163
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
2025-03-17 12:59:50 +01:00
Michael Hoennig
a2b81f009b update dependend relations when updating partner person (#162)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/162
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
2025-03-10 12:04:54 +01:00
Michael Hoennig
e3b11972e5 preparation for changing updatable columns (#161)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/161
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
2025-03-03 12:01:33 +01:00
Michael Hoennig
f8fda06beb scenario test for multiple debitors+memberships + validation for subsequent memberships (#160)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/160
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
2025-02-25 09:46:35 +01:00
Michael Hoennig
a0635960a5 migrate hosting-assets into external db (#158)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/158
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
2025-02-17 09:40:58 +01:00
Michael Hoennig
ddd96654ef use only persistViaSql (#156)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/156
2025-02-07 09:31:36 +01:00
Michael Hoennig
abafd64813 feature/remove-office-data-import (#155)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/155
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
2025-02-05 09:29:27 +01:00
Michael Hoennig
a1a753e00a replace office-data-import by db-restore (#154)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/154
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
2025-02-04 09:55:48 +01:00
190 changed files with 23032 additions and 3066 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'
@ -92,8 +91,8 @@ 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-check='. .aliases; . .tc-environment; gw test check -x pitest'
# HOWTO: run all 'normal' tests (no scenario+import-tests): `gw-test`
# You can also mention specific targets: `gw-test importOfficeData`.
# 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`
@ -110,20 +109,22 @@ function _gwTest1() {
echo "RUNNING gw $@"
printf -- '-%0.s' {1..80}; echo
./gradlew "$@"
local buildResultCode=$?
printf -- '-%0.s' {1..80}; echo
echo "DONE gw $@"
return $buildResultCode
}
function _gwTest() {
. .aliases;
. .tc-environment;
rm /tmp/gwTest.tmp
. .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 importOfficeData importHostingAssets "$@");
_gwTest1 importHostingAssets "$@");
elif [ $# -eq 0 ] || [[ $1 == -* ]]; then
time _gwTest1 test "$@";
else
@ -137,7 +138,7 @@ 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'
@ -147,6 +148,6 @@ if [ ! -f .environment ]; then
fi
source .environment
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-upload='./gradlew scenarioTest convertMarkdownToHtml && ssh hsh03-hsngdev@hsh03.hostsharing.net "rm -f doms/hsngdev.hs-example.de/htdocs-ssl/scenarios/office/*.html" && scp build/doc/scenarios/*.html hsh03-hsngdev@hsh03.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,7 +1,8 @@
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 HSADMINNG_POSTGRES_ADMIN_USERNAME=admin
export HSADMINNG_SUPERUSER=import-superuser@hostsharing.net
export HSADMINNG_CAS_SERVER=
export LANG=en_US.UTF-8

View File

@ -4,4 +4,6 @@ unset HSADMINNG_POSTGRES_ADMIN_PASSWORD
unset HSADMINNG_POSTGRES_RESTRICTED_USERNAME
unset HSADMINNG_SUPERUSER
unset HSADMINNG_MIGRATION_DATA_PATH
unset HSADMINNG_OFFICE_DATA_SQL_FILE
unset HSADMINNG_CAS_SERVER=

4
Jenkinsfile vendored
View File

@ -55,9 +55,9 @@ pipeline {
sh './gradlew bookingIntegrationTest hostingIntegrationTest --no-daemon'
}
}
stage('Import-Tests') {
stage('Test-Imports') {
steps {
sh './gradlew importOfficeData importHostingAssets --no-daemon'
sh './gradlew importHostingAssets --no-daemon'
}
}
stage ('Scenario-Tests') {

View File

@ -91,17 +91,15 @@ Next, compile and run the application on `localhost:8080` and the management ser
export HSADMINNG_CAS_SERVER=
# this runs the application with test-data and all modules:
gw bootRun --args='--spring.profiles.active=dev,complete,test-data'
gw bootRun --args='--spring.profiles.active=dev,fakeCasAuthenticator,complete,test-data'
The meaning of these profiles is:
- **dev**: the PostgreSQL users are created via Liquibase
- **fakeCasAuthenticator**: The username is simply taken from whatever is after "Bearer " in the "Authorization" header.
- **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":
@ -109,19 +107,19 @@ Now we can access the REST API, e.g. using curl:
# the following command should return a JSON array with just all customers:
curl -f -s\
-H 'current-subject: superuser-alex@hostsharing.net' \
-H 'Authorization: Bearer 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 -s\
-H 'current-subject: superuser-alex@hostsharing.net' -H 'assumed-roles: rbactest.customer#yyy:ADMIN' \
-H 'Authorization: Bearer superuser-alex@hostsharing.net' -H 'assumed-roles: rbactest.customer#yyy:ADMIN' \
http://localhost:8080/api/test/packages \
| jq
# add a new customer
curl -f -s\
-H 'current-subject: superuser-alex@hostsharing.net' -H "Content-Type: application/json" \
-H 'Authorization: Bearer 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 \
| jq
@ -132,9 +130,18 @@ 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:8081/actuator/swagger-ui/index.html (uses management-port and thus bypasses authentication).
And to see the full, currently implemented, API, open http://localhost:8080/swagger-ui/index.html).
For a locally running app without CAS-authentication (export HSADMINNG_CAS_SERVER=''),
authorize using the name of the subject (e.g. "superuser-alex@hostsharing.net" in case of test-data).
Otherwise, use a valid CAS-ticket.
If you still need to install some of these tools, find some hints in the next chapters.
If you want to run the application with real CAS-Authentication:
# set the CAS-SERVER-Root, also see `bin/cas-curl`.
export HSADMINNG_CAS_SERVER=https://login.hostsharing.net # or whatever your CAS-Server-URL you want to use
# run the application against the real CAS authenticator
gw bootRun --args='--spring.profiles.active=dev,realCasAuthenticator,complete,test-data'
### PostgreSQL Server
@ -656,7 +663,7 @@ howto
Add `--args='--spring.profiles.active=...` with the wanted profile selector:
```sh
gw bootRun --args='--spring.profiles.active=external-db,only -office,without-test-data'
gw bootRun --args='--spring.profiles.active=external-db,only-office,without-test-data'
```
These profiles mean:
@ -666,6 +673,29 @@ These profiles mean:
- **without-test-data**: no test-data is inserted
### How to Run the Application in a Debugger
Add `' --debug-jvm` to the command line:
```sh
gw bootRun ... --debug-jvm
```
At the very beginning, the application is going to wait for a debugger with a message like this:
> Listening for transport dt_socket at address: 5005
As soon as a debugger connects to that port, the application will continue to run.
In IntelliJ IDEA you need a 'Remote JVM Debug' run configuration like this:
![IntelliJ IDEA JVM-Debug Run Config](./doc/.images/intellij-idea-jvm-debug-run-config.png)
Now, to attach IntelliJ IDEA as a debugger, you just need to run that config in debug mode.
If it's selected, just hit the *bug*-symbol next to it.
### 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:

View File

@ -131,6 +131,15 @@ function casTicket() {
echo $HSADMINNG_CAS_TICKET
}
function casTgt() {
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
echo "CAS-TGT: $HSADMINNG_CAS_TGT"
}
function casValidate() {
HSADMINNG_CAS_TICKET=`casTicket`
@ -191,6 +200,9 @@ case "${1,,}" in
"unassume") ## do not assume any particular role anymore, use the plain user as RBAC subject
rm ~/.cas-curl-assume
;;
"tgt") ## prints the current ticket granting ticket
casTgt
;;
"validate") ## validates current ticket granting ticket and prints currently logged in user
casValidate
;;

View File

@ -31,7 +31,7 @@ def search_keywords_in_files(keywords):
sys.exit(1)
# Allowed comment symbols
comment_symbols = {"//", "#", ";"}
comment_symbols = {"//", "#", "##", "###", "####", "#####", ";"}
for root, dirs, files in os.walk("."):
# Ausschließen bestimmter Verzeichnisse

View File

@ -1,6 +1,6 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.1'
id 'org.springframework.boot' version '3.4.2'
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
@ -66,8 +66,8 @@ 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 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.5'
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'
@ -77,7 +77,6 @@ dependencies {
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.8.3'
implementation 'org.reflections:reflections:0.10.2'
compileOnly 'org.projectlombok:lombok'
@ -263,7 +262,7 @@ test {
'net.hostsharing.hsadminng.**.generated.**',
]
useJUnitPlatform {
excludeTags 'importOfficeData', 'importHostingAssets', 'scenarioTest'
excludeTags 'importHostingAssets', 'scenarioTest'
}
}
@ -338,7 +337,7 @@ jacocoTestCoverageVerification {
// HOWTO: run all unit-tests which don't need a database: gw-test unitTest
tasks.register('unitTest', Test) {
useJUnitPlatform {
excludeTags 'importOfficeData', 'importHostingAssets', 'scenarioTest', 'generalIntegrationTest',
excludeTags 'importHostingAssets', 'scenarioTest', 'generalIntegrationTest',
'officeIntegrationTest', 'bookingIntegrationTest', 'hostingIntegrationTest'
}
@ -396,17 +395,6 @@ tasks.register('hostingIntegrationTest', Test) {
mustRunAfter spotlessJava
}
tasks.register('importOfficeData', Test) {
useJUnitPlatform {
includeTags 'importOfficeData'
}
group 'verification'
description 'run the import jobs as tests'
mustRunAfter spotlessJava
}
tasks.register('importHostingAssets', Test) {
useJUnitPlatform {
includeTags 'importHostingAssets'
@ -439,7 +427,7 @@ pitest {
]
targetTests = ['net.hostsharing.hsadminng.**.*UnitTest', 'net.hostsharing.hsadminng.**.*RestTest']
excludedTestClasses = ['**AcceptanceTest*', '**IntegrationTest*', '**ImportOfficeData', '**ImportHostingAssets']
excludedTestClasses = ['**AcceptanceTest*', '**IntegrationTest*', '**ImportHostingAssets']
pitestVersion = '1.17.0'
junit5PluginVersion = '1.1.0'

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -0,0 +1,124 @@
# Änderung eines Geschäftspartners oder Rechnungsempfängers (Debitor)
**Status:**
- [x] vorgeschlagen von (Michael Hönnig)
- [ ] akzeptiert von (...)
- [ ] abgelehnt von (...)
- [ ] ersetzt durch (ersetzende ADR)
## Kontext und Problemstellung
Im vorgegebenen Datenmodell von Geschäftspartnern und Rechnungsempfängern (Debitoren), das auch fachliche Rollen wie Repräsentant, technische Ansprechpartner oder Mailinglisten-Subscriptions umfasst, stellt sich die Frage, wie eine Änderung der Geschäftspartner-Person effizient und konsistent umgesetzt werden kann.
Diese fachlichen Rollen hängen jeweils an der Partner-Person.
Ein konkretes Beispiel hierfür ist die Änderung von einer natürlichen Person, die verstorben ist, zu deren Erbengemeinschaft.
**Hierbei zeigte sich, dass die API-Bedienung durch die Vielzahl neu zu erstellender Objekte und deren Verknüpfungen komplex und fehleranfällig ist. Zudem lassen sich nicht alle Änderung in einer einzigen Transaktion durchführen, was zu Inkonsistenzen führen kann.“**
Angepasst werden müssen:
1. alle Relations mit der alten Partner-Person:
- die PARTNER-Relation
- die DEBITOR-Relations (ggf. mehrere)
- die OPERATIONS-Relations (ggf. mehrere)
- die SUBSCRIBER-Relations (ggf. mehrere)
- die REPRESENTATIVE-Relations (ggf. mehrere)
- etc.
2. Die PARTNER-Relation hat die Besonderheit, dass sie vom Partner referenziert wird und daher auch dort ausgetauscht werden muss.
3. Die DEBITOR-Relation hat die Besonderheit, dass sie vom Debitor referenziert wird und daher auch dort ausgetauscht werden muss.
Daher sollen möglichst viele dieser *Neuverdrahtungen* im Backend gemacht werden.
Und dafür braucht es dann eine zentrale Stelle, an der die Kaskade ausgelöst wird.
Derzeit gibt es drei mögliche Varianten, diese Änderung dynamisch umzusetzen, die jeweils unterschiedliche Auswirkungen auf Aufwände, API und Zugriffsrechte haben.
### Technischer Hintergrund
Zum Zeitpunkt der Erstellung dieses ADR existieren folgende relevante Entitäten:
- **Person**: Natürliche oder juristische Person (Name, Firma, Anrede etc.)
- **Contact**: Kontaktdaten einer fachlichen Rolle
- **Relation**: Mit einem Typ (z.B. PARTNER, DEBITOR, REPRESENTATIVE) und Kontaktdaten versehene Beziehung von einer Person (Holder) zu einer anderen (Anchor)
- **Partner**: Sind quasi Zusatzdaten einer PARTNER-Relation (derzeit nur die Partnernummer), welche eine Partner-Person mit der Hostsharing-Person verknüpft
- **Debitor**: Sind quasi Zusatzdaten einer DEBITOR-Relation, welche eine Debitor-Person mit einer Partner-Person verknüpft
Zugriffsrechte werden über ein hierarchisches, dynamisches RBAC-System gesteuert, bei dem der **OWNER** einer Entitäten-Instanz alle Rechte hat, **ADMIN** definierte Spalten aktualisieren darf, **AGENT** Verknüpfungen anlegen kann, und **TENANT**, **GUEST** sowie **REFERRER** nur Lesezugriff haben.
Partner und Debitor nutzen dabei die RBAC-Rollen der zugehörigen Relations.
## In Betracht gezogene Varianten
* **1. Relations ersetzen:** Austausch der PARTNER-/DEBITOR-/OPERATIONS-/...-Relations gegen eine neue Relation für die neue Partner-Person (z.B. Erbengemeinschaft) als neuen Holder als PATCH auf /api/hs/office/partners/UUID
* **2. Relations direkt aktualisieren:** Änderung der Holder-Referenz in der bestehenden PARTNER-Relation auf die neue Partner-Person (z.B. Erbengemeinschaft) als PATCH auf /api/hs/office/relations/UUID
* **3. Relations via Partner aktualisieren:** Änderung der Partner-Person in die PARTNER-Relation als PATCH auf /api/hs/office/partners/UUID
### Variante 1: Relations ersetzen
Der Austausch der Partner- (und Debitor-) Person erfolgt über das Erstellen einer neuen PARTNER- bzw. DEBITOR-Relation, im Partner bzw. Debitor wird dann die Referenz auf die alte PARTNER- bzw. DEBITOR-Relation gegen die neue ausgetauscht.
#### Vorteile
- **Beibehaltung der API:** Dieses Verhalten ist bereits implementiert und benötigt keinen großen Umbau an der API, sondern nur eine Erweiterung um das Austauschen weiterer Relations.
- **UPDATE-Permission für AGENT:** Es wäre möglich, der AGENT-Rolle einer Relation UPDATE-Rechte an der Relation zu geben, weil nur die unkritische Contact-Referenz änderbar wäre.
- **Kongruenz von Fachlichkeit+API**: Fachlich handelt es sich um den Austausch der Partner-Person, dazu passend wäre der Endpunkt, allerdings wird in dieser Variante nicht direkt die Partner-Person ausgetauscht, sondern eine neue PARTNER-Relation mit der neuen Partner-Person eingesetzt.
#### Nachteile
- **Verlust expliziter GRANTs:** Gibt es explizite GRANTs an der PARTNER-Relation, gehen diese verloren, da die Relation ausgetauscht wird. Die Übernahme dieser expliziten Grants erfordert also einen zusätzlichen Implementationsaufwand.
- **Divergenz zwischen Fachlichkeit und API:** Fachlich handelt es sich um den Austausch der Partner-Person, würde aber eine neue PARTNER-Relation dieser Person in den Partner eingesetzt werden. Das erfordert ein höheres Verständnis des Datenmodells.
- **Keine Anwendbarkeit auf abhängige Relations:** Beim Aktualisieren der abhängigen Relations (z.B. Representative, Operational- und Billing-Kontakt sowie der Mailinglisten-Subscriptions) stehen wir wieder vor dem Ausgangsproblem und müssten jeweils neue Relations erzeugen und die alten Relations löschen, was dann wieder zum Verlust expliziter GRANTs führt.
- **Performance bei vielen abhängigen Relations:** die abhängigen Relations können nur über Loops, nicht aber durch direkt SQL UPDATEs ausgetauscht werden, was zu einer schlechteren Performance führt
### Variante 2: Relations direkt aktualisieren
Die bestehende PARTNER-Relation bliebe erhalten, und der Holder wird von der verstorbenen Person auf die Erbengemeinschaft geändert.
#### Vorteile
- **Anwendbarkeit auf Partner- und Debitor-Person:** Der Code wäre an einer generischen Stelle, welche dann Partner- und Debitor-Person austauschbar machen würde
- **Einheitlichkeit/Generizität der API:** Die REST-API für Änderungen gehört dann einheitlich zum Relation-Endpunkt, was der bestehenden Handhabung von Contact-Änderungen entspricht.
#### Nachteile
- **UPDATE Permission für Relation-AGENT wäre kritisch:** Der Relation-AGENT darf nicht das Recht bekommen, den Holder auszutauschen. Da es keine Spalten-spezifischen Update-Rechte gibt, könnte dieser auch den Contact nicht mehr austauschen. Derzeit ist das allerdings auch noch nicht so implementiert.
- **Umbau der API:** Der Austausch einer Partner-Person würde vom Partner-Endpunkt (/api/hs/office/partner) zur Relation (/api/hs/office/partner) wandern, was ein größerer Umbau, auch bei den Tests wäre.
- **Divergenz von Fachlichkeit und API**: Fachlich handelt es sich um den Austausch der Partner-Person, aber man würde die Person nicht am Partner selbst austauschen, sondern an der PARTNER-Relation.
### Variante 3: Relations via Partner aktualisieren
Der Austausch der Partner- (bzw. Debitor-) Person würde weiterhin beim Partner bzw. Debitor erfolgen, jedoch würde die Personen-Referenz direkt in der bestehenden Partner- (bzw. Debitor-) Relation umgesetzt werden, statt eine neue Relation mit der neuen Partner- (bzw. Debitor) Person einzusetzen. Die direkt wie auch abhängige Relations könnten also einfach per SQL UPDATE aktualisiert werden.
#### Vorteile
- **Beibehaltung der API:** Der Endpunkt /api/hs/office/partners/UUID bliebe erhalten, wenn auch lokal ein Umbau auf Person-Update statt Relation-Update erfolgen müsste, Anpassungen in Verwendungen dieser API, z.B. in Tests, wären allerdings wenig aufwändig und das Risiko für weitere Aufwände recht gering.
- **UPDATE-Permission für AGENT:** Es wäre möglich, der AGENT-Rolle einer Relation UPDATE-Rechte an der Relation zu geben, aber eine Aktualisierung über die REST-Controller nur an kontrollierten Stellen zuzulassen.
- **Kongruenz von Fachlichkeit+API**: Fachlich handelt es sich um den Austausch der Partner-Person, was auch in dieser Variante technisch abgebildet würde, wenn auch eine Ebene tiefer im JSON, nämlich in der Partner-Relation.
#### Nachteile
Nennenswerte Nachteile wurden nicht identifiziert, allenfalls ist es etwas schräge, dass die RBAC-Rechte an den Relations ein UPDATE zulassen, was aber an der API nur für bestimmte Relations (ggf. kontrolliert) erreichbar wäre.
## Entscheidung und Ergebnis
**Entscheidung:** 3. Relations via Partner aktualisieren
**Begründung:**
- die Fachlichkeit wird an der API gut abgebildet (PATCH der Partner-Person auf /api/hs/office/partners/UUID)
- der Aufwand ist relativ gering (vieles ist mit SQL UPDATEs machbar)
- die UPDATE Permission dürfte an Relation-AGENT granted werden, ohne damit Schindluder getrieben werden kann (weil das an der API verhindert werden kann)
| Kriterium \ Relations ... | 1. ersetzen | 2. direkt aktualisieren | 3. via Partner aktualisieren |
|-----------------------------------------------|------------:|------------------------:|-----------------------------:|
| **Technische und Aufwands-Kriterien** | | | |
| Beibehaltung der API vs. Umbau (inkl. Risiko) | +2 | -2 | +1 |
| Anwendbarkeit auf Partner- und Debitor-Person | | +1 | |
| Anwendbarkeit auf abhängige Relations | -3 | | |
| Performance bei vielen abhängigen Relations | -1 | | |
| Aufwand für explizite Grants | -1 | | |
| **Zwischenergebnis** | **-3** | **-1** | **+1** |
| | | | |
| **Fachliche Kriterien** | | | |
| Kongruenz von Fachlichkeit+API | +1 | -1 | +1 |
| Einheitlichkeit/Generizität der API | | +1 | |
| UPDATE Permission für Relation-AGENT möglich | +1 | | +1 |
| **Zwischenergebnis** | **+2** | **0** | **+2** |
| | | | |
| **Endergebnis** | **-1** | **-1** | **+3** |

View File

@ -0,0 +1,124 @@
# Changing a Business Partner or Invoice Recipient (Debitor)
**Status:**
- [x] Proposed by (Michael Hönnig)
- [ ] Accepted by (...)
- [ ] Rejected by (...)
- [ ] Replaced by (replacing ADR)
## Context and Problem Statement
In the given data model of business partners and invoice recipients (debitors), which also includes business roles such as representative, technical contacts, or mailing list subscriptions, the question arises of how to efficiently and consistently implement a change of the business partner person. These business roles are each linked to the partner person.
A concrete example is changing from a natural person who has passed away to their heir community.
**It has been shown that handling the API is complex and error-prone due to the large number of newly created objects and their links. Additionally, not all changes can be carried out in a single transaction, which can lead to inconsistencies.**
The following elements must be updated:
1. All relations with the old partner person:
- The PARTNER relation
- The DEBITOR relations (possibly multiple)
- The OPERATIONS relations (possibly multiple)
- The SUBSCRIBER relations (possibly multiple)
- The REPRESENTATIVE relations (possibly multiple)
- etc.
2. The PARTNER relation has the peculiarity that it is referenced by the partner and therefore must also be replaced there.
3. The DEBITOR relation has the peculiarity that it is referenced by the debitor and therefore must also be replaced there.
As a result, as many of these *rewirings* as possible should be done in the backend.
A central point is needed to trigger this cascade.
Currently, there are three possible approaches to implementing this change dynamically, each with different impacts on effort, API, and access rights.
### Technical Background
At the time of this ADR's creation, the following relevant entities exist:
- **Person**: A natural or legal entity (name, company, salutation, etc.)
- **Contact**: Contact data of a business role
- **Relation**: A relationship from one person (Holder) to another (Anchor), with a type (e.g., PARTNER, DEBITOR, REPRESENTATIVE) and contact data
- **Partner**: Essentially additional data of a PARTNER relation (currently only the partner number), linking a partner person to the Hostsharing person
- **Debitor**: Essentially additional data of a DEBITOR relation, linking a debitor person to a partner person
Access rights are managed through a hierarchical, dynamic RBAC system, where the **OWNER** of an entity instance has all rights, **ADMIN** can update defined fields, **AGENT** can create links, and **TENANT**, **GUEST**, and **REFERRER** have read-only access.
Partners and debitors use the RBAC roles of the associated relations.
## Considered Alternatives
* **1. Replace Relations:** Replace PARTNER/DEBITOR/OPERATIONS/... relations with a new relation for the new partner person (e.g., heir community) as the new Holder via PATCH on /api/hs/office/partners/UUID
* **2. Directly Update Relations:** Change the Holder reference in the existing PARTNER relation to the new partner person (e.g., heir community) via PATCH on /api/hs/office/relations/UUID
* **3. Update Relations via Partner:** Change the partner person in the PARTNER relation via PATCH on /api/hs/office/partners/UUID
### Option 1: Replace Relations
The exchange of the partner (and debitor) person is done by creating a new PARTNER or DEBITOR relation, and then updating the reference in the partner or debitor to point to the new relation instead of the old one.
#### Advantages
- **Preserving the API:** This behavior is already implemented and requires no major API remodelling, only an extension to swap additional relations.
- **UPDATE permission for AGENT:** The AGENT role of a relation could be granted UPDATE rights because only the non-critical contact reference would be modifiable.
- **Congruence of business logic and API:** Conceptually, this aligns with replacing the partner person, though technically, a new PARTNER relation is created instead of directly replacing the person.
#### Disadvantages
- **Loss of explicit GRANTs:** Explicit GRANTs on the PARTNER relation would be lost due to the relation being replaced. Preserving these would require additional implementation effort.
- **Mismatch between business logic and API:** The exchange of the partner person would not directly occur at the partner but rather through a new PARTNER relation.
- **Not applicable to dependent relations:** Updating dependent relations (e.g., representatives, operational contacts, billing contacts, mailing list subscriptions) would require creating new relations and deleting old ones, again leading to the loss of explicit GRANTs.
- **Performance issues with many dependent relations:** Dependent relations can only be exchanged via loops rather than direct SQL UPDATEs, leading to poorer performance.
### Option 2: Directly Update Relations
The existing PARTNER relation remains unchanged, and the Holder is switched from the deceased person to the heir community.
#### Advantages
- **Applicability to both partner and debitor persons:** This approach would work for both partner and debitor persons at a generic level.
- **API uniformity and generality:** REST API changes would belong uniformly to the relation endpoint, consistent with how contact changes are currently handled.
#### Disadvantages
- **UPDATE permission for relation-AGENT would be problematic:** The relation-AGENT must not have permission to swap the Holder. Since there are no column-specific update rights, they would also lose the ability to change the Contact.
- **API remodelling:** The exchange of a partner person would move from the partner endpoint (/api/hs/office/partner) to the relation endpoint, requiring significant restructuring, including tests.
- **Mismatch between business logic and API:** Although conceptually it involves replacing a partner person, technically, the change would occur at the PARTNER relation.
### Option 3: Update Relations via Partner
The partner (or debitor) person would still be updated at the partner or debitor level, but instead of creating a new relation, the reference to the person would be updated in the existing PARTNER (or DEBITOR) relation. Dependent relations could be updated efficiently via SQL UPDATE.
#### Advantages
- **Preserving the API:** The endpoint /api/hs/office/partners/UUID remains unchanged, requiring only internal adjustments.
- **UPDATE permission for AGENT:** AGENT roles could be granted UPDATE rights, while API controls could limit modifications.
- **Congruence of business logic and API:** The technical implementation matches the conceptual model of replacing a partner person.
#### Disadvantages
No significant drawbacks were identified, other than allowing UPDATE permissions on relations while controlling updates at the API level.
## Decision and Outcome
**Decision:** 3. Update Relations via Partner
**Rationale:**
- The API accurately reflects the business logic (PATCH partner person on /api/hs/office/partners/UUID)
- The effort required is relatively low (many updates can be done via SQL UPDATEs)
- UPDATE permission can be granted to relation-AGENT without security risks (since the API controls access)
| Criteria \ Relations ... | 1. Replace | 2. Directly Update | 3. Update via Partner |
|------------------------------------------------|-----------:|-------------------:|----------------------:|
| **Technical and Effort Criteria** | | | |
| Preserve API vs. Remodelling (incl. risk) | +2 | -2 | +1 |
| Applicability to Partner and Debitor Person | | +1 | |
| Applicability to dependent relations | -3 | | |
| Performance with many dependent relations | -1 | | |
| Effort for explicit grants | -1 | | |
| **Intermediate Score** | **-3** | **-1** | **+1** |
| | | | |
| **Business Criteria** | | | |
| Congruence of Business Logic and API | +1 | -1 | +1 |
| Uniformity/Generality of the API | | +1 | |
| UPDATE Permission for Relation-AGENT possible | +1 | | +1 |
| **Intermediate Score** | **+2** | **0** | **+2** |
| | | | |
| **Final Score** | **-1** | **-1** | **+3** |

View File

@ -108,6 +108,40 @@ der Person des _Subscriber-Contact_ (_Holder_) zur repräsentierten Person (_Anc
Zusätzlich wird diese Relation mit dem Kurznamen der abonnierten Mailingliste markiert.
### Coop-Asset-Transactions (Geschäftsguthabens-Transaktionen)
- positiver Wert => Geschäftsguthaben nehmen zu
- negativer Wert => Geschäftsguthaben nehmen ab
**REVERSAL**: **Korrekturbuchung** einer fehlerhaften Buchung, positiver oder negativer Wert ist möglich
**DEPOSIT**: **Zahlungseingang** vom Mitglied nach Beteiligung mit Geschäftsanteilen, immer positiver Wert
**DISBURSAL**: **Zahlungsausgang** an Mitglied nach Kündigung von Geschäftsanteilen, immer negativer Wert
**TRANSFER**: **Übertragung** von Geschäftsguthaben an ein anderes Mitglied, immer negativer Wert
**ADOPTION**: **Übernahme** von Geschäftsguthaben von einem anderen Mitglied, immer positiver Wert
**CLEARING**: **Verrechnung** von Geschäftsguthaben mit Schulden des Mitglieds, immer negativer Wert
**LOSS**: **Verlust** von Geschäftsguthaben bei Zuweisung Eigenkapitalverlust nach Kündigung von Geschäftsanteilen, immer negativer Wert
**LIMITATION**: **Verjährung** von Geschäftsguthaben, wenn Auszahlung innerhalb der Frist nicht möglich war.
### Coop-Share-Transactions (Geschäftsanteil-Transaktionen)
- positiver Wert => Geschäftsanteile nehmen zu
- negativer Wert => Geschäftsanteile nehmen ab
-
**REVERSAL**: **Korrekturbuchung** einer fehlerhaften Buchung, positiver oder negativer Wert ist möglich
**SUBSCRIPTION**: **Beteiligung** mit Geschäftsanteilen, z.B. durch Beitrittserklärung, immer positiver Wert
**CANCELLATION**: **Kündigung** von Geschäftsanteilen, z.B. durch Austritt, immer negativer Wert
#### Anchor / Relation-Anchor
siehe [Relation](#Relation)

View File

@ -116,7 +116,7 @@ classDiagram
+BankAccount refundBankAccount
+String defaultPrefix: mei
}
debitor-MeierGmbH o-- partner-MeierGmbH
debitor-MeierGmbH o.. partner-MeierGmbH
debitor-MeierGmbH *-- rel-MeierGmbH-Buha
class contactData-MeierGmbH-Buha {

View File

@ -1,9 +1,11 @@
package net.hostsharing.hsadminng;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@OpenAPIDefinition
public class HsadminNgApplication {
public static void main(String[] args) {

View File

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

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

View File

@ -0,0 +1,36 @@
package net.hostsharing.hsadminng.config;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
// Do NOT use @Component (or similar) here, this would register the filter directly.
// But we need to register it in the SecurityFilterChain created by WebSecurityConfig.
// The bean gets created in net.hostsharing.hsadminng.config.WebSecurityConfig.authenticationFilter.
@AllArgsConstructor
public class CasAuthenticationFilter extends OncePerRequestFilter {
private CasAuthenticator authenticator;
@Override
@SneakyThrows
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
if (request.getHeader("authorization") != null) {
final var authenticatedRequest = new AuthenticatedHttpServletRequestWrapper(request);
final var currentSubject = authenticator.authenticate(request);
final var authentication = new UsernamePasswordAuthenticationToken(currentSubject, null, null);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(authenticatedRequest, response);
} else {
filterChain.doFilter(request, response);
}
}
}

View File

@ -1,71 +1,8 @@
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 {
public interface CasAuthenticator {
@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;
}
String authenticate(final HttpServletRequest httpRequest);
}

View File

@ -4,11 +4,11 @@ import lombok.SneakyThrows;
import jakarta.servlet.http.HttpServletRequest;
public class FakeAuthenticator implements Authenticator {
public class FakeCasAuthenticator implements CasAuthenticator {
@Override
@SneakyThrows
public String authenticate(final HttpServletRequest httpRequest) {
return httpRequest.getHeader("current-subject");
return httpRequest.getHeader("Authorization").replaceAll("^Bearer ", "");
}
}

View File

@ -0,0 +1,18 @@
package net.hostsharing.hsadminng.config;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.TYPE;
/** Explicitly marks a REST-Controller for not requiring authorization for Swagger UI.
*
* @see SecurityRequirement
*/
@Target(TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface NoSecurityRequirement {
}

View File

@ -0,0 +1,80 @@
package net.hostsharing.hsadminng.config;
import io.micrometer.core.annotation.Timed;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
import jakarta.servlet.http.HttpServletRequest;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
// HOWTO add logger
@Slf4j
public class RealCasAuthenticator implements CasAuthenticator {
@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 ticket = httpRequest.getHeader("authorization").replaceAll("^Bearer ", "");
final var serviceTicket = ticket.startsWith("TGT-")
? fetchServiceTicket(ticket)
: ticket;
final var userName = extractUserName(verifyServiceTicket(serviceTicket));
// HOWTO log some message for a certain log level (trace, debug, info, warn, error)
log.debug("CAS-user: {}", userName);
return userName;
}
private String fetchServiceTicket(final String ticketGrantingTicket) {
final var tgtUrl = casServerUrl + "/cas/v1/tickets/" + ticketGrantingTicket;
final var restTemplate = new RestTemplate();
final var formData = new LinkedMultiValueMap<String, String>();
formData.add("service", serviceUrl);
return restTemplate.postForObject(tgtUrl, formData, String.class);
}
private Document verifyServiceTicket(final String serviceTicket) throws SAXException, IOException, ParserConfigurationException {
if ( !serviceTicket.startsWith("ST-") ) {
throwBadCredentialsException("Invalid authorization ticket");
}
final var url = casServerUrl + "/cas/p3/serviceValidate" +
"?service=" + serviceUrl +
"&ticket=" + serviceTicket;
final var response = restTemplate.getForObject(url, String.class);
return DocumentBuilderFactory.newInstance().newDocumentBuilder()
.parse(new java.io.ByteArrayInputStream(response.getBytes()));
}
private String extractUserName(final Document verification) {
if (verification.getElementsByTagName("cas:authenticationSuccess").getLength() == 0) {
throwBadCredentialsException("CAS service ticket could not be verified");
}
return verification.getElementsByTagName("cas:user").item(0).getTextContent();
}
private void throwBadCredentialsException(final String message) {
throw new BadCredentialsException(message);
}
}

View File

@ -1,36 +1,71 @@
package net.hostsharing.hsadminng.config;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
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;
import org.springframework.security.web.authentication.AuthenticationFilter;
import jakarta.servlet.http.HttpServletResponse;
@Configuration
@EnableWebSecurity
// TODO.impl: securitySchemes should work in OpenAPI yaml, but the Spring templates seem not to support it
@SecurityScheme(type = SecuritySchemeType.HTTP, name = "casTicket", scheme = "bearer", bearerFormat = "CAS ticket", description = "CAS ticket", in = SecuritySchemeIn.HEADER)
public class WebSecurityConfig {
@Lazy
@Autowired
private CasAuthenticationFilter authenticationFilter;
@Bean
@Profile("!test")
public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/**").permitAll() // TODO.impl: implement authentication
.requestMatchers("/swagger-ui/**").permitAll()
.requestMatchers("/v3/api-docs/**").permitAll()
.requestMatchers("/actuator/**").permitAll()
.anyRequest().authenticated()
.requestMatchers(
"/swagger-ui/**",
"/v3/api-docs/**",
"/actuator/**",
"/api/hs/hosting/asset-types/**"
).permitAll()
.requestMatchers("/api/**").authenticated()
.anyRequest().denyAll()
)
.addFilterBefore(authenticationFilter, AuthenticationFilter.class)
.csrf(AbstractHttpConfigurer::disable)
.exceptionHandling(exception -> exception
.authenticationEntryPoint((request, response, authException) ->
// For unknown reasons Spring security returns 403 FORBIDDEN for a BadCredentialsException.
// But it should return 401 UNAUTHORIZED.
response.sendError(HttpServletResponse.SC_UNAUTHORIZED)
)
)
.build();
}
@Bean
@Profile("!test")
public Authenticator casServiceTicketValidator() {
return new CasAuthenticator();
@Profile("realCasAuthenticator")
public CasAuthenticator realCasServiceTicketValidator() {
return new RealCasAuthenticator();
}
@Bean
@Profile("fakeCasAuthenticator")
public CasAuthenticator fakeCasServiceTicketValidator() {
return new FakeCasAuthenticator();
}
@Bean
public CasAuthenticationFilter authenticationFilter(final CasAuthenticator authenticator) {
return new CasAuthenticationFilter(authenticator);
}
}

View File

@ -4,6 +4,7 @@ import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.request.RequestContextHolder;
@ -47,6 +48,12 @@ public class Context {
define(toTask(request), toCurl(request), currentSubject, assumedRoles);
}
@Transactional(propagation = MANDATORY)
public void assumeRoles(final String assumedRoles) {
final var currentSubject = SecurityContextHolder.getContext().getAuthentication().getName();
define(toTask(request), toCurl(request), currentSubject, assumedRoles);
}
@Transactional(propagation = MANDATORY)
public void define(
final String currentTask,

View File

@ -21,12 +21,12 @@ public class HttpServletRequestWithCachedBody extends HttpServletRequestWrapper
}
@Override
public ServletInputStream getInputStream() throws IOException {
public ServletInputStream getInputStream() {
return new HttpServletRequestBodyCache(this.cachedBody);
}
@Override
public BufferedReader getReader() throws IOException {
public BufferedReader getReader() {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
return new BufferedReader(new InputStreamReader(byteArrayInputStream));
}

View File

@ -28,6 +28,7 @@ import java.util.regex.Pattern;
import static net.hostsharing.hsadminng.errors.CustomErrorResponse.*;
@ControllerAdvice
// HOWTO handle exceptions to produce specific http error codes and sensible error messages
public class RestResponseEntityExceptionHandler
extends ResponseEntityExceptionHandler {

View File

@ -121,10 +121,4 @@ public final class HashGenerator {
}
return withSalt(stringBuilder.toString());
}
public static void main(String[] args) {
System.out.println(
HashGenerator.using(Algorithm.LINUX_YESCRYPT).withRandomSalt().hash("my plaintext domain transfer passphrase")
);
}
}

View File

@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.booking.item;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingItemsApi;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemInsertResource;
@ -32,6 +33,7 @@ import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateR
@RestController
@Profile("!only-office")
@SecurityRequirement(name = "casTicket")
public class HsBookingItemController implements HsBookingItemsApi {
@Autowired
@ -56,10 +58,9 @@ public class HsBookingItemController implements HsBookingItemsApi {
@Transactional(readOnly = true)
@Timed("app.bookingItems.api.getListOfBookingItemsByProjectUuid")
public ResponseEntity<List<HsBookingItemResource>> getListOfBookingItemsByProjectUuid(
final String currentSubject,
final String assumedRoles,
final UUID projectUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var entities = bookingItemRepo.findAllByProjectUuid(projectUuid);
@ -71,11 +72,10 @@ public class HsBookingItemController implements HsBookingItemsApi {
@Transactional
@Timed("app.bookingItems.api.postNewBookingItem")
public ResponseEntity<HsBookingItemResource> postNewBookingItem(
final String currentSubject,
final String assumedRoles,
final HsBookingItemInsertResource body) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var entityToSave = mapper.map(body, HsBookingItemRbacEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
final var saveProcessor = new BookingItemEntitySaveProcessor(em, entityToSave);
@ -101,11 +101,10 @@ public class HsBookingItemController implements HsBookingItemsApi {
@Transactional(readOnly = true)
@Timed("app.bookingItems.api.getSingleBookingItemByUuid")
public ResponseEntity<HsBookingItemResource> getSingleBookingItemByUuid(
final String currentSubject,
final String assumedRoles,
final UUID bookingItemUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = bookingItemRepo.findByUuid(bookingItemUuid);
result.ifPresent(entity -> em.detach(entity)); // prevent further LAZY-loading
@ -119,10 +118,9 @@ public class HsBookingItemController implements HsBookingItemsApi {
@Transactional
@Timed("app.bookingItems.api.deleteBookingIemByUuid")
public ResponseEntity<Void> deleteBookingIemByUuid(
final String currentSubject,
final String assumedRoles,
final UUID bookingItemUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = bookingItemRepo.deleteByUuid(bookingItemUuid);
return result == 0
@ -134,12 +132,11 @@ public class HsBookingItemController implements HsBookingItemsApi {
@Transactional
@Timed("app.bookingItems.api.patchBookingItem")
public ResponseEntity<HsBookingItemResource> patchBookingItem(
final String currentSubject,
final String assumedRoles,
final UUID bookingItemUuid,
final HsBookingItemPatchResource body) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var current = bookingItemRepo.findByUuid(bookingItemUuid).orElseThrow();

View File

@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.hs.booking.project;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorRepository;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingProjectsApi;
@ -22,6 +23,7 @@ import java.util.function.BiConsumer;
@RestController
@Profile("!only-office")
@SecurityRequirement(name = "casTicket")
public class HsBookingProjectController implements HsBookingProjectsApi {
@Autowired
@ -40,10 +42,9 @@ public class HsBookingProjectController implements HsBookingProjectsApi {
@Transactional(readOnly = true)
@Timed("app.bookingProjects.api.getListOfBookingProjectsByDebitorUuid")
public ResponseEntity<List<HsBookingProjectResource>> getListOfBookingProjectsByDebitorUuid(
final String currentSubject,
final String assumedRoles,
final UUID debitorUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var entities = bookingProjectRepo.findAllByDebitorUuid(debitorUuid);
@ -55,11 +56,10 @@ public class HsBookingProjectController implements HsBookingProjectsApi {
@Transactional
@Timed("app.bookingProjects.api.postNewBookingProject")
public ResponseEntity<HsBookingProjectResource> postNewBookingProject(
final String currentSubject,
final String assumedRoles,
final HsBookingProjectInsertResource body) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var entityToSave = mapper.map(body, HsBookingProjectRbacEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
@ -78,11 +78,10 @@ public class HsBookingProjectController implements HsBookingProjectsApi {
@Transactional(readOnly = true)
@Timed("app.bookingProjects.api.getBookingProjectByUuid")
public ResponseEntity<HsBookingProjectResource> getBookingProjectByUuid(
final String currentSubject,
final String assumedRoles,
final UUID bookingProjectUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = bookingProjectRepo.findByUuid(bookingProjectUuid);
return result
@ -95,10 +94,9 @@ public class HsBookingProjectController implements HsBookingProjectsApi {
@Transactional
@Timed("app.bookingProjects.api.deleteBookingIemByUuid")
public ResponseEntity<Void> deleteBookingIemByUuid(
final String currentSubject,
final String assumedRoles,
final UUID bookingProjectUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = bookingProjectRepo.deleteByUuid(bookingProjectUuid);
return result == 0
@ -110,12 +108,11 @@ public class HsBookingProjectController implements HsBookingProjectsApi {
@Transactional
@Timed("app.bookingProjects.api.patchBookingProject")
public ResponseEntity<HsBookingProjectResource> patchBookingProject(
final String currentSubject,
final String assumedRoles,
final UUID bookingProjectUuid,
final HsBookingProjectPatchResource body) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var current = bookingProjectRepo.findByUuid(bookingProjectUuid).orElseThrow();

View File

@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.hs.hosting.asset;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealRepository;
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntitySaveProcessor;
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntityValidatorRegistry;
@ -29,6 +30,7 @@ import java.util.function.BiConsumer;
@RestController
@Profile("!only-office")
@SecurityRequirement(name = "casTicket")
public class HsHostingAssetController implements HsHostingAssetsApi {
@Autowired
@ -53,12 +55,11 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
@Transactional(readOnly = true)
@Timed("app.hosting.assets.api.getListOfHostingAssets")
public ResponseEntity<List<HsHostingAssetResource>> getListOfHostingAssets(
final String currentSubject,
final String assumedRoles,
final UUID debitorUuid,
final UUID parentAssetUuid,
final HsHostingAssetTypeResource type) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var entities = rbacAssetRepo.findAllByCriteria(debitorUuid, parentAssetUuid, HsHostingAssetType.of(type));
@ -71,11 +72,10 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
@Transactional
@Timed("app.hosting.assets.api.postNewHostingAsset")
public ResponseEntity<HsHostingAssetResource> postNewHostingAsset(
final String currentSubject,
final String assumedRoles,
final HsHostingAssetInsertResource body) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var entity = mapper.map(body, HsHostingAssetRbacEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
@ -100,11 +100,10 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
@Transactional(readOnly = true)
@Timed("app.hosting.assets.api.getSingleHostingAssetByUuid")
public ResponseEntity<HsHostingAssetResource> getSingleHostingAssetByUuid(
final String currentSubject,
final String assumedRoles,
final UUID assetUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = rbacAssetRepo.findByUuid(assetUuid);
return result
@ -117,10 +116,9 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
@Transactional
@Timed("app.hosting.assets.api.deleteHostingAssetByUuid")
public ResponseEntity<Void> deleteHostingAssetByUuid(
final String currentSubject,
final String assumedRoles,
final UUID assetUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = rbacAssetRepo.deleteByUuid(assetUuid);
return result == 0
@ -132,12 +130,11 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
@Transactional
@Timed("app.hosting.assets.api.patchHostingAsset")
public ResponseEntity<HsHostingAssetResource> patchHostingAsset(
final String currentSubject,
final String assumedRoles,
final UUID assetUuid,
final HsHostingAssetPatchResource body) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var entity = rbacAssetRepo.findByUuid(assetUuid).orElseThrow();

View File

@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.hs.hosting.asset;
import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.config.NoSecurityRequirement;
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;
@ -14,6 +15,7 @@ import java.util.Map;
@RestController
@Profile("!only-office")
@NoSecurityRequirement
public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
@Override

View File

@ -125,10 +125,4 @@ public class Dns {
return Result.fromException(exception);
}
}
public static void main(String[] args) {
final var result = new Dns("example.org").fetchRecordsOfType("TXT");
System.out.println(result);
}
}

View File

@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.hs.office.bankaccount;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
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;
@ -18,7 +19,7 @@ import java.util.List;
import java.util.UUID;
@RestController
@SecurityRequirement(name = "casTicket")
public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
@Autowired
@ -34,10 +35,9 @@ public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
@Transactional(readOnly = true)
@Timed("app.office.bankAccounts.api.patchDebitor")
public ResponseEntity<List<HsOfficeBankAccountResource>> getListOfBankAccounts(
final String currentSubject,
final String assumedRoles,
final String holder) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var entities = bankAccountRepo.findByOptionalHolderLike(holder);
@ -49,11 +49,10 @@ public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
@Transactional
@Timed("app.office.bankAccounts.api.postNewBankAccount")
public ResponseEntity<HsOfficeBankAccountResource> postNewBankAccount(
final String currentSubject,
final String assumedRoles,
final HsOfficeBankAccountInsertResource body) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
IbanUtil.validate(body.getIban());
BicUtil.validate(body.getBic());
@ -76,11 +75,10 @@ public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
@Transactional(readOnly = true)
@Timed("app.office.bankAccounts.api.getSingleBankAccountByUuid")
public ResponseEntity<HsOfficeBankAccountResource> getSingleBankAccountByUuid(
final String currentSubject,
final String assumedRoles,
final UUID bankAccountUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = bankAccountRepo.findByUuid(bankAccountUuid);
if (result.isEmpty()) {
@ -93,10 +91,9 @@ public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
@Transactional
@Timed("app.office.bankAccounts.api.deleteBankAccountByUuid")
public ResponseEntity<Void> deleteBankAccountByUuid(
final String currentSubject,
final String assumedRoles,
final UUID BankAccountUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = bankAccountRepo.deleteByUuid(BankAccountUuid);
if (result == 0) {

View File

@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.hs.office.contact;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeContactsApi;
@ -13,15 +14,14 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import jakarta.annotation.PostConstruct;
import java.util.List;
import java.util.UUID;
import java.util.function.BiConsumer;
import static net.hostsharing.hsadminng.errors.Validate.validate;
import static net.hostsharing.hsadminng.mapper.KeyValueMap.from;
@RestController
@SecurityRequirement(name = "casTicket")
public class HsOfficeContactController implements HsOfficeContactsApi {
@Autowired
@ -30,18 +30,28 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
@Autowired
private StrictMapper mapper;
@Autowired
private HsOfficeContactFromResourceConverter<HsOfficeContactRbacEntity> contactFromResourceConverter;
@Autowired
private HsOfficeContactRbacRepository contactRepo;
@PostConstruct
public void init() {
// HOWTO: add a ModelMapper converter for a generic entity class to a ModelMapper to be used in a certain context
// This @PostConstruct could be implemented in the converter, but only without generics.
// But this converter is for HsOfficeContactRbacEntity and HsOfficeContactRealEntity.
mapper.addConverter(contactFromResourceConverter, HsOfficeContactInsertResource.class, HsOfficeContactRbacEntity.class);
}
@Override
@Transactional(readOnly = true)
@Timed("app.office.contacts.api.getListOfContacts")
public ResponseEntity<List<HsOfficeContactResource>> getListOfContacts(
final String currentSubject,
final String assumedRoles,
final String caption,
final String emailAddress) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
validate("caption, emailAddress").atMaxOne(caption, emailAddress);
final var entities = emailAddress != null
@ -56,13 +66,12 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
@Transactional
@Timed("app.office.contacts.api.postNewContact")
public ResponseEntity<HsOfficeContactResource> postNewContact(
final String currentSubject,
final String assumedRoles,
final HsOfficeContactInsertResource body) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var entityToSave = mapper.map(body, HsOfficeContactRbacEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
final var entityToSave = mapper.map(body, HsOfficeContactRbacEntity.class);
final var saved = contactRepo.save(entityToSave);
@ -79,11 +88,10 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
@Transactional(readOnly = true)
@Timed("app.office.contacts.api.getSingleContactByUuid")
public ResponseEntity<HsOfficeContactResource> getSingleContactByUuid(
final String currentSubject,
final String assumedRoles,
final UUID contactUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = contactRepo.findByUuid(contactUuid);
if (result.isEmpty()) {
@ -96,10 +104,9 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
@Transactional
@Timed("app.office.contacts.api.deleteContactByUuid")
public ResponseEntity<Void> deleteContactByUuid(
final String currentSubject,
final String assumedRoles,
final UUID contactUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = contactRepo.deleteByUuid(contactUuid);
if (result == 0) {
@ -113,12 +120,11 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
@Transactional
@Timed("app.office.contacts.api.patchContact")
public ResponseEntity<HsOfficeContactResource> patchContact(
final String currentSubject,
final String assumedRoles,
final UUID contactUuid,
final HsOfficeContactPatchResource body) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var current = contactRepo.findByUuid(contactUuid).orElseThrow();
@ -128,11 +134,4 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
final var mapped = mapper.map(saved, HsOfficeContactResource.class);
return ResponseEntity.ok(mapped);
}
@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

@ -0,0 +1,27 @@
package net.hostsharing.hsadminng.hs.office.contact;
import lombok.SneakyThrows;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactInsertResource;
import org.modelmapper.Converter;
import org.modelmapper.spi.MappingContext;
import org.springframework.stereotype.Component;
import static net.hostsharing.hsadminng.mapper.KeyValueMap.from;
// HOWTO: implement a ModelMapper converter which converts from a (JSON) resource instance to a generic entity instance (RBAC vs. REAL)
@Component
public class HsOfficeContactFromResourceConverter<E extends HsOfficeContact>
implements Converter<HsOfficeContactInsertResource, E> {
@Override
@SneakyThrows
public E convert(final MappingContext<HsOfficeContactInsertResource, E> context) {
final var resource = context.getSource();
final var entity = context.getDestinationType().getDeclaredConstructor().newInstance();
entity.setCaption(resource.getCaption());
entity.putPostalAddress(from(resource.getPostalAddress()));
entity.putEmailAddresses(from(resource.getEmailAddresses()));
entity.putPhoneNumbers(from(resource.getPhoneNumbers()));
return entity;
}
}

View File

@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.hs.office.coopassets;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.errors.MultiValidationException;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopAssetsApi;
@ -37,6 +38,7 @@ import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOffic
import static net.hostsharing.hsadminng.lambda.WithNonNull.withNonNull;
@RestController
@SecurityRequirement(name = "casTicket")
public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAssetsApi {
@Autowired
@ -58,12 +60,11 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
@Transactional(readOnly = true)
@Timed("app.office.coopAssets.api.getListOfCoopAssets")
public ResponseEntity<List<HsOfficeCoopAssetsTransactionResource>> getListOfCoopAssets(
final String currentSubject,
final String assumedRoles,
final UUID membershipUuid,
final @DateTimeFormat(iso = ISO.DATE) LocalDate fromValueDate,
final @DateTimeFormat(iso = ISO.DATE) LocalDate toValueDate) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var entities = coopAssetsTransactionRepo.findCoopAssetsTransactionByOptionalMembershipUuidAndDateRange(
membershipUuid,
@ -81,11 +82,10 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
@Transactional
@Timed("app.office.coopAssets.api.postNewCoopAssetTransaction")
public ResponseEntity<HsOfficeCoopAssetsTransactionResource> postNewCoopAssetTransaction(
final String currentSubject,
final String assumedRoles,
final HsOfficeCoopAssetsTransactionInsertResource requestBody) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
validate(requestBody);
final var entityToSave = mapper.map(
@ -107,9 +107,9 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
@Transactional(readOnly = true)
@Timed("app.office.coopAssets.api.getSingleCoopAssetTransactionByUuid")
public ResponseEntity<HsOfficeCoopAssetsTransactionResource> getSingleCoopAssetTransactionByUuid(
final String currentSubject, final String assumedRoles, final UUID assetTransactionUuid) {
final String assumedRoles, final UUID assetTransactionUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = coopAssetsTransactionRepo.findByUuid(assetTransactionUuid);
if (result.isEmpty()) {

View File

@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.hs.office.coopshares;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.errors.MultiValidationException;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopSharesApi;
@ -27,6 +28,7 @@ import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOffic
import static net.hostsharing.hsadminng.hs.validation.UuidResolver.resolve;
@RestController
@SecurityRequirement(name = "casTicket")
public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopSharesApi {
@Autowired
@ -45,12 +47,11 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
@Transactional(readOnly = true)
@Timed("app.office.coopShares.api.getListOfCoopShares")
public ResponseEntity<List<HsOfficeCoopSharesTransactionResource>> getListOfCoopShares(
final String currentSubject,
final String assumedRoles,
final UUID membershipUuid,
final @DateTimeFormat(iso = ISO.DATE) LocalDate fromValueDate,
final @DateTimeFormat(iso = ISO.DATE) LocalDate toValueDate) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var entities = coopSharesTransactionRepo.findCoopSharesTransactionByOptionalMembershipUuidAndDateRange(
membershipUuid,
@ -68,11 +69,10 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
@Transactional
@Timed("app.office.coopShares.repo.postNewCoopSharesTransaction")
public ResponseEntity<HsOfficeCoopSharesTransactionResource> postNewCoopSharesTransaction(
final String currentSubject,
final String assumedRoles,
final HsOfficeCoopSharesTransactionInsertResource requestBody) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
validate(requestBody);
final var entityToSave = mapper.map(
@ -95,9 +95,9 @@ 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 assumedRoles, final UUID shareTransactionUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = coopSharesTransactionRepo.findByUuid(shareTransactionUuid);
if (result.isEmpty()) {

View File

@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.hs.office.debitor;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountRepository;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealRepository;
@ -32,7 +33,7 @@ import static net.hostsharing.hsadminng.hs.validation.UuidResolver.resolve;
import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag;
@RestController
@SecurityRequirement(name = "casTicket")
public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
@Autowired
@ -63,12 +64,11 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
@Transactional(readOnly = true)
@Timed("app.office.debitors.api.getListOfDebitors")
public ResponseEntity<List<HsOfficeDebitorResource>> getListOfDebitors(
final String currentSubject,
final String assumedRoles,
final String name,
final UUID partnerUuid,
final String partnerNumber) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var entities = partnerNumber != null
? debitorRepo.findDebitorsByPartnerNumber(cropTag("P-", partnerNumber))
@ -84,11 +84,10 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
@Transactional
@Timed("app.office.debitors.api.postNewDebitor")
public ResponseEntity<HsOfficeDebitorResource> postNewDebitor(
String currentSubject,
String assumedRoles,
HsOfficeDebitorInsertResource body) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
Validate.isTrue(
body.getDebitorRel() == null || body.getDebitorRelUuid() == null,
@ -117,11 +116,10 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
@Transactional(readOnly = true)
@Timed("app.office.debitors.api.getSingleDebitorByUuid")
public ResponseEntity<HsOfficeDebitorResource> getSingleDebitorByUuid(
final String currentSubject,
final String assumedRoles,
final UUID debitorUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = debitorRepo.findByUuid(debitorUuid);
if (result.isEmpty()) {
@ -134,11 +132,10 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
@Transactional(readOnly = true)
@Timed("app.office.debitors.api.getSingleDebitorByDebitorNumber")
public ResponseEntity<HsOfficeDebitorResource> getSingleDebitorByDebitorNumber(
final String currentSubject,
final String assumedRoles,
final Integer debitorNumber) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = debitorRepo.findDebitorByDebitorNumber(debitorNumber);
if (result.isEmpty()) {
@ -151,10 +148,9 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
@Transactional
@Timed("app.office.debitors.api.deleteDebitorByUuid")
public ResponseEntity<Void> deleteDebitorByUuid(
final String currentSubject,
final String assumedRoles,
final UUID debitorUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = debitorRepo.deleteByUuid(debitorUuid);
if (result == 0) {
@ -168,12 +164,11 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
@Transactional
@Timed("app.office.debitors.api.patchDebitor")
public ResponseEntity<HsOfficeDebitorResource> patchDebitor(
final String currentSubject,
final String assumedRoles,
final UUID debitorUuid,
final HsOfficeDebitorPatchResource body) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var current = debitorRepo.findByUuid(debitorUuid).orElseThrow().reload(em);

View File

@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.hs.office.membership;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeMembershipsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipInsertResource;
@ -24,6 +25,7 @@ import static net.hostsharing.hsadminng.errors.Validate.validate;
import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag;
@RestController
@SecurityRequirement(name = "casTicket")
public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
@Autowired
@ -42,11 +44,10 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
@Transactional(readOnly = true)
@Timed("app.office.membership.api.getListOfMemberships")
public ResponseEntity<List<HsOfficeMembershipResource>> getListOfMemberships(
final String currentSubject,
final String assumedRoles,
final UUID partnerUuid,
final String partnerNumber) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
validate("partnerUuid, partnerNumber").atMaxOne(partnerUuid, partnerNumber);
@ -67,11 +68,10 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
@Transactional
@Timed("app.office.membership.api.postNewMembership")
public ResponseEntity<HsOfficeMembershipResource> postNewMembership(
final String currentSubject,
final String assumedRoles,
final HsOfficeMembershipInsertResource body) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var entityToSave = mapper.map(body, HsOfficeMembershipEntity.class, SEPA_MANDATE_RESOURCE_TO_ENTITY_POSTMAPPER);
@ -92,11 +92,10 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
@Transactional(readOnly = true)
@Timed("app.office.membership.api.getSingleMembershipByUuid")
public ResponseEntity<HsOfficeMembershipResource> getSingleMembershipByUuid(
final String currentSubject,
final String assumedRoles,
final UUID membershipUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = membershipRepo.findByUuid(membershipUuid);
if (result.isEmpty()) {
@ -111,11 +110,10 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
@Transactional(readOnly = true)
@Timed("app.office.membership.api.getSingleMembershipByMembershipNumber")
public ResponseEntity<HsOfficeMembershipResource> getSingleMembershipByMembershipNumber(
final String currentSubject,
final String assumedRoles,
final Integer membershipNumber) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = membershipRepo.findMembershipByMemberNumber(membershipNumber);
if (result.isEmpty()) {
@ -130,10 +128,9 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
@Transactional
@Timed("app.office.membership.api.deleteMembershipByUuid")
public ResponseEntity<Void> deleteMembershipByUuid(
final String currentSubject,
final String assumedRoles,
final UUID membershipUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = membershipRepo.deleteByUuid(membershipUuid);
if (result == 0) {
@ -147,12 +144,11 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
@Transactional
@Timed("app.office.membership.api.patchMembership")
public ResponseEntity<HsOfficeMembershipResource> patchMembership(
final String currentSubject,
final String assumedRoles,
final UUID membershipUuid,
final HsOfficeMembershipPatchResource body) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var current = membershipRepo.findByUuid(membershipUuid).orElseThrow();

View File

@ -1,10 +1,13 @@
package net.hostsharing.hsadminng.hs.office.partner;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.errors.ReferenceNotFoundException;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactFromResourceConverter;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficePartnersApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerPatchResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerResource;
@ -22,6 +25,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import jakarta.annotation.PostConstruct;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.util.List;
@ -32,7 +36,7 @@ import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.
import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag;
@RestController
@SecurityRequirement(name = "casTicket")
public class HsOfficePartnerController implements HsOfficePartnersApi {
@Autowired
@ -42,24 +46,31 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
private StrictMapper mapper;
@Autowired
private HsOfficePartnerRbacRepository partnerRepo;
private HsOfficeContactFromResourceConverter<HsOfficeContactRealEntity> contactFromResourceConverter;
@Autowired
private HsOfficeRelationRealRepository relationRepo;
private HsOfficePartnerRbacRepository rbacPartnerRepo;
@Autowired
private HsOfficeRelationRealRepository realRelationRepo;
@PersistenceContext
private EntityManager em;
@PostConstruct
public void init() {
mapper.addConverter(contactFromResourceConverter, HsOfficeContactInsertResource.class, HsOfficeContactRealEntity.class);
}
@Override
@Transactional(readOnly = true)
@Timed("app.office.partners.api.getListOfPartners")
public ResponseEntity<List<HsOfficePartnerResource>> getListOfPartners(
final String currentSubject,
final String assumedRoles,
final String name) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var entities = partnerRepo.findPartnerByOptionalNameLike(name);
final var entities = rbacPartnerRepo.findPartnerByOptionalNameLike(name);
final var resources = mapper.mapList(entities, HsOfficePartnerResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(resources);
@ -69,15 +80,14 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
@Transactional
@Timed("app.office.partners.api.postNewPartner")
public ResponseEntity<HsOfficePartnerResource> postNewPartner(
final String currentSubject,
final String assumedRoles,
final HsOfficePartnerInsertResource body) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var entityToSave = createPartnerEntity(body);
final var saved = partnerRepo.save(entityToSave);
final var saved = rbacPartnerRepo.save(entityToSave);
final var uri =
MvcUriComponentsBuilder.fromController(getClass())
@ -92,13 +102,12 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
@Transactional(readOnly = true)
@Timed("app.office.partners.api.getSinglePartnerByUuid")
public ResponseEntity<HsOfficePartnerResource> getSinglePartnerByUuid(
final String currentSubject,
final String assumedRoles,
final UUID partnerUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = partnerRepo.findByUuid(partnerUuid);
final var result = rbacPartnerRepo.findByUuid(partnerUuid);
if (result.isEmpty()) {
return ResponseEntity.notFound().build();
}
@ -110,13 +119,12 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
@Transactional(readOnly = true)
@Timed("app.office.partners.api.getSinglePartnerByPartnerNumber")
public ResponseEntity<HsOfficePartnerResource> getSinglePartnerByPartnerNumber(
final String currentSubject,
final String assumedRoles,
final Integer partnerNumber) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = partnerRepo.findPartnerByPartnerNumber(partnerNumber);
final var result = rbacPartnerRepo.findPartnerByPartnerNumber(partnerNumber);
if (result.isEmpty()) {
return ResponseEntity.notFound().build();
}
@ -128,17 +136,16 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
@Transactional
@Timed("app.office.partners.api.deletePartnerByUuid")
public ResponseEntity<Void> deletePartnerByUuid(
final String currentSubject,
final String assumedRoles,
final UUID partnerUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var partnerToDelete = partnerRepo.findByUuid(partnerUuid);
final var partnerToDelete = rbacPartnerRepo.findByUuid(partnerUuid);
if (partnerToDelete.isEmpty()) {
return ResponseEntity.notFound().build();
}
if (partnerRepo.deleteByUuid(partnerUuid) != 1) {
if (rbacPartnerRepo.deleteByUuid(partnerUuid) != 1) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
@ -149,29 +156,61 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
@Transactional
@Timed("app.office.partners.api.patchPartner")
public ResponseEntity<HsOfficePartnerResource> patchPartner(
final String currentSubject,
final String assumedRoles,
final UUID partnerUuid,
final HsOfficePartnerPatchResource body) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var current = partnerRepo.findByUuid(partnerUuid).orElseThrow();
final var previousPartnerRel = current.getPartnerRel();
final var current = rbacPartnerRepo.findByUuid(partnerUuid).orElseThrow();
final var previousPartnerPerson = current.getPartnerRel().getHolder();
new HsOfficePartnerEntityPatcher(em, current).apply(body);
new HsOfficePartnerEntityPatcher(mapper, em, current).apply(body);
final var saved = partnerRepo.save(current);
optionallyCreateExPartnerRelation(saved, previousPartnerRel);
final var saved = rbacPartnerRepo.save(current);
optionallyCreateExPartnerRelation(saved, previousPartnerPerson);
optionallyUpdateRelatedRelations(saved, previousPartnerPerson);
final var mapped = mapper.map(saved, HsOfficePartnerResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(mapped);
}
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 void optionallyCreateExPartnerRelation(final HsOfficePartnerRbacEntity saved, final HsOfficePersonRealEntity previousPartnerPerson) {
final var partnerPersonHasChanged = !saved.getPartnerRel().getHolder().getUuid().equals(previousPartnerPerson.getUuid());
if (partnerPersonHasChanged) {
realRelationRepo.save(saved.getPartnerRel().toBuilder()
.uuid(null)
.type(EX_PARTNER)
.anchor(saved.getPartnerRel().getHolder())
.holder(previousPartnerPerson)
.build());
}
}
private void optionallyUpdateRelatedRelations(final HsOfficePartnerRbacEntity saved, final HsOfficePersonRealEntity previousPartnerPerson) {
final var partnerPersonHasChanged = !saved.getPartnerRel().getHolder().getUuid().equals(previousPartnerPerson.getUuid());
if (partnerPersonHasChanged) {
// self-debitors of the old partner-person become self-debitors of the new partner person
em.createNativeQuery("""
UPDATE hs_office.relation
SET holderUuid = :newPartnerPersonUuid
WHERE type = 'DEBITOR' AND
holderUuid = :oldPartnerPersonUuid AND anchorUuid = :oldPartnerPersonUuid
""")
.setParameter("oldPartnerPersonUuid", previousPartnerPerson.getUuid())
.setParameter("newPartnerPersonUuid", saved.getPartnerRel().getHolder().getUuid())
.executeUpdate();
// re-anchor all relations from the old partner person to the new partner persion
em.createNativeQuery("""
UPDATE hs_office.relation
SET anchorUuid = :newPartnerPersonUuid
WHERE anchorUuid = :oldPartnerPersonUuid
""")
.setParameter("oldPartnerPersonUuid", previousPartnerPerson.getUuid())
.setParameter("newPartnerPersonUuid", saved.getPartnerRel().getHolder().getUuid())
.executeUpdate();
}
}

View File

@ -1,35 +1,36 @@
package net.hostsharing.hsadminng.hs.office.partner;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerPatchResource;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationPatcher;
import net.hostsharing.hsadminng.mapper.EntityPatcher;
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import jakarta.persistence.EntityManager;
class HsOfficePartnerEntityPatcher implements EntityPatcher<HsOfficePartnerPatchResource> {
private final StrictMapper mapper;
private final EntityManager em;
private final HsOfficePartnerRbacEntity entity;
HsOfficePartnerEntityPatcher(
final StrictMapper mapper,
final EntityManager em,
final HsOfficePartnerRbacEntity entity) {
this.mapper = mapper;
this.em = em;
this.entity = entity;
}
@Override
public void apply(final HsOfficePartnerPatchResource resource) {
OptionalFromJson.of(resource.getPartnerRelUuid()).ifPresent(newValue -> {
verifyNotNull(newValue, "partnerRel");
entity.setPartnerRel(em.getReference(HsOfficeRelationRealEntity.class, newValue));
});
new HsOfficePartnerDetailsEntityPatcher(em, entity.getDetails()).apply(resource.getDetails());
}
if (resource.getPartnerRel() != null) {
new HsOfficeRelationPatcher(mapper, em, entity.getPartnerRel()).apply(resource.getPartnerRel());
}
private void verifyNotNull(final Object newValue, final String propertyName) {
if (newValue == null) {
throw new IllegalArgumentException("property '" + propertyName + "' must not be null");
if (resource.getDetails() != null) {
new HsOfficePartnerDetailsEntityPatcher(em, entity.getDetails()).apply(resource.getDetails());
}
}
}

View File

@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.hs.office.person;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficePersonsApi;
@ -17,7 +18,7 @@ import java.util.List;
import java.util.UUID;
@RestController
@SecurityRequirement(name = "casTicket")
public class HsOfficePersonController implements HsOfficePersonsApi {
@Autowired
@ -33,10 +34,9 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
@Transactional(readOnly = true)
@Timed("app.office.persons.api.getListOfPersons")
public ResponseEntity<List<HsOfficePersonResource>> getListOfPersons(
final String currentSubject,
final String assumedRoles,
final String name) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var entities = personRepo.findPersonByOptionalNameLike(name);
@ -48,11 +48,10 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
@Transactional
@Timed("app.office.persons.api.postNewPerson")
public ResponseEntity<HsOfficePersonResource> postNewPerson(
final String currentSubject,
final String assumedRoles,
final HsOfficePersonInsertResource body) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var entityToSave = mapper.map(body, HsOfficePersonRbacEntity.class);
@ -71,11 +70,10 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
@Transactional(readOnly = true)
@Timed("app.office.persons.api.getSinglePersonByUuid")
public ResponseEntity<HsOfficePersonResource> getSinglePersonByUuid(
final String currentSubject,
final String assumedRoles,
final UUID personUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = personRepo.findByUuid(personUuid);
if (result.isEmpty()) {
@ -88,10 +86,9 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
@Transactional
@Timed("app.office.persons.api.deletePersonByUuid")
public ResponseEntity<Void> deletePersonByUuid(
final String currentSubject,
final String assumedRoles,
final UUID personUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = personRepo.deleteByUuid(personUuid);
if (result == 0) {
@ -105,12 +102,11 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
@Transactional
@Timed("app.office.persons.api.patchPerson")
public ResponseEntity<HsOfficePersonResource> patchPerson(
final String currentSubject,
final String assumedRoles,
final UUID personUuid,
final HsOfficePersonPatchResource body) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var current = personRepo.findByUuid(personUuid).orElseThrow();

View File

@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.hs.office.relation;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.errors.Validate;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
@ -26,6 +27,7 @@ import java.util.function.BiConsumer;
import static net.hostsharing.hsadminng.mapper.KeyValueMap.from;
@RestController
@SecurityRequirement(name = "casTicket")
public class HsOfficeRelationController implements HsOfficeRelationsApi {
@Autowired
@ -50,14 +52,13 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
@Transactional(readOnly = true)
@Timed("app.office.relations.api.getListOfRelations")
public ResponseEntity<List<HsOfficeRelationResource>> getListOfRelations(
final String currentSubject,
final String assumedRoles,
final UUID personUuid,
final HsOfficeRelationTypeResource relationType,
final String mark,
final String personData,
final String contactData) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final List<HsOfficeRelationRbacEntity> entities =
rbacRelationRepo.findRelationRelatedToPersonUuidRelationTypeMarkPersonAndContactData(
@ -74,11 +75,10 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
@Transactional
@Timed("app.office.relations.api.postNewRelation")
public ResponseEntity<HsOfficeRelationResource> postNewRelation(
final String currentSubject,
final String assumedRoles,
final HsOfficeRelationInsertResource body) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var entityToSave = new HsOfficeRelationRbacEntity();
entityToSave.setType(HsOfficeRelationType.valueOf(body.getType()));
@ -126,11 +126,10 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
@Transactional(readOnly = true)
@Timed("app.office.relations.api.getSingleRelationByUuid")
public ResponseEntity<HsOfficeRelationResource> getSingleRelationByUuid(
final String currentSubject,
final String assumedRoles,
final UUID relationUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = rbacRelationRepo.findByUuid(relationUuid);
if (result.isEmpty()) {
@ -143,10 +142,9 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
@Transactional
@Timed("apprelations.api..deleteRelationByUuid")
public ResponseEntity<Void> deleteRelationByUuid(
final String currentSubject,
final String assumedRoles,
final UUID relationUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = rbacRelationRepo.deleteByUuid(relationUuid);
if (result == 0) {
@ -160,16 +158,15 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
@Transactional
@Timed("app.office.relations.api.patchRelation")
public ResponseEntity<HsOfficeRelationResource> patchRelation(
final String currentSubject,
final String assumedRoles,
final UUID relationUuid,
final HsOfficeRelationPatchResource body) {
final HsOfficeRelationContactPatchResource body) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var current = rbacRelationRepo.findByUuid(relationUuid).orElseThrow();
new HsOfficeRelationEntityPatcher(em, current).apply(body);
new HsOfficeRelationEntityContactPatcher(em, current).apply(body);
final var saved = rbacRelationRepo.save(current);
final var mapped = mapper.map(saved, HsOfficeRelationResource.class);

View File

@ -1,25 +1,25 @@
package net.hostsharing.hsadminng.hs.office.relation;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeRelationPatchResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeRelationContactPatchResource;
import net.hostsharing.hsadminng.mapper.EntityPatcher;
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
import jakarta.persistence.EntityManager;
import java.util.UUID;
class HsOfficeRelationEntityPatcher implements EntityPatcher<HsOfficeRelationPatchResource> {
public class HsOfficeRelationEntityContactPatcher implements EntityPatcher<HsOfficeRelationContactPatchResource> {
private final EntityManager em;
private final HsOfficeRelation entity;
HsOfficeRelationEntityPatcher(final EntityManager em, final HsOfficeRelation entity) {
public HsOfficeRelationEntityContactPatcher(final EntityManager em, final HsOfficeRelation entity) {
this.em = em;
this.entity = entity;
}
@Override
public void apply(final HsOfficeRelationPatchResource resource) {
public void apply(final HsOfficeRelationContactPatchResource resource) {
OptionalFromJson.of(resource.getContactUuid()).ifPresent(newValue -> {
verifyNotNull(newValue, "contact");
entity.setContact(em.getReference(HsOfficeContactRealEntity.class, newValue));

View File

@ -0,0 +1,50 @@
package net.hostsharing.hsadminng.hs.office.relation;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeRelationPatchResource;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
import net.hostsharing.hsadminng.mapper.EntityPatcher;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import jakarta.persistence.EntityManager;
import jakarta.validation.ValidationException;
public class HsOfficeRelationPatcher implements EntityPatcher<HsOfficeRelationPatchResource> {
private final StrictMapper mapper;
private final EntityManager em;
private final HsOfficeRelation entity;
public HsOfficeRelationPatcher(final StrictMapper mapper, final EntityManager em, final HsOfficeRelation entity) {
this.mapper = mapper;
this.em = em;
this.entity = entity;
}
@Override
public void apply(final HsOfficeRelationPatchResource resource) {
if (resource.getHolder() != null && resource.getHolderUuid() != null) {
throw new ValidationException("either \"holder\" or \"holder.uuid\" can be given, not both");
} else {
if (resource.getHolder() != null) {
final var newHolder = mapper.map(resource.getHolder(), HsOfficePersonRealEntity.class);
em.persist(newHolder);
entity.setHolder(newHolder);
} else if (resource.getHolderUuid() != null) {
entity.setHolder(em.getReference(HsOfficePersonRealEntity.class, resource.getHolderUuid().get()));
}
}
if (resource.getContact() != null && resource.getContactUuid() != null) {
throw new ValidationException("either \"contact\" or \"contact.uuid\" can be given, not both");
} else {
if (resource.getContact() != null) {
final var newContact = mapper.map(resource.getContact(), HsOfficeContactRealEntity.class);
em.persist(newContact);
entity.setContact(newContact);
} else if (resource.getContactUuid() != null) {
entity.setContact(em.getReference(HsOfficeContactRealEntity.class, resource.getContactUuid().get()));
}
}
}
}

View File

@ -51,7 +51,7 @@ public class HsOfficeRelationRbacEntity extends HsOfficeRelation {
"""))
.withRestrictedViewOrderBy(SQL.expression(
"(select idName from hs_office.person_iv p where p.uuid = target.holderUuid)"))
.withUpdatableColumns("contactUuid")
.withUpdatableColumns("anchorUuid", "holderUuid", "contactUuid") // BEWARE: additional checks at API-level
.importEntityAlias("anchorPerson", HsOfficePersonRbacEntity.class, usingDefaultCase(),
dependsOnColumn("anchorUuid"),
directlyFetchedByDependsOnColumn(),

View File

@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.hs.office.sepamandate;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountRepository;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository;
@ -26,7 +27,7 @@ import java.util.function.BiConsumer;
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
@RestController
@SecurityRequirement(name = "casTicket")
public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
@Autowired
@ -51,10 +52,9 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
@Transactional(readOnly = true)
@Timed("app.office.sepaMandates.api.getListOfSepaMandates")
public ResponseEntity<List<HsOfficeSepaMandateResource>> getListOfSepaMandates(
final String currentSubject,
final String assumedRoles,
final String iban) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var entities = sepaMandateRepo.findSepaMandateByOptionalIban(iban);
@ -67,11 +67,10 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
@Transactional
@Timed("app.office.sepaMandates.api.postNewSepaMandate")
public ResponseEntity<HsOfficeSepaMandateResource> postNewSepaMandate(
final String currentSubject,
final String assumedRoles,
final HsOfficeSepaMandateInsertResource body) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var entityToSave = mapper.map(body, HsOfficeSepaMandateEntity.class, SEPA_MANDATE_RESOURCE_TO_ENTITY_POSTMAPPER);
@ -91,11 +90,10 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
@Transactional(readOnly = true)
@Timed("app.office.sepaMandates.api.getSingleSepaMandateByUuid")
public ResponseEntity<HsOfficeSepaMandateResource> getSingleSepaMandateByUuid(
final String currentSubject,
final String assumedRoles,
final UUID sepaMandateUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = sepaMandateRepo.findByUuid(sepaMandateUuid);
if (result.isEmpty()) {
@ -109,10 +107,9 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
@Transactional
@Timed("app.office.sepaMandates.api.deleteSepaMandateByUuid")
public ResponseEntity<Void> deleteSepaMandateByUuid(
final String currentSubject,
final String assumedRoles,
final UUID sepaMandateUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = sepaMandateRepo.deleteByUuid(sepaMandateUuid);
if (result == 0) {
@ -126,12 +123,11 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
@Transactional
@Timed("app.office.sepaMandates.api.patchSepaMandate")
public ResponseEntity<HsOfficeSepaMandateResource> patchSepaMandate(
final String currentSubject,
final String assumedRoles,
final UUID sepaMandateUuid,
final HsOfficeSepaMandatePatchResource body) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var current = sepaMandateRepo.findByUuid(sepaMandateUuid).orElseThrow();

View File

@ -1,12 +1,12 @@
package net.hostsharing.hsadminng.ping;
import org.springframework.security.core.context.SecurityContextHolder;
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 {
@ -14,9 +14,8 @@ public class PingController {
@ResponseBody
@RequestMapping(value = "/api/ping", method = RequestMethod.GET)
public String ping(
@RequestHeader(name = "current-subject") @NotNull String currentSubject,
@RequestHeader(name = "assumed-roles", required = false) String assumedRoles
) {
return "pong " + currentSubject + "\n";
return "pong " + SecurityContextHolder.getContext().getAuthentication().getName() + "\n";
}
}

View File

@ -22,7 +22,7 @@ class RbacRbacSystemRebuildGenerator {
void generateTo(final StringWriter plPgSql) {
plPgSql.writeLn("""
-- ============================================================================
--changeset RbacRbacSystemRebuildGenerator:${liquibaseTagPrefix}-rbac-rebuild endDelimiter:--//
--changeset RbacRbacSystemRebuildGenerator:${liquibaseTagPrefix}-rbac-rebuild runOnChange:true validCheckSum:ANY endDelimiter:--//
-- ----------------------------------------------------------------------------
-- HOWTO: Rebuild RBAC-system for table ${rawTableName} after changing its RBAC specification.

View File

@ -19,7 +19,7 @@ public class RbacRestrictedViewGenerator {
void generateTo(final StringWriter plPgSql) {
plPgSql.writeLn("""
-- ============================================================================
--changeset RbacRestrictedViewGenerator:${liquibaseTagPrefix}-rbac-RESTRICTED-VIEW endDelimiter:--//
--changeset RbacRestrictedViewGenerator:${liquibaseTagPrefix}-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--//
-- ----------------------------------------------------------------------------
call rbac.generateRbacRestrictedView('${rawTableName}',
$orderBy$

View File

@ -1261,15 +1261,11 @@ public class RbacSpec {
m -> isStatic(m.getModifiers()) && m.getName().equals("main")
)
.findFirst()
.orElse(null);
if (mainMethod != null) {
try {
mainMethod.invoke(null, new Object[] { null });
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
} else {
System.err.println("WARNING: no main method in: " + c.getName() + " => no RBAC rules generated");
.orElseThrow(() -> new RuntimeException("no main method in: " + c.getName() + " => cannot generate RBAC rules"));
try {
mainMethod.invoke(null, new Object[] { null });
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}

View File

@ -202,6 +202,5 @@ public class RbacViewMermaidFlowchartGenerator {
.replace("%{flowchart}", flowchart.toString())
.replace("%{case}", forCase == null ? "" : " " + forCase),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
System.out.println("Markdown-File: " + path.toAbsolutePath());
}
}

View File

@ -49,6 +49,5 @@ public class RbacViewPostgresGenerator {
toString(),
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING);
System.out.println(outputPath.toAbsolutePath());
}
}

View File

@ -52,7 +52,7 @@ class RolesGrantsAndPermissionsGenerator {
private void generateHeader(final StringWriter plPgSql, final String triggerType) {
plPgSql.writeLn("""
-- ============================================================================
--changeset RolesGrantsAndPermissionsGenerator:${liquibaseTagPrefix}-rbac-${triggerType}-trigger endDelimiter:--//
--changeset RolesGrantsAndPermissionsGenerator:${liquibaseTagPrefix}-rbac-${triggerType}-trigger runOnChange:true validCheckSum:ANY endDelimiter:--//
-- ----------------------------------------------------------------------------
""",
with("liquibaseTagPrefix", liquibaseTagPrefix),
@ -358,9 +358,6 @@ class RolesGrantsAndPermissionsGenerator {
}
private String roleRef(final PostgresTriggerReference rootRefVar, final RbacSpec.RbacRoleDefinition roleDef) {
if (roleDef == null) {
System.out.println("null");
}
if (roleDef.getEntityAlias().isGlobal()) {
return "rbac.global_ADMIN()";
}
@ -523,12 +520,11 @@ class RolesGrantsAndPermissionsGenerator {
return NEW;
end; $$;
create trigger build_rbac_system_after_insert_tg
create or replace trigger build_rbac_system_after_insert_tg
after insert on ${rawTableQualifiedName}
for each row
execute procedure ${rawTableQualifiedName}_build_rbac_system_after_insert_tf();
"""
.replace("${schemaPrefix}", schemaPrefix(qualifiedRawTableName))
.replace("${rawTableQualifiedName}", qualifiedRawTableName)
);
@ -558,7 +554,7 @@ class RolesGrantsAndPermissionsGenerator {
return NEW;
end; $$;
create trigger update_rbac_system_after_update_tg
create or replace trigger update_rbac_system_after_update_tg
after update on ${rawTableQualifiedName}
for each row
execute procedure ${rawTableQualifiedName}_update_rbac_system_after_update_tf();

View File

@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.rbac.grant;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacGrantsApi;
@ -17,6 +18,7 @@ import java.util.List;
import java.util.UUID;
@RestController
@SecurityRequirement(name = "casTicket")
public class RbacGrantController implements RbacGrantsApi {
@Autowired
@ -35,12 +37,11 @@ public class RbacGrantController implements RbacGrantsApi {
@Transactional(readOnly = true)
@Timed("app.rbac.grants.api.getListOfGrantsByUuid")
public ResponseEntity<RbacGrantResource> getListOfGrantsByUuid(
final String currentSubject,
final String assumedRoles,
final UUID grantedRoleUuid,
final UUID granteeSubjectUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var id = new RbacGrantId(granteeSubjectUuid, grantedRoleUuid);
final var result = rbacGrantRepository.findById(id);
@ -54,10 +55,9 @@ public class RbacGrantController implements RbacGrantsApi {
@Transactional(readOnly = true)
@Timed("app.rbac.grants.api.getListOfSubjectGrants")
public ResponseEntity<List<RbacGrantResource>> getListOfSubjectGrants(
final String currentSubject,
final String assumedRoles) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
return ResponseEntity.ok(mapper.mapList(rbacGrantRepository.findAll(), RbacGrantResource.class));
}
@ -66,11 +66,10 @@ public class RbacGrantController implements RbacGrantsApi {
@Transactional
@Timed("app.rbac.grants.api.postNewRoleGrantToSubject")
public ResponseEntity<RbacGrantResource> postNewRoleGrantToSubject(
final String currentSubject,
final String assumedRoles,
final RbacGrantResource body) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var granted = rbacGrantRepository.save(mapper.map(body, RbacGrantEntity.class));
em.flush();
@ -88,12 +87,11 @@ public class RbacGrantController implements RbacGrantsApi {
@Transactional
@Timed("app.rbac.grants.api.deleteRoleGrantFromSubject")
public ResponseEntity<Void> deleteRoleGrantFromSubject(
final String currentSubject,
final String assumedRoles,
final UUID grantedRoleUuid,
final UUID granteeSubjectUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
rbacGrantRepository.deleteByRbacGrantId(new RbacGrantId(granteeSubjectUuid, grantedRoleUuid));

View File

@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.rbac.role;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacRolesApi;
@ -13,6 +14,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@SecurityRequirement(name = "casTicket")
public class RbacRoleController implements RbacRolesApi {
@Autowired
@ -28,10 +30,9 @@ public class RbacRoleController implements RbacRolesApi {
@Transactional(readOnly = true)
@Timed("app.rbac.roles.api.getListOfRoles")
public ResponseEntity<List<RbacRoleResource>> getListOfRoles(
final String currentSubject,
final String assumedRoles) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final List<RbacRoleEntity> result = rbacRoleRepository.findAll();

View File

@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.rbac.subject;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacSubjectsApi;
@ -16,6 +17,7 @@ import java.util.List;
import java.util.UUID;
@RestController
@SecurityRequirement(name = "casTicket")
public class RbacSubjectController implements RbacSubjectsApi {
@Autowired
@ -42,7 +44,7 @@ public class RbacSubjectController implements RbacSubjectsApi {
rbacSubjectRepository.create(saved);
final var uri =
MvcUriComponentsBuilder.fromController(getClass())
.path("/api/rbac.yaml/users/{id}")
.path("/api/rbac/subjects/{id}")
.buildAndExpand(saved.getUuid())
.toUri();
return ResponseEntity.created(uri).body(mapper.map(saved, RbacSubjectResource.class));
@ -52,11 +54,10 @@ public class RbacSubjectController implements RbacSubjectsApi {
@Transactional
@Timed("app.rbac.subjects.api.deleteSubjectByUuid")
public ResponseEntity<Void> deleteSubjectByUuid(
final String currentSubject,
final String assumedRoles,
final UUID subjectUuid
) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
rbacSubjectRepository.deleteByUuid(subjectUuid);
@ -67,11 +68,10 @@ public class RbacSubjectController implements RbacSubjectsApi {
@Transactional(readOnly = true)
@Timed("app.rbac.subjects.api.getSingleSubjectByUuid")
public ResponseEntity<RbacSubjectResource> getSingleSubjectByUuid(
final String currentSubject,
final String assumedRoles,
final UUID subjectUuid) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = rbacSubjectRepository.findByUuid(subjectUuid);
if (result == null) {
@ -84,11 +84,10 @@ public class RbacSubjectController implements RbacSubjectsApi {
@Transactional(readOnly = true)
@Timed("app.rbac.subjects.api.getListOfSubjects")
public ResponseEntity<List<RbacSubjectResource>> getListOfSubjects(
final String currentSubject,
final String assumedRoles,
final String userName
) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
return ResponseEntity.ok(mapper.mapList(rbacSubjectRepository.findByOptionalNameLike(userName), RbacSubjectResource.class));
}
@ -97,11 +96,10 @@ public class RbacSubjectController implements RbacSubjectsApi {
@Transactional(readOnly = true)
@Timed("app.rbac.subjects.api.getListOfSubjectPermissions")
public ResponseEntity<List<RbacSubjectPermissionResource>> getListOfSubjectPermissions(
final String currentSubject,
final String assumedRoles,
final UUID subjectUuid
) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
return ResponseEntity.ok(mapper.mapList(
rbacSubjectRepository.findPermissionsOfUserByUuid(subjectUuid),

View File

@ -1,5 +1,6 @@
package net.hostsharing.hsadminng.rbac.test.cust;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.test.generated.api.v1.api.TestCustomersApi;
@ -15,6 +16,7 @@ import jakarta.persistence.PersistenceContext;
import java.util.List;
@RestController
@SecurityRequirement(name = "casTicket")
public class TestCustomerController implements TestCustomersApi {
@Autowired
@ -32,11 +34,10 @@ public class TestCustomerController implements TestCustomersApi {
@Override
@Transactional(readOnly = true)
public ResponseEntity<List<TestCustomerResource>> listCustomers(
String currentSubject,
String assumedRoles,
String prefix
) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = testCustomerRepository.findCustomerByOptionalPrefixLike(prefix);
@ -46,11 +47,10 @@ public class TestCustomerController implements TestCustomersApi {
@Override
@Transactional
public ResponseEntity<TestCustomerResource> addCustomer(
final String currentSubject,
final String assumedRoles,
final TestCustomerResource customer) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var saved = testCustomerRepository.save(mapper.map(customer, TestCustomerEntity.class));
final var uri =

View File

@ -1,5 +1,6 @@
package net.hostsharing.hsadminng.rbac.test.pac;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
import net.hostsharing.hsadminng.context.Context;
@ -15,6 +16,7 @@ import java.util.List;
import java.util.UUID;
@RestController
@SecurityRequirement(name = "casTicket")
public class TestPackageController implements TestPackagesApi {
@Autowired
@ -29,11 +31,10 @@ public class TestPackageController implements TestPackagesApi {
@Override
@Transactional(readOnly = true)
public ResponseEntity<List<TestPackageResource>> listPackages(
String currentSubject,
String assumedRoles,
String name
) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var result = testPackageRepository.findAllByOptionalNameLike(name);
return ResponseEntity.ok(mapper.mapList(result, TestPackageResource.class));
@ -42,12 +43,11 @@ public class TestPackageController implements TestPackagesApi {
@Override
@Transactional
public ResponseEntity<TestPackageResource> updatePackage(
final String currentSubject,
final String assumedRoles,
final UUID packageUuid,
final TestPackageUpdateResource body) {
context.define(currentSubject, assumedRoles);
context.assumeRoles(assumedRoles);
final var current = testPackageRepository.findByUuid(packageUuid);
OptionalFromJson.of(body.getDescription()).ifPresent(current::setDescription);

View File

@ -3,14 +3,6 @@ components:
parameters:
currentSubject:
name: current-subject
in: header
required: true
schema:
type: string
description: Identifying name of the current subject (e.g. user).
assumedRoles:
name: assumed-roles
in: header

View File

@ -1,20 +0,0 @@
components:
parameters:
currentSubject:
name: current-subject
in: header
required: true
schema:
type: string
description: Identifying name of the currently logged in subject.
assumedRoles:
name: assumed-roles
in: header
required: false
schema:
type: string
description: Semicolon-separated list of roles to assume. The current subject needs to have the right to assume these roles.

View File

@ -0,0 +1 @@
../auth.yaml

View File

@ -4,7 +4,6 @@ get:
description: 'Fetch a single booking item its uuid, if visible for the current subject.'
operationId: getSingleBookingItemByUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: bookingItemUuid
in: path
@ -32,7 +31,6 @@ patch:
description: 'Updates a single booking item identified by its uuid, if permitted for the current subject.'
operationId: patchBookingItem
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: bookingItemUuid
in: path
@ -63,7 +61,6 @@ delete:
description: 'Delete a single booking item identified by its uuid, if permitted for the current subject.'
operationId: deleteBookingIemByUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: bookingItemUuid
in: path

View File

@ -5,7 +5,6 @@ get:
- hs-booking-items
operationId: getListOfBookingItemsByProjectUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: projectUuid
in: query
@ -34,7 +33,6 @@ post:
- hs-booking-items
operationId: postNewBookingItem
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
requestBody:
description: A JSON object describing the new booking item.

View File

@ -4,7 +4,6 @@ get:
description: 'Fetch a single booking project its uuid, if visible for the current subject.'
operationId: getBookingProjectByUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: bookingProjectUuid
in: path
@ -32,7 +31,6 @@ patch:
description: 'Updates a single booking project identified by its uuid, if permitted for the current subject.'
operationId: patchBookingProject
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: bookingProjectUuid
in: path
@ -63,7 +61,6 @@ delete:
description: 'Delete a single booking project identified by its uuid, if permitted for the current subject.'
operationId: deleteBookingIemByUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: bookingProjectUuid
in: path

View File

@ -5,7 +5,6 @@ get:
- hs-booking-projects
operationId: getListOfBookingProjectsByDebitorUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: debitorUuid
in: query
@ -34,7 +33,6 @@ post:
- hs-booking-projects
operationId: postNewBookingProject
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
requestBody:
description: A JSON object describing the new booking project.

View File

@ -1,20 +0,0 @@
components:
parameters:
currentSubject:
name: current-subject
in: header
required: true
schema:
type: string
description: Identifying name of the currently logged in subject.
assumedRoles:
name: assumed-roles
in: header
required: false
schema:
type: string
description: Semicolon-separated list of roles to assume. The current subject needs to have the right to assume these roles.

View File

@ -0,0 +1 @@
../auth.yaml

View File

@ -4,7 +4,6 @@ get:
description: 'Fetch a single managed asset by its uuid, if visible for the current subject.'
operationId: getSingleHostingAssetByUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: assetUuid
in: path
@ -32,7 +31,6 @@ patch:
description: 'Updates a single hosting asset identified by its uuid, if permitted for the current subject.'
operationId: patchHostingAsset
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: assetUuid
in: path
@ -63,7 +61,6 @@ delete:
description: 'Delete a single hosting asset identified by its uuid, if permitted for the current subject.'
operationId: deleteHostingAssetByUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: assetUuid
in: path

View File

@ -5,7 +5,6 @@ get:
- hs-hosting-assets
operationId: getListOfHostingAssets
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: projectUuid
in: query
@ -47,7 +46,6 @@ post:
- hs-hosting-assets
operationId: postNewHostingAsset
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
requestBody:
description: A JSON object describing the new hosting asset.

View File

@ -4,7 +4,6 @@ get:
description: 'Fetch a single bank account by its uuid, if visible for the current subject.'
operationId: getSingleBankAccountByUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: bankAccountUUID
in: path
@ -31,7 +30,6 @@ delete:
description: 'Delete a single bank account by its uuid, if permitted for the current subject.'
operationId: deleteBankAccountByUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: bankAccountUUID
in: path

View File

@ -5,7 +5,6 @@ get:
- hs-office-bank-accounts
operationId: getListOfBankAccounts
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: holder
in: query
@ -33,7 +32,6 @@ post:
- hs-office-bank-accounts
operationId: postNewBankAccount
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
requestBody:
content:

View File

@ -4,7 +4,6 @@ get:
description: 'Fetch a single business contact by its uuid, if visible for the current subject.'
operationId: getSingleContactByUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: contactUUID
in: path
@ -32,7 +31,6 @@ patch:
description: 'Updates a single contact by its uuid, if permitted for the current subject.'
operationId: patchContact
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: contactUUID
in: path
@ -63,7 +61,6 @@ delete:
description: 'Delete a single business contact by its uuid, if permitted for the current subject.'
operationId: deleteContactByUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: contactUUID
in: path

View File

@ -5,7 +5,6 @@ get:
- hs-office-contacts
operationId: getListOfContacts
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: caption
in: query
@ -40,7 +39,6 @@ post:
- hs-office-contacts
operationId: postNewContact
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
requestBody:
content:

View File

@ -4,7 +4,6 @@ get:
description: 'Fetch a single asset transaction by its uuid, if visible for the current subject.'
operationId: getSingleCoopAssetTransactionByUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: assetTransactionUUID
in: path

View File

@ -5,7 +5,6 @@ get:
- hs-office-coopAssets
operationId: getListOfCoopAssets
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: membershipUuid
in: query
@ -48,7 +47,6 @@ post:
- hs-office-coopAssets
operationId: postNewCoopAssetTransaction
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
requestBody:
description: A JSON object describing the new cooperative assets transaction.

View File

@ -4,7 +4,6 @@ get:
description: 'Fetch a single share transaction by its uuid, if visible for the current subject.'
operationId: getSingleCoopShareTransactionByUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: shareTransactionUUID
in: path

View File

@ -5,7 +5,6 @@ get:
- hs-office-coopShares
operationId: getListOfCoopShares
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: membershipUuid
in: query
@ -48,7 +47,6 @@ post:
- hs-office-coopShares
operationId: postNewCoopSharesTransaction
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
requestBody:
description: A JSON object describing the new cooperative shares transaction.

View File

@ -4,7 +4,6 @@ get:
description: 'Fetch a single debitor by its debitorNumber, if visible for the current subject.'
operationId: getSingleDebitorByDebitorNumber
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: debitorNumber
in: path

View File

@ -4,7 +4,6 @@ get:
description: 'Fetch a single debitor by its uuid, if visible for the current subject.'
operationId: getSingleDebitorByUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: debitorUUID
in: path
@ -32,7 +31,6 @@ patch:
description: 'Updates a single debitor by its uuid, if permitted for the current subject.'
operationId: patchDebitor
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: debitorUUID
in: path
@ -63,7 +61,6 @@ delete:
description: 'Delete a single debitor by its uuid, if permitted for the current subject.'
operationId: deleteDebitorByUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: debitorUUID
in: path

View File

@ -5,7 +5,6 @@ get:
- hs-office-debitors
operationId: getListOfDebitors
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: name
in: query
@ -47,7 +46,6 @@ post:
- hs-office-debitors
operationId: postNewDebitor
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
requestBody:
content:

View File

@ -4,7 +4,6 @@ get:
description: 'Fetch a single membership by its membershipNumber, if visible for the current subject.'
operationId: getSingleMembershipByMembershipNumber
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: membershipNumber
in: path

View File

@ -4,7 +4,6 @@ get:
description: 'Fetch a single membership by its uuid, if visible for the current subject.'
operationId: getSingleMembershipByUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: membershipUUID
in: path
@ -32,7 +31,6 @@ patch:
description: 'Updates a single membership by its uuid, if permitted for the current subject.'
operationId: patchMembership
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: membershipUUID
in: path
@ -63,7 +61,6 @@ delete:
description: 'Delete a single membership by its uuid, if permitted for the current subject.'
operationId: deleteMembershipByUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: membershipUUID
in: path

View File

@ -6,7 +6,6 @@ get:
- hs-office-memberships
operationId: getListOfMemberships
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: partnerUuid
in: query
@ -42,7 +41,6 @@ post:
- hs-office-memberships
operationId: postNewMembership
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
requestBody:
description: A JSON object describing the new membership.

View File

@ -48,12 +48,11 @@ components:
HsOfficePartnerPatch:
type: object
properties:
partnerRel.uuid:
type: string
format: uuid
nullable: true
partnerRel:
$ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationPatch'
details:
$ref: '#/components/schemas/HsOfficePartnerDetailsPatch'
additionalProperties: false
HsOfficePartnerDetailsPatch:
type: object

View File

@ -4,7 +4,6 @@ get:
description: 'Fetch a single business partner by its partner-number (prefixed with "P-"), if visible for the current subject.'
operationId: getSinglePartnerByPartnerNumber
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: partnerNumber
in: path

View File

@ -4,7 +4,6 @@ get:
description: 'Fetch a single business partner by its uuid, if visible for the current subject.'
operationId: getSinglePartnerByUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: partnerUUID
in: path
@ -32,7 +31,6 @@ patch:
description: 'Updates a single business partner by its uuid, if permitted for the current subject.'
operationId: patchPartner
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: partnerUUID
in: path
@ -63,7 +61,6 @@ delete:
description: 'Delete a single business partner by its uuid, if permitted for the current subject.'
operationId: deletePartnerByUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: partnerUUID
in: path

View File

@ -5,7 +5,6 @@ get:
- hs-office-partners
operationId: getListOfPartners
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: name
in: query
@ -33,7 +32,6 @@ post:
- hs-office-partners
operationId: postNewPartner
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
requestBody:
content:

View File

@ -4,7 +4,6 @@ get:
description: 'Fetch a single business person by its uuid, if visible for the current subject.'
operationId: getSinglePersonByUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: personUUID
in: path
@ -32,7 +31,6 @@ patch:
description: 'Updates a single person by its uuid, if permitted for the current subject.'
operationId: patchPerson
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: personUUID
in: path
@ -63,7 +61,6 @@ delete:
description: 'Delete a single business person by its uuid, if permitted for the current subject.'
operationId: deletePersonByUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: personUUID
in: path

View File

@ -5,7 +5,6 @@ get:
- hs-office-persons
operationId: getListOfPersons
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: name
in: query
@ -33,7 +32,6 @@ post:
- hs-office-persons
operationId: postNewPerson
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
requestBody:
content:

View File

@ -34,7 +34,7 @@ components:
contact:
$ref: 'hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContact'
HsOfficeRelationPatch:
HsOfficeRelationContactPatch:
type: object
properties:
contact.uuid:
@ -42,6 +42,27 @@ components:
format: uuid
nullable: true
HsOfficeRelationPatch:
type: object
properties:
anchor.uuid:
type: string
format: uuid
nullable: true
holder.uuid:
type: string
format: uuid
nullable: true
holder:
$ref: 'hs-office-person-schemas.yaml#/components/schemas/HsOfficePersonInsert'
contact.uuid:
type: string
format: uuid
nullable: true
contact:
$ref: 'hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContactInsert'
additionalProperties: false
# arbitrary relation with explicit type
HsOfficeRelationInsert:
type: object

View File

@ -4,7 +4,6 @@ get:
description: 'Fetch a single person relation by its uuid, if visible for the current subject.'
operationId: getSingleRelationByUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: relationUUID
in: path
@ -32,7 +31,6 @@ patch:
description: 'Updates a single person relation by its uuid, if permitted for the current subject.'
operationId: patchRelation
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: relationUUID
in: path
@ -44,7 +42,7 @@ patch:
content:
'application/json':
schema:
$ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationPatch'
$ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationContactPatch'
responses:
"200":
description: OK
@ -63,7 +61,6 @@ delete:
description: 'Delete a single person relation by its uuid, if permitted for the current subject.'
operationId: deleteRelationByUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: relationUUID
in: path

View File

@ -7,7 +7,6 @@ get:
- hs-office-relations
operationId: getListOfRelations
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: personUuid
in: query
@ -60,7 +59,6 @@ post:
- hs-office-relations
operationId: postNewRelation
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
requestBody:
content:

View File

@ -4,7 +4,6 @@ get:
description: 'Fetch a single SEPA Mandate by its uuid, if visible for the current subject.'
operationId: getSingleSepaMandateByUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: sepaMandateUUID
in: path
@ -32,7 +31,6 @@ patch:
description: 'Updates a single SEPA Mandate by its uuid, if permitted for the current subject.'
operationId: patchSepaMandate
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: sepaMandateUUID
in: path
@ -63,7 +61,6 @@ delete:
description: 'Delete a single SEPA Mandate by its uuid, if permitted for the current subject.'
operationId: deleteSepaMandateByUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: sepaMandateUUID
in: path

View File

@ -5,7 +5,6 @@ get:
- hs-office-sepaMandates
operationId: getListOfSepaMandates
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: iban
in: query
@ -33,7 +32,6 @@ post:
- hs-office-sepaMandates
operationId: postNewSepaMandate
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
requestBody:
description: A JSON object describing the new SEPA-Mandate.

View File

@ -3,7 +3,6 @@ get:
- rbac-grants
operationId: getListOfGrantsByUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: grantedRoleUuid
in: path
@ -38,7 +37,6 @@ delete:
- rbac-grants
operationId: deleteRoleGrantFromSubject
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: grantedRoleUuid
in: path

View File

@ -3,7 +3,6 @@ get:
- rbac-grants
operationId: getListOfSubjectGrants
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
responses:
"200":
@ -20,7 +19,6 @@ post:
- rbac-grants
operationId: postNewRoleGrantToSubject
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
requestBody:
required: true

View File

@ -3,7 +3,6 @@ get:
- rbac-roles
operationId: getListOfRoles
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
responses:
"200":

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