Compare commits

..

1 Commits

Author SHA1 Message Date
Michael Hoennig
2ebb7556a4 use only persistViaSql 2025-02-06 11:37:43 +01:00
105 changed files with 1146 additions and 2324 deletions

View File

@ -109,15 +109,13 @@ 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 -f /tmp/gwTest.tmp
. .aliases;
. .tc-environment;
rm /tmp/gwTest.tmp
if [ "$1" == "--all" ]; then
shift # to remove the --all from $@
# delierately in separate gradlew-calls to avoid Testcontains-PostgreSQL problem spillover
@ -148,6 +146,6 @@ if [ ! -f .environment ]; then
fi
source .environment
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-upload='./gradlew scenarioTest convertMarkdownToHtml && ssh hsh03-hsngdev@h50.hostsharing.net "rm -f doms/hsngdev.hs-example.de/htdocs-ssl/scenarios/office/*.html" && scp build/doc/scenarios/*.html hsh03-hsngdev@h50.hostsharing.net:doms/hsngdev.hs-example.de/htdocs-ssl/scenarios/office'
alias scenario-reports-open='open https://hsngdev.hs-example.de/scenarios/office'

View File

@ -7,7 +7,6 @@
<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" />
@ -35,4 +34,4 @@
<RunAsTest>true</RunAsTest>
<method v="2" />
</configuration>
</component>
</component>

View File

@ -3,6 +3,7 @@
<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" />

View File

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

View File

@ -4,6 +4,4 @@ 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=

View File

@ -132,10 +132,9 @@ Also try for example 'admin@xxx.example.com' or 'unknown@example.org'.
If you want a formatted JSON output, you can pipe the result to `jq` or similar.
And to see the full, currently implemented, API, open http://localhost:8080/swagger-ui/index.html).
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.
And to see the full, currently implemented, API, open http://localhost:8081/actuator/swagger-ui/index.html (uses management-port and thus bypasses authentication).
If you still need to install some of these tools, find some hints in the next chapters.
### PostgreSQL Server
@ -667,29 +666,6 @@ 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

@ -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.2'
id 'org.springframework.boot' version '3.4.1'
id 'io.spring.dependency-management' version '1.1.7' // manages implicit dependencies
id 'io.openapiprocessor.openapi-processor' version '2023.2' // generates Controller-interface and resources from API-spec
id 'com.github.jk1.dependency-license-report' version '2.9' // checks dependency-license compatibility
@ -67,6 +67,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.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'
@ -76,7 +77,7 @@ 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.5'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.3'
implementation 'org.reflections:reflections:0.10.2'
compileOnly 'org.projectlombok:lombok'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

View File

@ -1,124 +0,0 @@
# Ä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

@ -1,124 +0,0 @@
# 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,40 +108,6 @@ 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,11 +1,9 @@
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

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

View File

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

View File

@ -1,33 +0,0 @@
package net.hostsharing.hsadminng.config;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
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);
authenticatedRequest.addHeader("current-subject", currentSubject);
filterChain.doFilter(authenticatedRequest, response);
} else {
filterChain.doFilter(request, response);
}
}
}

View File

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

View File

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

@ -1,100 +0,0 @@
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.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;
import java.util.function.Supplier;
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 userName = StringUtils.isBlank(casServerUrl)
? bypassCurrentSubject(httpRequest)
: casAuthentication(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("authorization").replaceAll("^Bearer ", "");
System.err.println("CasAuthenticator.bypassCurrentSubject: " + userName);
return userName;
}
private String casAuthentication(final HttpServletRequest httpRequest)
throws SAXException, IOException, ParserConfigurationException {
final var ticket = httpRequest.getHeader("authorization").replaceAll("^Bearer ", "");
final var serviceTicket = ticket.startsWith("TGT-")
? fetchServiceTicket(ticket)
: ticket;
final var userName = extractUserName(verifyServiceTicket(serviceTicket));
System.err.println("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 = ((Supplier<String>) () -> restTemplate.getForObject(url, String.class)).get();
return DocumentBuilderFactory.newInstance().newDocumentBuilder()
.parse(new java.io.ByteArrayInputStream(response.getBytes()));
}
private String extractUserName(final Document verification) {
if (verification.getElementsByTagName("cas:authenticationSuccess").getLength() == 0) {
System.err.println("CAS service ticket could not be validated");
System.err.println(verification);
throwBadCredentialsException("CAS service ticket could not be validated");
}
return verification.getElementsByTagName("cas:user").item(0).getTextContent();
}
private String throwBadCredentialsException(final String message) {
throw new BadCredentialsException(message);
}
}

View File

@ -1,63 +1,36 @@
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 {
private static final String[] PERMITTED_PATHS = new String[] { "/swagger-ui/**", "/v3/api-docs/**", "/actuator/**" };
private static final String[] AUTHENTICATED_PATHS = new String[] { "/api/**" };
@Lazy
@Autowired
private CasAuthenticationFilter authenticationFilter;
@Bean
@Profile("!test")
public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(PERMITTED_PATHS).permitAll()
.requestMatchers(AUTHENTICATED_PATHS).authenticated()
.anyRequest().denyAll()
.requestMatchers("/api/**").permitAll() // TODO.impl: implement authentication
.requestMatchers("/swagger-ui/**").permitAll()
.requestMatchers("/v3/api-docs/**").permitAll()
.requestMatchers("/actuator/**").permitAll()
.anyRequest().authenticated()
)
.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 CasAuthenticator casServiceTicketValidator() {
return new RealCasAuthenticator();
public Authenticator casServiceTicketValidator() {
return new CasAuthenticator();
}
@Bean
public CasAuthenticationFilter authenticationFilter(final CasAuthenticator authenticator) {
return new CasAuthenticationFilter(authenticator);
}
}

View File

@ -3,7 +3,6 @@ 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;
@ -33,7 +32,6 @@ import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateR
@RestController
@Profile("!only-office")
@SecurityRequirement(name = "casTicket")
public class HsBookingItemController implements HsBookingItemsApi {
@Autowired

View File

@ -1,7 +1,6 @@
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;
@ -23,7 +22,6 @@ import java.util.function.BiConsumer;
@RestController
@Profile("!only-office")
@SecurityRequirement(name = "casTicket")
public class HsBookingProjectController implements HsBookingProjectsApi {
@Autowired

View File

@ -1,7 +1,6 @@
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;
@ -30,7 +29,6 @@ import java.util.function.BiConsumer;
@RestController
@Profile("!only-office")
@SecurityRequirement(name = "casTicket")
public class HsHostingAssetController implements HsHostingAssetsApi {
@Autowired

View File

@ -1,7 +1,6 @@
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;
@ -15,7 +14,6 @@ import java.util.Map;
@RestController
@Profile("!only-office")
@NoSecurityRequirement
public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
@Override

View File

@ -1,7 +1,6 @@
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;
@ -19,7 +18,7 @@ import java.util.List;
import java.util.UUID;
@RestController
@SecurityRequirement(name = "casTicket")
public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
@Autowired

View File

@ -1,7 +1,6 @@
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;
@ -14,14 +13,15 @@ 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,20 +30,9 @@ 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")
@ -73,7 +62,7 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
context.define(currentSubject, assumedRoles);
final var entityToSave = mapper.map(body, HsOfficeContactRbacEntity.class);
final var entityToSave = mapper.map(body, HsOfficeContactRbacEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
final var saved = contactRepo.save(entityToSave);
@ -139,4 +128,11 @@ 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

@ -1,27 +0,0 @@
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,7 +1,6 @@
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;
@ -38,7 +37,6 @@ 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

View File

@ -1,7 +1,6 @@
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;
@ -28,7 +27,6 @@ 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

View File

@ -1,7 +1,6 @@
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;
@ -33,7 +32,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

View File

@ -1,7 +1,6 @@
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;
@ -25,7 +24,6 @@ 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

View File

@ -1,13 +1,10 @@
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;
@ -25,7 +22,6 @@ 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;
@ -36,7 +32,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
@ -46,22 +42,14 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
private StrictMapper mapper;
@Autowired
private HsOfficeContactFromResourceConverter<HsOfficeContactRealEntity> contactFromResourceConverter;
private HsOfficePartnerRbacRepository partnerRepo;
@Autowired
private HsOfficePartnerRbacRepository rbacPartnerRepo;
@Autowired
private HsOfficeRelationRealRepository realRelationRepo;
private HsOfficeRelationRealRepository relationRepo;
@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")
@ -71,7 +59,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
final String name) {
context.define(currentSubject, assumedRoles);
final var entities = rbacPartnerRepo.findPartnerByOptionalNameLike(name);
final var entities = partnerRepo.findPartnerByOptionalNameLike(name);
final var resources = mapper.mapList(entities, HsOfficePartnerResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(resources);
@ -89,7 +77,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
final var entityToSave = createPartnerEntity(body);
final var saved = rbacPartnerRepo.save(entityToSave);
final var saved = partnerRepo.save(entityToSave);
final var uri =
MvcUriComponentsBuilder.fromController(getClass())
@ -110,7 +98,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
context.define(currentSubject, assumedRoles);
final var result = rbacPartnerRepo.findByUuid(partnerUuid);
final var result = partnerRepo.findByUuid(partnerUuid);
if (result.isEmpty()) {
return ResponseEntity.notFound().build();
}
@ -128,7 +116,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
context.define(currentSubject, assumedRoles);
final var result = rbacPartnerRepo.findPartnerByPartnerNumber(partnerNumber);
final var result = partnerRepo.findPartnerByPartnerNumber(partnerNumber);
if (result.isEmpty()) {
return ResponseEntity.notFound().build();
}
@ -145,12 +133,12 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
final UUID partnerUuid) {
context.define(currentSubject, assumedRoles);
final var partnerToDelete = rbacPartnerRepo.findByUuid(partnerUuid);
final var partnerToDelete = partnerRepo.findByUuid(partnerUuid);
if (partnerToDelete.isEmpty()) {
return ResponseEntity.notFound().build();
}
if (rbacPartnerRepo.deleteByUuid(partnerUuid) != 1) {
if (partnerRepo.deleteByUuid(partnerUuid) != 1) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
@ -168,55 +156,22 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
context.define(currentSubject, assumedRoles);
final var current = rbacPartnerRepo.findByUuid(partnerUuid).orElseThrow();
final var previousPartnerPerson = current.getPartnerRel().getHolder();
final var current = partnerRepo.findByUuid(partnerUuid).orElseThrow();
final var previousPartnerRel = current.getPartnerRel();
new HsOfficePartnerEntityPatcher(mapper, em, current).apply(body);
new HsOfficePartnerEntityPatcher(em, current).apply(body);
final var saved = rbacPartnerRepo.save(current);
optionallyCreateExPartnerRelation(saved, previousPartnerPerson);
optionallyUpdateRelatedRelations(saved, previousPartnerPerson);
final var saved = partnerRepo.save(current);
optionallyCreateExPartnerRelation(saved, previousPartnerRel);
final var mapped = mapper.map(saved, HsOfficePartnerResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(mapped);
}
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();
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());
}
}

View File

@ -1,36 +1,35 @@
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.HsOfficeRelationPatcher;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
import net.hostsharing.hsadminng.mapper.EntityPatcher;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
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));
});
if (resource.getPartnerRel() != null) {
new HsOfficeRelationPatcher(mapper, em, entity.getPartnerRel()).apply(resource.getPartnerRel());
}
new HsOfficePartnerDetailsEntityPatcher(em, entity.getDetails()).apply(resource.getDetails());
}
if (resource.getDetails() != null) {
new HsOfficePartnerDetailsEntityPatcher(em, entity.getDetails()).apply(resource.getDetails());
private void verifyNotNull(final Object newValue, final String propertyName) {
if (newValue == null) {
throw new IllegalArgumentException("property '" + propertyName + "' must not be null");
}
}
}

View File

@ -1,7 +1,6 @@
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;
@ -18,7 +17,7 @@ import java.util.List;
import java.util.UUID;
@RestController
@SecurityRequirement(name = "casTicket")
public class HsOfficePersonController implements HsOfficePersonsApi {
@Autowired

View File

@ -1,7 +1,6 @@
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;
@ -27,7 +26,6 @@ import java.util.function.BiConsumer;
import static net.hostsharing.hsadminng.mapper.KeyValueMap.from;
@RestController
@SecurityRequirement(name = "casTicket")
public class HsOfficeRelationController implements HsOfficeRelationsApi {
@Autowired
@ -165,13 +163,13 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
final String currentSubject,
final String assumedRoles,
final UUID relationUuid,
final HsOfficeRelationContactPatchResource body) {
final HsOfficeRelationPatchResource body) {
context.define(currentSubject, assumedRoles);
final var current = rbacRelationRepo.findByUuid(relationUuid).orElseThrow();
new HsOfficeRelationEntityContactPatcher(em, current).apply(body);
new HsOfficeRelationEntityPatcher(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.HsOfficeRelationContactPatchResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeRelationPatchResource;
import net.hostsharing.hsadminng.mapper.EntityPatcher;
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
import jakarta.persistence.EntityManager;
import java.util.UUID;
public class HsOfficeRelationEntityContactPatcher implements EntityPatcher<HsOfficeRelationContactPatchResource> {
class HsOfficeRelationEntityPatcher implements EntityPatcher<HsOfficeRelationPatchResource> {
private final EntityManager em;
private final HsOfficeRelation entity;
public HsOfficeRelationEntityContactPatcher(final EntityManager em, final HsOfficeRelation entity) {
HsOfficeRelationEntityPatcher(final EntityManager em, final HsOfficeRelation entity) {
this.em = em;
this.entity = entity;
}
@Override
public void apply(final HsOfficeRelationContactPatchResource resource) {
public void apply(final HsOfficeRelationPatchResource resource) {
OptionalFromJson.of(resource.getContactUuid()).ifPresent(newValue -> {
verifyNotNull(newValue, "contact");
entity.setContact(em.getReference(HsOfficeContactRealEntity.class, newValue));

View File

@ -1,50 +0,0 @@
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("anchorUuid", "holderUuid", "contactUuid") // BEWARE: additional checks at API-level
.withUpdatableColumns("contactUuid")
.importEntityAlias("anchorPerson", HsOfficePersonRbacEntity.class, usingDefaultCase(),
dependsOnColumn("anchorUuid"),
directlyFetchedByDependsOnColumn(),

View File

@ -1,7 +1,6 @@
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;
@ -27,7 +26,7 @@ import java.util.function.BiConsumer;
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
@RestController
@SecurityRequirement(name = "casTicket")
public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
@Autowired

View File

@ -22,7 +22,7 @@ class RbacRbacSystemRebuildGenerator {
void generateTo(final StringWriter plPgSql) {
plPgSql.writeLn("""
-- ============================================================================
--changeset RbacRbacSystemRebuildGenerator:${liquibaseTagPrefix}-rbac-rebuild runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRbacSystemRebuildGenerator:${liquibaseTagPrefix}-rbac-rebuild 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 runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRestrictedViewGenerator:${liquibaseTagPrefix}-rbac-RESTRICTED-VIEW endDelimiter:--//
-- ----------------------------------------------------------------------------
call rbac.generateRbacRestrictedView('${rawTableName}',
$orderBy$

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 runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RolesGrantsAndPermissionsGenerator:${liquibaseTagPrefix}-rbac-${triggerType}-trigger endDelimiter:--//
-- ----------------------------------------------------------------------------
""",
with("liquibaseTagPrefix", liquibaseTagPrefix),
@ -523,11 +523,12 @@ class RolesGrantsAndPermissionsGenerator {
return NEW;
end; $$;
create or replace trigger build_rbac_system_after_insert_tg
create 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)
);
@ -557,7 +558,7 @@ class RolesGrantsAndPermissionsGenerator {
return NEW;
end; $$;
create or replace trigger update_rbac_system_after_update_tg
create 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,7 +1,6 @@
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;
@ -18,7 +17,6 @@ import java.util.List;
import java.util.UUID;
@RestController
@SecurityRequirement(name = "casTicket")
public class RbacGrantController implements RbacGrantsApi {
@Autowired

View File

@ -1,7 +1,6 @@
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;
@ -14,7 +13,6 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@SecurityRequirement(name = "casTicket")
public class RbacRoleController implements RbacRolesApi {
@Autowired

View File

@ -1,7 +1,6 @@
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;
@ -17,7 +16,6 @@ import java.util.List;
import java.util.UUID;
@RestController
@SecurityRequirement(name = "casTicket")
public class RbacSubjectController implements RbacSubjectsApi {
@Autowired

View File

@ -1,6 +1,5 @@
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;
@ -16,7 +15,6 @@ import jakarta.persistence.PersistenceContext;
import java.util.List;
@RestController
@SecurityRequirement(name = "casTicket")
public class TestCustomerController implements TestCustomersApi {
@Autowired

View File

@ -1,6 +1,5 @@
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;
@ -16,7 +15,6 @@ import java.util.List;
import java.util.UUID;
@RestController
@SecurityRequirement(name = "casTicket")
public class TestPackageController implements TestPackagesApi {
@Autowired

View File

@ -6,7 +6,7 @@ components:
currentSubject:
name: current-subject
in: header
required: false
required: true
schema:
type: string
description: Identifying name of the current subject (e.g. user).

View File

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

View File

@ -34,34 +34,13 @@ components:
contact:
$ref: 'hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContact'
HsOfficeRelationContactPatch:
type: object
properties:
contact.uuid:
type: string
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:

View File

@ -44,7 +44,7 @@ patch:
content:
'application/json':
schema:
$ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationContactPatch'
$ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationPatch'
responses:
"200":
description: OK

View File

@ -4,7 +4,6 @@ get:
tags:
- testCustomers
operationId: listCustomers
parameters:
- $ref: 'auth.yaml#/components/parameters/currentSubject'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'

View File

@ -17,7 +17,7 @@ management:
# HOWTO: view the effective application configuration properties:
# http://localhost:8081/actuator/configprops
include: info, health, metrics, metric-links, mappings, openapi, configprops, env
include: info, health, metrics, metric-links, mappings, openapi, swaggerui, configprops, env
endpoint:
env:
# TODO.spec: check this, maybe set to when_authorized?
@ -37,11 +37,6 @@ spring:
url: ${HSADMINNG_POSTGRES_JDBC_URL}
username: postgres
data:
rest:
# do NOT implicilty expose SpringData repositories as REST-controllers
detection-strategy: annotated
sql:
init:
mode: never
@ -54,6 +49,10 @@ spring:
liquibase:
contexts: ${spring.profiles.active}
# keep this in sync with test/.../application.yml
springdoc:
use-management-port: true
hsadminng:
postgres:
leakproof:
@ -67,9 +66,3 @@ metrics:
http:
server:
requests: true
logging:
level:
org:
springframework:
security: TRACE

View File

@ -22,12 +22,13 @@ select (objectTable || '#' || objectIdName || ':' || roleType) as roleIdName, *
--//
-- ============================================================================
--changeset michael.hoennig:rbac-views-ROLE-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset michael.hoennig:rbac-views-ROLE-RESTRICTED-VIEW endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Creates a view to the role table with row-level limitation
based on the grants of the current user or assumed roles.
*/
drop view if exists rbac.role_rv;
create or replace view rbac.role_rv as
select *
-- @formatter:off
@ -105,7 +106,7 @@ create or replace view rbac.grant_ev as
-- ============================================================================
--changeset michael.hoennig:rbac-views-GRANT-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset michael.hoennig:rbac-views-GRANT-RESTRICTED-VIEW endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Creates a view to the grants table with row-level limitation
@ -221,12 +222,13 @@ select distinct *
-- ============================================================================
--changeset michael.hoennig:rbac-views-USER-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset michael.hoennig:rbac-views-USER-RESTRICTED-VIEW endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Creates a view to the users table with row-level limitation
based on the grants of the current user or assumed roles.
*/
drop view if exists rbac.subject_rv;
create or replace view rbac.subject_rv as
select distinct *
-- @formatter:off
@ -314,13 +316,14 @@ execute function rbac.delete_subject_tf();
--/
-- ============================================================================
--changeset michael.hoennig:rbac-views-OWN-GRANTED-PERMISSIONS-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset michael.hoennig:rbac-views-OWN-GRANTED-PERMISSIONS-VIEW endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Creates a view to all permissions granted to the current user or
based on the grants of the current user or assumed roles.
*/
-- @formatter:off
drop view if exists rbac.own_granted_permissions_rv;
create or replace view rbac.own_granted_permissions_rv as
select r.uuid as roleuuid, p.uuid as permissionUuid,
(r.objecttable || ':' || r.objectidname || ':' || r.roletype) as roleName, p.op,

View File

@ -111,7 +111,7 @@ end; $$;
-- ============================================================================
--changeset michael.hoennig:rbac-generators-IDENTITY-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset michael.hoennig:rbac-generators-IDENTITY-VIEW endDelimiter:--//
-- ----------------------------------------------------------------------------
create or replace procedure rbac.generateRbacIdentityViewFromQuery(targetTable text, sqlQuery text)
@ -171,7 +171,7 @@ end; $$;
-- ============================================================================
--changeset michael.hoennig:rbac-generators-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset michael.hoennig:rbac-generators-RESTRICTED-VIEW endDelimiter:--//
-- ----------------------------------------------------------------------------
create or replace procedure rbac.generateRbacRestrictedView(targetTable text, orderBy text, columnUpdates text = null, columnNames text = '*')
@ -235,7 +235,7 @@ begin
*/
newColumns := 'new.' || replace(columnNames, ', ', ', new.');
sql := format($sql$
create or replace function %1$s_instead_of_insert_tf()
create function %1$s_instead_of_insert_tf()
returns trigger
language plpgsql as $f$
declare
@ -254,7 +254,7 @@ begin
Creates an instead of insert trigger for the restricted view.
*/
sql := format($sql$
create or replace trigger instead_of_insert_tg
create trigger instead_of_insert_tg
instead of insert
on %1$s_rv
for each row
@ -266,7 +266,7 @@ begin
Instead of delete trigger function for the restricted view.
*/
sql := format($sql$
create or replace function %1$s_instead_of_delete_tf()
create function %1$s_instead_of_delete_tf()
returns trigger
language plpgsql as $f$
begin
@ -283,7 +283,7 @@ begin
Creates an instead of delete trigger for the restricted view.
*/
sql := format($sql$
create or replace trigger instead_of_delete_tg
create trigger instead_of_delete_tg
instead of delete
on %1$s_rv
for each row
@ -297,7 +297,7 @@ begin
*/
if columnUpdates is not null then
sql := format($sql$
create or replace function %1$s_instead_of_update_tf()
create function %1$s_instead_of_update_tf()
returns trigger
language plpgsql as $f$
begin
@ -316,7 +316,7 @@ begin
Creates an instead of delete trigger for the restricted view.
*/
sql = format($sql$
create or replace trigger instead_of_update_tg
create trigger instead_of_update_tg
instead of update
on %1$s_rv
for each row

View File

@ -1,7 +1,7 @@
--liquibase formatted sql
-- ============================================================================
--changeset michael.hoennig:rbac-global-OBJECT runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset michael.hoennig:rbac-global-OBJECT endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
The purpose of this table is provide root business objects
@ -11,12 +11,12 @@
In production databases, there is only a single row in this table,
in test stages, there can be one row for each test data realm.
*/
create table if not exists rbac.global
create table rbac.global
(
uuid uuid primary key references rbac.object (uuid) on delete cascade,
name varchar(63) unique
);
create unique index if not exists Global_Singleton on rbac.global ((0));
create unique index Global_Singleton on rbac.global ((0));
grant select on rbac.global to ${HSADMINNG_POSTGRES_RESTRICTED_USERNAME};
--//
@ -75,12 +75,13 @@ $$;
-- ============================================================================
--changeset michael.hoennig:rbac-global-IDENTITY-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset michael.hoennig:rbac-global-IDENTITY-VIEW endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Creates a view to the rbac.global object table which maps the identifying name to the objectUuid.
*/
drop view if exists rbac.global_iv;
create or replace view rbac.global_iv as
select target.uuid, target.name as idName
from rbac.global as target;

View File

@ -17,7 +17,7 @@ call rbac.generateRbacRoleDescriptors('rbactest.customer');
-- ============================================================================
--changeset RolesGrantsAndPermissionsGenerator:rbactest-customer-rbac-insert-trigger runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RolesGrantsAndPermissionsGenerator:rbactest-customer-rbac-insert-trigger endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
@ -69,7 +69,7 @@ begin
return NEW;
end; $$;
create or replace trigger build_rbac_system_after_insert_tg
create trigger build_rbac_system_after_insert_tg
after insert on rbactest.customer
for each row
execute procedure rbactest.customer_build_rbac_system_after_insert_tf();
@ -165,7 +165,7 @@ call rbac.generateRbacIdentityViewFromProjection('rbactest.customer',
-- ============================================================================
--changeset RbacRestrictedViewGenerator:rbactest-customer-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRestrictedViewGenerator:rbactest-customer-rbac-RESTRICTED-VIEW endDelimiter:--//
-- ----------------------------------------------------------------------------
call rbac.generateRbacRestrictedView('rbactest.customer',
$orderBy$
@ -180,7 +180,7 @@ call rbac.generateRbacRestrictedView('rbactest.customer',
-- ============================================================================
--changeset RbacRbacSystemRebuildGenerator:rbactest-customer-rbac-rebuild runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRbacSystemRebuildGenerator:rbactest-customer-rbac-rebuild endDelimiter:--//
-- ----------------------------------------------------------------------------
-- HOWTO: Rebuild RBAC-system for table rbactest.customer after changing its RBAC specification.

View File

@ -17,7 +17,7 @@ call rbac.generateRbacRoleDescriptors('rbactest.package');
-- ============================================================================
--changeset RolesGrantsAndPermissionsGenerator:rbactest-package-rbac-insert-trigger runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RolesGrantsAndPermissionsGenerator:rbactest-package-rbac-insert-trigger endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
@ -73,7 +73,7 @@ begin
return NEW;
end; $$;
create or replace trigger build_rbac_system_after_insert_tg
create trigger build_rbac_system_after_insert_tg
after insert on rbactest.package
for each row
execute procedure rbactest.package_build_rbac_system_after_insert_tf();
@ -81,7 +81,7 @@ execute procedure rbactest.package_build_rbac_system_after_insert_tf();
-- ============================================================================
--changeset RolesGrantsAndPermissionsGenerator:rbactest-package-rbac-update-trigger runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RolesGrantsAndPermissionsGenerator:rbactest-package-rbac-update-trigger endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
@ -134,7 +134,7 @@ begin
return NEW;
end; $$;
create or replace trigger update_rbac_system_after_update_tg
create trigger update_rbac_system_after_update_tg
after update on rbactest.package
for each row
execute procedure rbactest.package_update_rbac_system_after_update_tf();
@ -230,7 +230,7 @@ call rbac.generateRbacIdentityViewFromProjection('rbactest.package',
-- ============================================================================
--changeset RbacRestrictedViewGenerator:rbactest-package-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRestrictedViewGenerator:rbactest-package-rbac-RESTRICTED-VIEW endDelimiter:--//
-- ----------------------------------------------------------------------------
call rbac.generateRbacRestrictedView('rbactest.package',
$orderBy$
@ -245,7 +245,7 @@ call rbac.generateRbacRestrictedView('rbactest.package',
-- ============================================================================
--changeset RbacRbacSystemRebuildGenerator:rbactest-package-rbac-rebuild runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRbacSystemRebuildGenerator:rbactest-package-rbac-rebuild endDelimiter:--//
-- ----------------------------------------------------------------------------
-- HOWTO: Rebuild RBAC-system for table rbactest.package after changing its RBAC specification.

View File

@ -17,7 +17,7 @@ call rbac.generateRbacRoleDescriptors('rbactest.domain');
-- ============================================================================
--changeset RolesGrantsAndPermissionsGenerator:rbactest-domain-rbac-insert-trigger runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RolesGrantsAndPermissionsGenerator:rbactest-domain-rbac-insert-trigger endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
@ -69,7 +69,7 @@ begin
return NEW;
end; $$;
create or replace trigger build_rbac_system_after_insert_tg
create trigger build_rbac_system_after_insert_tg
after insert on rbactest.domain
for each row
execute procedure rbactest.domain_build_rbac_system_after_insert_tf();
@ -77,7 +77,7 @@ execute procedure rbactest.domain_build_rbac_system_after_insert_tf();
-- ============================================================================
--changeset RolesGrantsAndPermissionsGenerator:rbactest-domain-rbac-update-trigger runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RolesGrantsAndPermissionsGenerator:rbactest-domain-rbac-update-trigger endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
@ -133,7 +133,7 @@ begin
return NEW;
end; $$;
create or replace trigger update_rbac_system_after_update_tg
create trigger update_rbac_system_after_update_tg
after update on rbactest.domain
for each row
execute procedure rbactest.domain_update_rbac_system_after_update_tf();
@ -229,7 +229,7 @@ call rbac.generateRbacIdentityViewFromProjection('rbactest.domain',
-- ============================================================================
--changeset RbacRestrictedViewGenerator:rbactest-domain-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRestrictedViewGenerator:rbactest-domain-rbac-RESTRICTED-VIEW endDelimiter:--//
-- ----------------------------------------------------------------------------
call rbac.generateRbacRestrictedView('rbactest.domain',
$orderBy$
@ -244,7 +244,7 @@ call rbac.generateRbacRestrictedView('rbactest.domain',
-- ============================================================================
--changeset RbacRbacSystemRebuildGenerator:rbactest-domain-rbac-rebuild runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRbacSystemRebuildGenerator:rbactest-domain-rbac-rebuild endDelimiter:--//
-- ----------------------------------------------------------------------------
-- HOWTO: Rebuild RBAC-system for table rbactest.domain after changing its RBAC specification.

View File

@ -17,7 +17,7 @@ call rbac.generateRbacRoleDescriptors('hs_office.contact');
-- ============================================================================
--changeset RolesGrantsAndPermissionsGenerator:hs-office-contact-rbac-insert-trigger runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RolesGrantsAndPermissionsGenerator:hs-office-contact-rbac-insert-trigger endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
@ -69,7 +69,7 @@ begin
return NEW;
end; $$;
create or replace trigger build_rbac_system_after_insert_tg
create trigger build_rbac_system_after_insert_tg
after insert on hs_office.contact
for each row
execute procedure hs_office.contact_build_rbac_system_after_insert_tf();
@ -88,7 +88,7 @@ call rbac.generateRbacIdentityViewFromProjection('hs_office.contact',
-- ============================================================================
--changeset RbacRestrictedViewGenerator:hs-office-contact-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRestrictedViewGenerator:hs-office-contact-rbac-RESTRICTED-VIEW endDelimiter:--//
-- ----------------------------------------------------------------------------
call rbac.generateRbacRestrictedView('hs_office.contact',
$orderBy$
@ -104,7 +104,7 @@ call rbac.generateRbacRestrictedView('hs_office.contact',
-- ============================================================================
--changeset RbacRbacSystemRebuildGenerator:hs-office-contact-rbac-rebuild runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRbacSystemRebuildGenerator:hs-office-contact-rbac-rebuild endDelimiter:--//
-- ----------------------------------------------------------------------------
-- HOWTO: Rebuild RBAC-system for table hs_office.contact after changing its RBAC specification.

View File

@ -17,7 +17,7 @@ call rbac.generateRbacRoleDescriptors('hs_office.person');
-- ============================================================================
--changeset RolesGrantsAndPermissionsGenerator:hs-office-person-rbac-insert-trigger runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RolesGrantsAndPermissionsGenerator:hs-office-person-rbac-insert-trigger endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
@ -69,7 +69,7 @@ begin
return NEW;
end; $$;
create or replace trigger build_rbac_system_after_insert_tg
create trigger build_rbac_system_after_insert_tg
after insert on hs_office.person
for each row
execute procedure hs_office.person_build_rbac_system_after_insert_tf();
@ -88,7 +88,7 @@ call rbac.generateRbacIdentityViewFromProjection('hs_office.person',
-- ============================================================================
--changeset RbacRestrictedViewGenerator:hs-office-person-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRestrictedViewGenerator:hs-office-person-rbac-RESTRICTED-VIEW endDelimiter:--//
-- ----------------------------------------------------------------------------
call rbac.generateRbacRestrictedView('hs_office.person',
$orderBy$
@ -106,7 +106,7 @@ call rbac.generateRbacRestrictedView('hs_office.person',
-- ============================================================================
--changeset RbacRbacSystemRebuildGenerator:hs-office-person-rbac-rebuild runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRbacSystemRebuildGenerator:hs-office-person-rbac-rebuild endDelimiter:--//
-- ----------------------------------------------------------------------------
-- HOWTO: Rebuild RBAC-system for table hs_office.person after changing its RBAC specification.

View File

@ -17,7 +17,7 @@ call rbac.generateRbacRoleDescriptors('hs_office.relation');
-- ============================================================================
--changeset RolesGrantsAndPermissionsGenerator:hs-office-relation-rbac-insert-trigger runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RolesGrantsAndPermissionsGenerator:hs-office-relation-rbac-insert-trigger endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
@ -102,7 +102,7 @@ begin
return NEW;
end; $$;
create or replace trigger build_rbac_system_after_insert_tg
create trigger build_rbac_system_after_insert_tg
after insert on hs_office.relation
for each row
execute procedure hs_office.relation_build_rbac_system_after_insert_tf();
@ -110,7 +110,7 @@ execute procedure hs_office.relation_build_rbac_system_after_insert_tf();
-- ============================================================================
--changeset RolesGrantsAndPermissionsGenerator:hs-office-relation-rbac-update-trigger runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RolesGrantsAndPermissionsGenerator:hs-office-relation-rbac-update-trigger endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
@ -124,9 +124,7 @@ create or replace procedure hs_office.relation_update_rbac_system(
language plpgsql as $$
begin
if NEW.holderUuid is distinct from OLD.holderUuid
or NEW.anchorUuid is distinct from OLD.anchorUuid
or NEW.contactUuid is distinct from OLD.contactUuid then
if NEW.contactUuid is distinct from OLD.contactUuid then
delete from rbac.grant g where g.grantedbytriggerof = OLD.uuid;
call hs_office.relation_build_rbac_system(NEW);
end if;
@ -145,7 +143,7 @@ begin
return NEW;
end; $$;
create or replace trigger update_rbac_system_after_update_tg
create trigger update_rbac_system_after_update_tg
after update on hs_office.relation
for each row
execute procedure hs_office.relation_update_rbac_system_after_update_tf();
@ -243,22 +241,20 @@ call rbac.generateRbacIdentityViewFromProjection('hs_office.relation',
-- ============================================================================
--changeset RbacRestrictedViewGenerator:hs-office-relation-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRestrictedViewGenerator:hs-office-relation-rbac-RESTRICTED-VIEW endDelimiter:--//
-- ----------------------------------------------------------------------------
call rbac.generateRbacRestrictedView('hs_office.relation',
$orderBy$
(select idName from hs_office.person_iv p where p.uuid = target.holderUuid)
$orderBy$,
$updates$
anchorUuid = new.anchorUuid,
holderUuid = new.holderUuid,
contactUuid = new.contactUuid
$updates$);
--//
-- ============================================================================
--changeset RbacRbacSystemRebuildGenerator:hs-office-relation-rbac-rebuild runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRbacSystemRebuildGenerator:hs-office-relation-rbac-rebuild endDelimiter:--//
-- ----------------------------------------------------------------------------
-- HOWTO: Rebuild RBAC-system for table hs_office.relation after changing its RBAC specification.

View File

@ -17,7 +17,7 @@ call rbac.generateRbacRoleDescriptors('hs_office.partner');
-- ============================================================================
--changeset RolesGrantsAndPermissionsGenerator:hs-office-partner-rbac-insert-trigger runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RolesGrantsAndPermissionsGenerator:hs-office-partner-rbac-insert-trigger endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
@ -65,7 +65,7 @@ begin
return NEW;
end; $$;
create or replace trigger build_rbac_system_after_insert_tg
create trigger build_rbac_system_after_insert_tg
after insert on hs_office.partner
for each row
execute procedure hs_office.partner_build_rbac_system_after_insert_tf();
@ -73,7 +73,7 @@ execute procedure hs_office.partner_build_rbac_system_after_insert_tf();
-- ============================================================================
--changeset RolesGrantsAndPermissionsGenerator:hs-office-partner-rbac-update-trigger runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RolesGrantsAndPermissionsGenerator:hs-office-partner-rbac-update-trigger endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
@ -146,7 +146,7 @@ begin
return NEW;
end; $$;
create or replace trigger update_rbac_system_after_update_tg
create trigger update_rbac_system_after_update_tg
after update on hs_office.partner
for each row
execute procedure hs_office.partner_update_rbac_system_after_update_tf();
@ -242,7 +242,7 @@ call rbac.generateRbacIdentityViewFromProjection('hs_office.partner',
-- ============================================================================
--changeset RbacRestrictedViewGenerator:hs-office-partner-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRestrictedViewGenerator:hs-office-partner-rbac-RESTRICTED-VIEW endDelimiter:--//
-- ----------------------------------------------------------------------------
call rbac.generateRbacRestrictedView('hs_office.partner',
$orderBy$
@ -255,7 +255,7 @@ call rbac.generateRbacRestrictedView('hs_office.partner',
-- ============================================================================
--changeset RbacRbacSystemRebuildGenerator:hs-office-partner-rbac-rebuild runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRbacSystemRebuildGenerator:hs-office-partner-rbac-rebuild endDelimiter:--//
-- ----------------------------------------------------------------------------
-- HOWTO: Rebuild RBAC-system for table hs_office.partner after changing its RBAC specification.

View File

@ -17,7 +17,7 @@ call rbac.generateRbacRoleDescriptors('hs_office.partner_details');
-- ============================================================================
--changeset RolesGrantsAndPermissionsGenerator:hs-office-partner-details-rbac-insert-trigger runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RolesGrantsAndPermissionsGenerator:hs-office-partner-details-rbac-insert-trigger endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
@ -50,7 +50,7 @@ begin
return NEW;
end; $$;
create or replace trigger build_rbac_system_after_insert_tg
create trigger build_rbac_system_after_insert_tg
after insert on hs_office.partner_details
for each row
execute procedure hs_office.partner_details_build_rbac_system_after_insert_tf();
@ -149,7 +149,7 @@ call rbac.generateRbacIdentityViewFromQuery('hs_office.partner_details',
-- ============================================================================
--changeset RbacRestrictedViewGenerator:hs-office-partner-details-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRestrictedViewGenerator:hs-office-partner-details-rbac-RESTRICTED-VIEW endDelimiter:--//
-- ----------------------------------------------------------------------------
call rbac.generateRbacRestrictedView('hs_office.partner_details',
$orderBy$
@ -167,7 +167,7 @@ call rbac.generateRbacRestrictedView('hs_office.partner_details',
-- ============================================================================
--changeset RbacRbacSystemRebuildGenerator:hs-office-partner-details-rbac-rebuild runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRbacSystemRebuildGenerator:hs-office-partner-details-rbac-rebuild endDelimiter:--//
-- ----------------------------------------------------------------------------
-- HOWTO: Rebuild RBAC-system for table hs_office.partner_details after changing its RBAC specification.

View File

@ -17,7 +17,7 @@ call rbac.generateRbacRoleDescriptors('hs_office.bankaccount');
-- ============================================================================
--changeset RolesGrantsAndPermissionsGenerator:hs-office-bankaccount-rbac-insert-trigger runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RolesGrantsAndPermissionsGenerator:hs-office-bankaccount-rbac-insert-trigger endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
@ -69,7 +69,7 @@ begin
return NEW;
end; $$;
create or replace trigger build_rbac_system_after_insert_tg
create trigger build_rbac_system_after_insert_tg
after insert on hs_office.bankaccount
for each row
execute procedure hs_office.bankaccount_build_rbac_system_after_insert_tf();
@ -88,7 +88,7 @@ call rbac.generateRbacIdentityViewFromProjection('hs_office.bankaccount',
-- ============================================================================
--changeset RbacRestrictedViewGenerator:hs-office-bankaccount-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRestrictedViewGenerator:hs-office-bankaccount-rbac-RESTRICTED-VIEW endDelimiter:--//
-- ----------------------------------------------------------------------------
call rbac.generateRbacRestrictedView('hs_office.bankaccount',
$orderBy$
@ -103,7 +103,7 @@ call rbac.generateRbacRestrictedView('hs_office.bankaccount',
-- ============================================================================
--changeset RbacRbacSystemRebuildGenerator:hs-office-bankaccount-rbac-rebuild runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRbacSystemRebuildGenerator:hs-office-bankaccount-rbac-rebuild endDelimiter:--//
-- ----------------------------------------------------------------------------
-- HOWTO: Rebuild RBAC-system for table hs_office.bankaccount after changing its RBAC specification.

View File

@ -17,7 +17,7 @@ call rbac.generateRbacRoleDescriptors('hs_office.debitor');
-- ============================================================================
--changeset RolesGrantsAndPermissionsGenerator:hs-office-debitor-rbac-insert-trigger runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RolesGrantsAndPermissionsGenerator:hs-office-debitor-rbac-insert-trigger endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
@ -77,7 +77,7 @@ begin
return NEW;
end; $$;
create or replace trigger build_rbac_system_after_insert_tg
create trigger build_rbac_system_after_insert_tg
after insert on hs_office.debitor
for each row
execute procedure hs_office.debitor_build_rbac_system_after_insert_tf();
@ -85,7 +85,7 @@ execute procedure hs_office.debitor_build_rbac_system_after_insert_tf();
-- ============================================================================
--changeset RolesGrantsAndPermissionsGenerator:hs-office-debitor-rbac-update-trigger runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RolesGrantsAndPermissionsGenerator:hs-office-debitor-rbac-update-trigger endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
@ -119,7 +119,7 @@ begin
return NEW;
end; $$;
create or replace trigger update_rbac_system_after_update_tg
create trigger update_rbac_system_after_update_tg
after update on hs_office.debitor
for each row
execute procedure hs_office.debitor_update_rbac_system_after_update_tf();
@ -224,7 +224,7 @@ call rbac.generateRbacIdentityViewFromQuery('hs_office.debitor',
-- ============================================================================
--changeset RbacRestrictedViewGenerator:hs-office-debitor-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRestrictedViewGenerator:hs-office-debitor-rbac-RESTRICTED-VIEW endDelimiter:--//
-- ----------------------------------------------------------------------------
call rbac.generateRbacRestrictedView('hs_office.debitor',
$orderBy$
@ -244,7 +244,7 @@ call rbac.generateRbacRestrictedView('hs_office.debitor',
-- ============================================================================
--changeset RbacRbacSystemRebuildGenerator:hs-office-debitor-rbac-rebuild runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRbacSystemRebuildGenerator:hs-office-debitor-rbac-rebuild endDelimiter:--//
-- ----------------------------------------------------------------------------
-- HOWTO: Rebuild RBAC-system for table hs_office.debitor after changing its RBAC specification.

View File

@ -17,7 +17,7 @@ call rbac.generateRbacRoleDescriptors('hs_office.sepamandate');
-- ============================================================================
--changeset RolesGrantsAndPermissionsGenerator:hs-office-sepamandate-rbac-insert-trigger runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RolesGrantsAndPermissionsGenerator:hs-office-sepamandate-rbac-insert-trigger endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
@ -94,7 +94,7 @@ begin
return NEW;
end; $$;
create or replace trigger build_rbac_system_after_insert_tg
create trigger build_rbac_system_after_insert_tg
after insert on hs_office.sepamandate
for each row
execute procedure hs_office.sepamandate_build_rbac_system_after_insert_tf();
@ -198,7 +198,7 @@ call rbac.generateRbacIdentityViewFromQuery('hs_office.sepamandate',
-- ============================================================================
--changeset RbacRestrictedViewGenerator:hs-office-sepamandate-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRestrictedViewGenerator:hs-office-sepamandate-rbac-RESTRICTED-VIEW endDelimiter:--//
-- ----------------------------------------------------------------------------
call rbac.generateRbacRestrictedView('hs_office.sepamandate',
$orderBy$
@ -213,7 +213,7 @@ call rbac.generateRbacRestrictedView('hs_office.sepamandate',
-- ============================================================================
--changeset RbacRbacSystemRebuildGenerator:hs-office-sepamandate-rbac-rebuild runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRbacSystemRebuildGenerator:hs-office-sepamandate-rbac-rebuild endDelimiter:--//
-- ----------------------------------------------------------------------------
-- HOWTO: Rebuild RBAC-system for table hs_office.sepamandate after changing its RBAC specification.

View File

@ -32,41 +32,6 @@ create table if not exists hs_office.membership
--//
-- ============================================================================
--changeset michael.hoennig:hs-office-membership-SINGLE-MEMBERSHIP-CHECK endDelimiter:--//
-- ----------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION hs_office.validate_membership_validity()
RETURNS trigger AS $$
DECLARE
partnerNumber int;
BEGIN
IF EXISTS (
SELECT 1
FROM hs_office.membership
WHERE partnerUuid = NEW.partnerUuid
AND uuid <> NEW.uuid
AND NEW.validity && validity
) THEN
SELECT p.partnerNumber INTO partnerNumber
FROM hs_office.partner AS p
WHERE p.uuid = NEW.partnerUuid;
RAISE EXCEPTION 'Membership validity ranges overlap for partnerUuid %, partnerNumber %', NEW.partnerUuid, partnerNumber;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_validate_membership_validity
BEFORE INSERT OR UPDATE ON hs_office.membership
FOR EACH ROW
EXECUTE FUNCTION hs_office.validate_membership_validity();
--//
-- ============================================================================
--changeset michael.hoennig:hs-office-membership-MAIN-TABLE-JOURNAL endDelimiter:--//
-- ----------------------------------------------------------------------------

View File

@ -17,7 +17,7 @@ call rbac.generateRbacRoleDescriptors('hs_office.membership');
-- ============================================================================
--changeset RolesGrantsAndPermissionsGenerator:hs-office-membership-rbac-insert-trigger runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RolesGrantsAndPermissionsGenerator:hs-office-membership-rbac-insert-trigger endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
@ -81,7 +81,7 @@ begin
return NEW;
end; $$;
create or replace trigger build_rbac_system_after_insert_tg
create trigger build_rbac_system_after_insert_tg
after insert on hs_office.membership
for each row
execute procedure hs_office.membership_build_rbac_system_after_insert_tf();
@ -180,7 +180,7 @@ call rbac.generateRbacIdentityViewFromQuery('hs_office.membership',
-- ============================================================================
--changeset RbacRestrictedViewGenerator:hs-office-membership-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRestrictedViewGenerator:hs-office-membership-rbac-RESTRICTED-VIEW endDelimiter:--//
-- ----------------------------------------------------------------------------
call rbac.generateRbacRestrictedView('hs_office.membership',
$orderBy$
@ -195,7 +195,7 @@ call rbac.generateRbacRestrictedView('hs_office.membership',
-- ============================================================================
--changeset RbacRbacSystemRebuildGenerator:hs-office-membership-rbac-rebuild runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRbacSystemRebuildGenerator:hs-office-membership-rbac-rebuild endDelimiter:--//
-- ----------------------------------------------------------------------------
-- HOWTO: Rebuild RBAC-system for table hs_office.membership after changing its RBAC specification.

View File

@ -2,7 +2,7 @@
-- ============================================================================
--changeset michael.hoennig:hs-office-membership-TEST-DATA-GENERATOR runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset michael.hoennig:hs-office-membership-TEST-DATA-GENERATOR endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
@ -10,9 +10,7 @@
*/
create or replace procedure hs_office.membership_create_test_data(
forPartnerNumber numeric(5),
newMemberNumberSuffix char(2),
newValidity daterange,
newStatus hs_office.HsOfficeMembershipStatus)
newMemberNumberSuffix char(2) )
language plpgsql as $$
declare
relatedPartner hs_office.partner;
@ -22,35 +20,24 @@ begin
raise notice 'creating test Membership: M-% %', forPartnerNumber, newMemberNumberSuffix;
raise notice '- using partner (%): %', relatedPartner.uuid, relatedPartner;
if not exists (select true
from hs_office.membership
where partneruuid = relatedPartner.uuid and memberNumberSuffix = newMemberNumberSuffix)
then
insert into hs_office.membership (uuid, partneruuid, memberNumberSuffix, validity, status)
values (uuid_generate_v4(), relatedPartner.uuid, newMemberNumberSuffix,
newValidity, newStatus);
else
update hs_office.membership
set memberNumberSuffix = newMemberNumberSuffix,
validity = newValidity,
status = newStatus
where partneruuid = relatedPartner.uuid;
end if;
insert
into hs_office.membership (uuid, partneruuid, memberNumberSuffix, validity, status)
values (uuid_generate_v4(), relatedPartner.uuid, newMemberNumberSuffix, daterange('20221001' , null, '[]'), 'ACTIVE');
end; $$;
--//
-- ============================================================================
--changeset michael.hoennig:hs-office-membership-TEST-DATA-GENERATION runOnChange:true validCheckSum:ANY context:!without-test-data endDelimiter:--//
--changeset michael.hoennig:hs-office-membership-TEST-DATA-GENERATION context:!without-test-data endDelimiter:--//
-- ----------------------------------------------------------------------------
do language plpgsql $$
begin
call base.defineContext('creating Membership test-data', null, 'superuser-alex@hostsharing.net', 'rbac.global#global:ADMIN');
call hs_office.membership_create_test_data(10001, '01', daterange('20221001' , '20241231', '[)'), 'CANCELLED');
call hs_office.membership_create_test_data(10002, '02', daterange('20221001' , '20251231', '[]'), 'CANCELLED');
call hs_office.membership_create_test_data(10003, '03', daterange('20221001' , null, '[]'), 'ACTIVE');
call hs_office.membership_create_test_data(10001, '01');
call hs_office.membership_create_test_data(10002, '02');
call hs_office.membership_create_test_data(10003, '03');
end;
$$;
--//

View File

@ -17,7 +17,7 @@ call rbac.generateRbacRoleDescriptors('hs_office.coopsharetx');
-- ============================================================================
--changeset RolesGrantsAndPermissionsGenerator:hs-office-coopsharetx-rbac-insert-trigger runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RolesGrantsAndPermissionsGenerator:hs-office-coopsharetx-rbac-insert-trigger endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
@ -57,7 +57,7 @@ begin
return NEW;
end; $$;
create or replace trigger build_rbac_system_after_insert_tg
create trigger build_rbac_system_after_insert_tg
after insert on hs_office.coopsharetx
for each row
execute procedure hs_office.coopsharetx_build_rbac_system_after_insert_tf();
@ -153,7 +153,7 @@ call rbac.generateRbacIdentityViewFromProjection('hs_office.coopsharetx',
-- ============================================================================
--changeset RbacRestrictedViewGenerator:hs-office-coopsharetx-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRestrictedViewGenerator:hs-office-coopsharetx-rbac-RESTRICTED-VIEW endDelimiter:--//
-- ----------------------------------------------------------------------------
call rbac.generateRbacRestrictedView('hs_office.coopsharetx',
$orderBy$
@ -166,7 +166,7 @@ call rbac.generateRbacRestrictedView('hs_office.coopsharetx',
-- ============================================================================
--changeset RbacRbacSystemRebuildGenerator:hs-office-coopsharetx-rbac-rebuild runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRbacSystemRebuildGenerator:hs-office-coopsharetx-rbac-rebuild endDelimiter:--//
-- ----------------------------------------------------------------------------
-- HOWTO: Rebuild RBAC-system for table hs_office.coopsharetx after changing its RBAC specification.

View File

@ -17,7 +17,7 @@ call rbac.generateRbacRoleDescriptors('hs_office.coopassettx');
-- ============================================================================
--changeset RolesGrantsAndPermissionsGenerator:hs-office-coopassettx-rbac-insert-trigger runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RolesGrantsAndPermissionsGenerator:hs-office-coopassettx-rbac-insert-trigger endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
@ -57,7 +57,7 @@ begin
return NEW;
end; $$;
create or replace trigger build_rbac_system_after_insert_tg
create trigger build_rbac_system_after_insert_tg
after insert on hs_office.coopassettx
for each row
execute procedure hs_office.coopassettx_build_rbac_system_after_insert_tf();
@ -153,7 +153,7 @@ call rbac.generateRbacIdentityViewFromProjection('hs_office.coopassettx',
-- ============================================================================
--changeset RbacRestrictedViewGenerator:hs-office-coopassettx-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRestrictedViewGenerator:hs-office-coopassettx-rbac-RESTRICTED-VIEW endDelimiter:--//
-- ----------------------------------------------------------------------------
call rbac.generateRbacRestrictedView('hs_office.coopassettx',
$orderBy$
@ -166,7 +166,7 @@ call rbac.generateRbacRestrictedView('hs_office.coopassettx',
-- ============================================================================
--changeset RbacRbacSystemRebuildGenerator:hs-office-coopassettx-rbac-rebuild runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRbacSystemRebuildGenerator:hs-office-coopassettx-rbac-rebuild endDelimiter:--//
-- ----------------------------------------------------------------------------
-- HOWTO: Rebuild RBAC-system for table hs_office.coopassettx after changing its RBAC specification.

View File

@ -17,7 +17,7 @@ call rbac.generateRbacRoleDescriptors('hs_booking.project');
-- ============================================================================
--changeset RolesGrantsAndPermissionsGenerator:hs-booking-project-rbac-insert-trigger runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RolesGrantsAndPermissionsGenerator:hs-booking-project-rbac-insert-trigger endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
@ -88,7 +88,7 @@ begin
return NEW;
end; $$;
create or replace trigger build_rbac_system_after_insert_tg
create trigger build_rbac_system_after_insert_tg
after insert on hs_booking.project
for each row
execute procedure hs_booking.project_build_rbac_system_after_insert_tf();
@ -192,7 +192,7 @@ call rbac.generateRbacIdentityViewFromQuery('hs_booking.project',
-- ============================================================================
--changeset RbacRestrictedViewGenerator:hs-booking-project-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRestrictedViewGenerator:hs-booking-project-rbac-RESTRICTED-VIEW endDelimiter:--//
-- ----------------------------------------------------------------------------
call rbac.generateRbacRestrictedView('hs_booking.project',
$orderBy$
@ -206,7 +206,7 @@ call rbac.generateRbacRestrictedView('hs_booking.project',
-- ============================================================================
--changeset RbacRbacSystemRebuildGenerator:hs-booking-project-rbac-rebuild runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRbacSystemRebuildGenerator:hs-booking-project-rbac-rebuild endDelimiter:--//
-- ----------------------------------------------------------------------------
-- HOWTO: Rebuild RBAC-system for table hs_booking.project after changing its RBAC specification.

View File

@ -17,7 +17,7 @@ call rbac.generateRbacRoleDescriptors('hs_booking.item');
-- ============================================================================
--changeset RolesGrantsAndPermissionsGenerator:hs-booking-item-rbac-insert-trigger runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RolesGrantsAndPermissionsGenerator:hs-booking-item-rbac-insert-trigger endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
@ -87,7 +87,7 @@ begin
return NEW;
end; $$;
create or replace trigger build_rbac_system_after_insert_tg
create trigger build_rbac_system_after_insert_tg
after insert on hs_booking.item
for each row
execute procedure hs_booking.item_build_rbac_system_after_insert_tf();
@ -261,7 +261,7 @@ call rbac.generateRbacIdentityViewFromProjection('hs_booking.item',
-- ============================================================================
--changeset RbacRestrictedViewGenerator:hs-booking-item-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRestrictedViewGenerator:hs-booking-item-rbac-RESTRICTED-VIEW endDelimiter:--//
-- ----------------------------------------------------------------------------
call rbac.generateRbacRestrictedView('hs_booking.item',
$orderBy$
@ -277,7 +277,7 @@ call rbac.generateRbacRestrictedView('hs_booking.item',
-- ============================================================================
--changeset RbacRbacSystemRebuildGenerator:hs-booking-item-rbac-rebuild runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRbacSystemRebuildGenerator:hs-booking-item-rbac-rebuild endDelimiter:--//
-- ----------------------------------------------------------------------------
-- HOWTO: Rebuild RBAC-system for table hs_booking.item after changing its RBAC specification.

View File

@ -17,7 +17,7 @@ call rbac.generateRbacRoleDescriptors('hs_hosting.asset');
-- ============================================================================
--changeset RolesGrantsAndPermissionsGenerator:hs-hosting-asset-rbac-insert-trigger runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RolesGrantsAndPermissionsGenerator:hs-hosting-asset-rbac-insert-trigger endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
@ -105,7 +105,7 @@ begin
return NEW;
end; $$;
create or replace trigger build_rbac_system_after_insert_tg
create trigger build_rbac_system_after_insert_tg
after insert on hs_hosting.asset
for each row
execute procedure hs_hosting.asset_build_rbac_system_after_insert_tf();
@ -113,7 +113,7 @@ execute procedure hs_hosting.asset_build_rbac_system_after_insert_tf();
-- ============================================================================
--changeset RolesGrantsAndPermissionsGenerator:hs-hosting-asset-rbac-update-trigger runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RolesGrantsAndPermissionsGenerator:hs-hosting-asset-rbac-update-trigger endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
@ -147,7 +147,7 @@ begin
return NEW;
end; $$;
create or replace trigger update_rbac_system_after_update_tg
create trigger update_rbac_system_after_update_tg
after update on hs_hosting.asset
for each row
execute procedure hs_hosting.asset_update_rbac_system_after_update_tf();
@ -166,7 +166,7 @@ call rbac.generateRbacIdentityViewFromProjection('hs_hosting.asset',
-- ============================================================================
--changeset RbacRestrictedViewGenerator:hs-hosting-asset-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRestrictedViewGenerator:hs-hosting-asset-rbac-RESTRICTED-VIEW endDelimiter:--//
-- ----------------------------------------------------------------------------
call rbac.generateRbacRestrictedView('hs_hosting.asset',
$orderBy$
@ -183,7 +183,7 @@ call rbac.generateRbacRestrictedView('hs_hosting.asset',
-- ============================================================================
--changeset RbacRbacSystemRebuildGenerator:hs-hosting-asset-rbac-rebuild runOnChange:true validCheckSum:ANY endDelimiter:--//
--changeset RbacRbacSystemRebuildGenerator:hs-hosting-asset-rbac-rebuild endDelimiter:--//
-- ----------------------------------------------------------------------------
-- HOWTO: Rebuild RBAC-system for table hs_hosting.asset after changing its RBAC specification.

View File

@ -11,9 +11,7 @@ import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.ConditionEvents;
import com.tngtech.archunit.lang.SimpleConditionEvent;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.HsadminNgApplication;
import net.hostsharing.hsadminng.config.NoSecurityRequirement;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRbacEntity;
import net.hostsharing.hsadminng.rbac.context.ContextBasedTest;
@ -22,7 +20,6 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.repository.Repository;
import org.springframework.web.bind.annotation.RestController;
import jakarta.annotation.PostConstruct;
import jakarta.persistence.Table;
import java.lang.annotation.Annotation;
@ -354,15 +351,6 @@ public class ArchitectureTest {
static final ArchRule restControllerNaming =
classes().that().areAnnotatedWith(RestController.class).should().haveSimpleNameEndingWith("Controller");
@ArchTest
@SuppressWarnings("unused")
static final ArchRule restControllerSecurityRequirement =
// TODO.impl: seems that the Spring templates for the OpenAPI generator don't support this,
// thus we need this annotation to support Swagger UI authorization.
classes().that().areAnnotatedWith(RestController.class).should()
.beAnnotatedWith(SecurityRequirement.class).orShould()
.beAnnotatedWith(NoSecurityRequirement.class);
@ArchTest
@SuppressWarnings("unused")
static final ArchRule restControllerMethods = classes()
@ -432,7 +420,7 @@ public class ArchitectureTest {
if (isGeneratedSpringRepositoryMethod(item, method)) {
continue;
}
if (!method.getModifiers().contains(PUBLIC) || method.isAnnotatedWith(PostConstruct.class)) {
if (item.isAnnotatedWith(RestController.class) && !method.getModifiers().contains(PUBLIC)) {
continue;
}
final var message = String.format(

View File

@ -19,7 +19,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(properties = {"server.port=0", "hsadminng.cas.server=http://localhost:8088"})
@TestPropertySource(properties = {"server.port=0", "hsadminng.cas.server=http://localhost:8088/cas"})
@ActiveProfiles("wiremock") // IMPORTANT: To test prod config, do not use test profile!
@Tag("generalIntegrationTest")
class CasAuthenticationFilterIntegrationTest {
@ -37,10 +37,10 @@ class CasAuthenticationFilterIntegrationTest {
private WireMockServer wireMockServer;
@Test
public void shouldAcceptRequestWithValidCasTicket() {
public void shouldAcceptRequest() {
// given
final var username = "test-user-" + randomAlphanumeric(4);
wireMockServer.stubFor(get(urlEqualTo("/cas/p3/serviceValidate?service=" + serviceUrl + "&ticket=ST-valid"))
wireMockServer.stubFor(get(urlEqualTo("/cas/p3/serviceValidate?service=" + serviceUrl + "&ticket=valid"))
.willReturn(aResponse()
.withStatus(200)
.withBody("""
@ -56,7 +56,7 @@ class CasAuthenticationFilterIntegrationTest {
final var result = restTemplate.exchange(
"http://localhost:" + this.serverPort + "/api/ping",
HttpMethod.GET,
new HttpEntity<>(null, headers("Authorization", "ST-valid")),
new HttpEntity<>(null, headers("Authorization", "valid")),
String.class
);
@ -66,7 +66,7 @@ class CasAuthenticationFilterIntegrationTest {
}
@Test
public void shouldRejectRequestWithInvalidCasTicket() {
public void shouldRejectRequest() {
// given
wireMockServer.stubFor(get(urlEqualTo("/cas/p3/serviceValidate?service=" + serviceUrl + "&ticket=invalid"))
.willReturn(aResponse()

View File

@ -10,15 +10,14 @@ import static org.mockito.Mockito.mock;
class CasAuthenticatorUnitTest {
final RealCasAuthenticator casAuthenticator = new RealCasAuthenticator();
final CasAuthenticator casAuthenticator = new CasAuthenticator();
@Test
void bypassesAuthenticationIfNoCasServerIsConfigured() {
// given
final var request = mock(HttpServletRequest.class);
// bypassing the CAS-server HTTP-request fakes the user from the authorization header's fake CAS-ticket
given(request.getHeader("authorization")).willReturn("Bearer given-user");
given(request.getHeader("current-subject")).willReturn("given-user");
// when
final var userName = casAuthenticator.authenticate(request);

View File

@ -21,7 +21,7 @@ public class DisableSecurityConfig {
@Bean
@Profile("test")
public CasAuthenticator fakeAuthenticator() {
return new FakeCasAuthenticator();
public Authenticator fakeAuthenticator() {
return new FakeAuthenticator();
}
}

View File

@ -4,7 +4,7 @@ import lombok.SneakyThrows;
import jakarta.servlet.http.HttpServletRequest;
public class FakeCasAuthenticator implements CasAuthenticator {
public class FakeAuthenticator implements Authenticator {
@Override
@SneakyThrows

View File

@ -3,7 +3,6 @@ package net.hostsharing.hsadminng.config;
import java.util.Map;
import com.github.tomakehurst.wiremock.WireMockServer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@ -19,16 +18,12 @@ import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl;
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.post;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static java.util.Map.entry;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(properties = {"management.port=0", "server.port=0", "hsadminng.cas.server=http://localhost:8088"})
@TestPropertySource(properties = {"management.port=0", "server.port=0", "hsadminng.cas.server=http://localhost:8088/cas"})
@ActiveProfiles("wiremock") // IMPORTANT: To test prod config, do not use test profile!
@Tag("generalIntegrationTest")
class WebSecurityConfigIntegrationTest {
@ -48,151 +43,71 @@ class WebSecurityConfigIntegrationTest {
@Autowired
private WireMockServer wireMockServer;
@BeforeEach
void setUp() {
wireMockServer.stubFor(get(anyUrl())
@Test
public void shouldSupportPingEndpoint() {
// given
wireMockServer.stubFor(get(urlEqualTo("/cas/p3/serviceValidate?service=" + serviceUrl + "&ticket=test-user"))
.willReturn(aResponse()
.withStatus(200)
.withBody("""
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
<cas:authenticationFailure/>
<cas:authenticationSuccess>
<cas:user>test-user</cas:user>
</cas:authenticationSuccess>
</cas:serviceResponse>
""")));
}
@Test
void accessToApiWithValidServiceTicketSouldBePermitted() {
// given
givenCasTicketValidationResponse("ST-fake-cas-ticket", "fake-user-name");
// fake Authorization header
final var headers = new HttpHeaders();
headers.set("Authorization", "test-user");
// http request
final var result = restTemplate.exchange(
"http://localhost:" + this.serverPort + "/api/ping",
HttpMethod.GET,
httpHeaders(entry("Authorization", "Bearer ST-fake-cas-ticket")),
new HttpEntity<>(null, headers),
String.class
);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).startsWith("pong fake-user-name");
assertThat(result.getBody()).startsWith("pong test-user");
}
@Test
void accessToApiWithValidTicketGrantingTicketShouldBePermitted() {
// given
givenCasServiceTicketForTicketGrantingTicket("TGT-fake-cas-ticket", "ST-fake-cas-ticket");
givenCasTicketValidationResponse("ST-fake-cas-ticket", "fake-user-name");
// http request
final var result = restTemplate.exchange(
"http://localhost:" + this.serverPort + "/api/ping",
HttpMethod.GET,
httpHeaders(entry("Authorization", "Bearer TGT-fake-cas-ticket")),
String.class
);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).startsWith("pong fake-user-name");
}
@Test
void accessToApiWithInvalidTicketGrantingTicketShouldBePermitted() {
// given
givenCasServiceTicketForTicketGrantingTicket("TGT-fake-cas-ticket", "ST-fake-cas-ticket");
givenCasTicketValidationResponse("ST-fake-cas-ticket", "fake-user-name");
// http request
final var result = restTemplate.exchange(
"http://localhost:" + this.serverPort + "/api/ping",
HttpMethod.GET,
httpHeaders(entry("Authorization", "Bearer TGT-WRONG-cas-ticket")),
String.class
);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
void accessToApiWithoutTokenShouldBeDenied() {
final var result = this.restTemplate.getForEntity(
"http://localhost:" + this.serverPort + "/api/ping", String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
void accessToApiWithInvalidTokenShouldBeDenied() {
// given
givenCasTicketValidationResponse("ST-fake-cas-ticket", "fake-user-name");
// when
final var result = restTemplate.exchange(
"http://localhost:" + this.serverPort + "/api/ping",
HttpMethod.GET,
httpHeaders(entry("Authorization", "Bearer ST-WRONG-cas-ticket")),
String.class
);
// then
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
void accessToActuatorShouldBePermitted() {
public void shouldSupportActuatorEndpoint() {
final var result = this.restTemplate.getForEntity(
"http://localhost:" + this.managementPort + "/actuator", Map.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
void accessToSwaggerUiShouldBePermitted() {
public void shouldSupportSwaggerUi() {
final var result = this.restTemplate.getForEntity(
"http://localhost:" + this.serverPort + "/swagger-ui/index.html", String.class);
"http://localhost:" + this.managementPort + "/actuator/swagger-ui/index.html", String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
void accessToApiDocsEndpointShouldBePermitted() {
public void shouldSupportApiDocs() {
final var result = this.restTemplate.getForEntity(
"http://localhost:" + this.serverPort + "/v3/api-docs/swagger-config", String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).contains("\"configUrl\":\"/v3/api-docs/swagger-config\"");
"http://localhost:" + this.managementPort + "/actuator/v3/api-docs/swagger-config", String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); // permitted but not configured
}
@Test
void accessToActuatorEndpointShouldBePermitted() {
public void shouldSupportHealthEndpoint() {
final var result = this.restTemplate.getForEntity(
"http://localhost:" + this.managementPort + "/actuator/health", Map.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody().get("status")).isEqualTo("UP");
}
private void givenCasServiceTicketForTicketGrantingTicket(final String ticketGrantingTicket, final String serviceTicket) {
wireMockServer.stubFor(post(urlEqualTo("/cas/v1/tickets/" + ticketGrantingTicket))
.withFormParam("service", equalTo(serviceUrl))
.willReturn(aResponse()
.withStatus(201)
.withBody(serviceTicket)));
@Test
public void shouldSupportMetricsEndpoint() {
final var result = this.restTemplate.getForEntity(
"http://localhost:" + this.managementPort + "/actuator/metrics", Map.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
}
private void givenCasTicketValidationResponse(final String casToken, final String userName) {
wireMockServer.stubFor(get(urlEqualTo("/cas/p3/serviceValidate?service=" + serviceUrl + "&ticket=" + casToken))
.willReturn(aResponse()
.withStatus(200)
.withBody("""
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
<cas:authenticationSuccess>
<cas:user>${userName}</cas:user>
</cas:authenticationSuccess>
</cas:serviceResponse>
""".replace("${userName}", userName))));
}
@SafeVarargs
private HttpEntity<?> httpHeaders(final Map.Entry<String, String>... headerValues) {
final var headers = new HttpHeaders();
for ( Map.Entry<String, String> headerValue: headerValues ) {
headers.add(headerValue.getKey(), headerValue.getValue());
}
return new HttpEntity<>(headers);
}
}

View File

@ -16,9 +16,6 @@ import org.junit.jupiter.api.extension.TestWatcher;
import org.opentest4j.AssertionFailedError;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.AbstractResource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.core.io.Resource;
import org.springframework.transaction.support.TransactionTemplate;
@ -29,7 +26,6 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ValidationException;
import jakarta.validation.constraints.NotNull;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
@ -122,16 +118,10 @@ public class CsvDataImport extends ContextBasedTest {
return stream(lines.getFirst()).map(String::trim).toArray(String[]::new);
}
public static @NotNull AbstractResource resourceOf(final String sqlFile) {
return new File(sqlFile).exists()
? new FileSystemResource(sqlFile)
: new ClassPathResource(sqlFile);
}
protected Reader resourceReader(@NotNull final String resourcePath) {
try {
return new InputStreamReader(requireNonNull(resourceOf(resourcePath).getInputStream()));
} catch (final Exception exc) {
return new InputStreamReader(requireNonNull(getClass().getClassLoader().getResourceAsStream(resourcePath)));
} catch (Exception exc) {
throw new AssertionFailedError("cannot open '" + resourcePath + "'");
}
}

View File

@ -12,7 +12,6 @@ import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidatorRegistry;
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProject;
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
@ -30,18 +29,14 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.orm.jpa.EntityManagerFactoryInfo;
import org.springframework.test.annotation.Commit;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.net.IDN;
import java.util.ArrayList;
@ -57,7 +52,6 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.stream;
import static java.util.Map.entry;
import static java.util.Map.ofEntries;
@ -86,7 +80,7 @@ import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assumptions.assumeThat;
import static org.springframework.util.FileCopyUtils.copyToByteArray;
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_CLASS;
@Tag("importHostingAssets")
@DataJpaTest(properties = {
@ -101,6 +95,7 @@ import static org.springframework.util.FileCopyUtils.copyToByteArray;
@ActiveProfiles({ "without-test-data", "liquibase-migration", "hosting-asset-import" })
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@ExtendWith(OrderedDependedTestsExtension.class)
@Sql(value = "/db/released-only-office-schema-with-import-test-data.sql", executionPhase = BEFORE_TEST_CLASS) // release-schema
public class ImportHostingAssets extends CsvDataImport {
private static final Set<String> NOBODY_SUBSTITUTES = Set.of("nomail", "bounce");
@ -138,14 +133,9 @@ public class ImportHostingAssets extends CsvDataImport {
@Autowired
LiquibaseMigration liquibase;
@Value("${HSADMINNG_OFFICE_DATA_SQL_FILE:/db/released-only-office-schema-with-import-test-data.sql}")
String officeSchemaAndDataSqlFile;
@Test
@Order(11000)
@SneakyThrows
void liquibaseMigrationForBookingAndHosting() {
executeSqlScript(officeSchemaAndDataSqlFile);
liquibase.assertReferenceStatusAfterRestore(286, "hs-booking-SCHEMA");
makeSureThatTheImportAdminUserExists();
liquibase.runWithContexts("migration", "without-test-data");
@ -156,8 +146,8 @@ public class ImportHostingAssets extends CsvDataImport {
@Order(11010)
void createBookingProjects() {
record PartnerLegacyIdMapping(UUID uuid, Integer bp_id) {}
record DebitorRecord(UUID uuid, Integer version, String defaultPrefix) {}
record PartnerLegacyIdMapping(UUID uuid, Integer bp_id){}
record DebitorRecord(UUID uuid, Integer version, String defaultPrefix){}
final var partnerLegacyIdMappings = em.createNativeQuery(
"""
@ -171,18 +161,16 @@ public class ImportHostingAssets extends CsvDataImport {
//noinspection unchecked
final var debitorUuidToLegacyBpIdMap = ((List<PartnerLegacyIdMapping>) partnerLegacyIdMappings).stream()
.collect(toMap(row -> row.uuid, row -> row.bp_id));
final var debitors = em.createNativeQuery(
"select debitor.uuid, debitor.version, debitor.defaultPrefix from hs_office.debitor debitor",
DebitorRecord.class).getResultList();
final var debitors = em.createNativeQuery("SELECT debitor.uuid, debitor.version, debitor.defaultPrefix FROM hs_office.debitor debitor", DebitorRecord.class).getResultList();
//noinspection unchecked
((List<DebitorRecord>) debitors).forEach(debitor -> {
bookingProjects.put(
debitorUuidToLegacyBpIdMap.get(debitor.uuid), HsBookingProjectRealEntity.builder()
.version(debitor.version)
.caption(debitor.defaultPrefix + " default project")
.debitor(em.find(HsBookingDebitorEntity.class, debitor.uuid))
.build());
});
((List<DebitorRecord>)debitors).forEach(debitor -> {
bookingProjects.put(
debitorUuidToLegacyBpIdMap.get(debitor.uuid), HsBookingProjectRealEntity.builder()
.version(debitor.version)
.caption(debitor.defaultPrefix + " default project")
.debitor(em.find(HsBookingDebitorEntity.class, debitor.uuid))
.build());
});
}
@Test
@ -1243,7 +1231,9 @@ public class ImportHostingAssets extends CsvDataImport {
bookingItems.put(packet_id, bookingItem);
final var haType = determineHaType(basepacket_code);
logError(() -> assertThat(!free || haType == MANAGED_WEBSPACE || defaultPrefix(bookingItem)
logError(() -> assertThat(!free || haType == MANAGED_WEBSPACE || bookingItem.getRelatedProject()
.getDebitor()
.getDefaultPrefix()
.equals("hsh"))
.as("packet.free only supported for Hostsharing-Assets and ManagedWebspace in customer-ManagedServer, but is set for "
+ packet_name)
@ -1298,13 +1288,6 @@ public class ImportHostingAssets extends CsvDataImport {
});
}
private String defaultPrefix(final HsBookingItem bookingItem) {
return ofNullable(bookingItem.getProject())
.map(HsBookingProject::getDebitor)
.map(HsBookingDebitorEntity::getDefaultPrefix)
.orElse("<no default prefix for BI: " + bookingItem.getCaption() + ">");
}
private void importPacketComponents(final String[] header, final List<String[]> records) {
final var columns = new Columns(header);
records.stream()
@ -1957,17 +1940,4 @@ public class ImportHostingAssets extends CsvDataImport {
.map(row -> row.stream().map(Object::toString).collect(joining(", ")))
.collect(joining("\n"));
}
@SneakyThrows
private void executeSqlScript(final String sqlFile) {
jpaAttempt.transacted(() -> {
try (InputStream resourceStream = resourceOf(sqlFile).getInputStream()) {
final var sqlScript = new String(copyToByteArray(resourceStream), UTF_8);
final var emf = (EntityManagerFactoryInfo) em.getEntityManagerFactory();
new JdbcTemplate(emf.getDataSource()).execute(sqlScript);
} catch (IOException e) {
throw new RuntimeException(e);
}
}).assertSuccessful();
}
}

View File

@ -37,7 +37,6 @@ import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TE
@Tag("officeIntegrationTest")
@DataJpaTest(properties = {
"spring.datasource.url=jdbc:tc:postgresql:15.5-bookworm:///liquibaseMigrationTestTC",
"hsadminng.superuser=${HSADMINNG_SUPERUSER:import-superuser@hostsharing.net}",
"spring.liquibase.enabled=false" // @Sql should go first, Liquibase will be initialized programmatically
})
@DirtiesContext

View File

@ -92,16 +92,7 @@ class HsOfficeCoopSharesTransactionControllerAcceptanceTest extends ContextBased
final var givenMembership = membershipRepo.findMembershipByMemberNumber(1000202).orElseThrow();
RestAssured // @formatter:off
.given()
.header("current-subject", "superuser-alex@hostsharing.net")
.port(port)
.when()
.get("http://localhost/api/hs/office/coopsharestransactions?membershipUuid=" + givenMembership.getUuid())
.then().log().all()
.assertThat()
.statusCode(200)
.contentType("application/json")
.body("", lenientlyEquals("""
.given().header("current-subject", "superuser-alex@hostsharing.net").port(port).when().get("http://localhost/api/hs/office/coopsharestransactions?membershipUuid=" + givenMembership.getUuid()).then().log().all().assertThat().statusCode(200).contentType("application/json").body("", lenientlyEquals("""
[
{
"transactionType": "SUBSCRIPTION",
@ -156,16 +147,8 @@ class HsOfficeCoopSharesTransactionControllerAcceptanceTest extends ContextBased
final var givenMembership = membershipRepo.findMembershipByMemberNumber(1000202).orElseThrow();
RestAssured // @formatter:off
.given()
.header("current-subject", "superuser-alex@hostsharing.net")
.port(port)
.when()
.get("http://localhost/api/hs/office/coopsharestransactions?membershipUuid=" + givenMembership.getUuid() + "&fromValueDate=2020-01-01&toValueDate=2021-12-31")
.then().log().all()
.assertThat()
.statusCode(200)
.contentType("application/json")
.body("", lenientlyEquals("""
.given().header("current-subject", "superuser-alex@hostsharing.net").port(port).when()
.get("http://localhost/api/hs/office/coopsharestransactions?membershipUuid=" + givenMembership.getUuid() + "&fromValueDate=2020-01-01&toValueDate=2021-12-31").then().log().all().assertThat().statusCode(200).contentType("application/json").body("", lenientlyEquals("""
[
{
"transactionType": "CANCELLATION",
@ -344,16 +327,7 @@ class HsOfficeCoopSharesTransactionControllerAcceptanceTest extends ContextBased
final var givenCoopShareTransactionUuid = coopSharesTransactionRepo.findCoopSharesTransactionByOptionalMembershipUuidAndDateRange(null, LocalDate.of(2010, 3, 15), LocalDate.of(2010, 3, 15)).get(0).getUuid();
RestAssured // @formatter:off
.given()
.header("current-subject", "superuser-alex@hostsharing.net")
.port(port)
.when()
.get("http://localhost/api/hs/office/coopsharestransactions/" + givenCoopShareTransactionUuid)
.then().log().body()
.assertThat()
.statusCode(200)
.contentType("application/json")
.body("", lenientlyEquals("""
.given().header("current-subject", "superuser-alex@hostsharing.net").port(port).when().get("http://localhost/api/hs/office/coopsharestransactions/" + givenCoopShareTransactionUuid).then().log().body().assertThat().statusCode(200).contentType("application/json").body("", lenientlyEquals("""
{
"transactionType": "SUBSCRIPTION"
}
@ -366,13 +340,7 @@ class HsOfficeCoopSharesTransactionControllerAcceptanceTest extends ContextBased
final var givenCoopShareTransactionUuid = coopSharesTransactionRepo.findCoopSharesTransactionByOptionalMembershipUuidAndDateRange(null, LocalDate.of(2010, 3, 15), LocalDate.of(2010, 3, 15)).get(0).getUuid();
RestAssured // @formatter:off
.given()
.header("current-subject", "selfregistered-user-drew@hostsharing.org")
.port(port)
.get("http://localhost/api/hs/office/coopsharestransactions/" + givenCoopShareTransactionUuid)
.then().log().body()
.assertThat()
.statusCode(404); // @formatter:on
.given().header("current-subject", "selfregistered-user-drew@hostsharing.org").port(port).when().get("http://localhost/api/hs/office/coopsharestransactions/" + givenCoopShareTransactionUuid).then().log().body().assertThat().statusCode(404); // @formatter:on
}
@Test

View File

@ -86,16 +86,16 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle
"memberNumber": "M-1000101",
"memberNumberSuffix": "01",
"validFrom": "2022-10-01",
"validTo": "2024-12-30",
"status": "CANCELLED"
"validTo": null,
"status": "ACTIVE"
},
{
"partner": { "partnerNumber": "P-10002" },
"memberNumber": "M-1000202",
"memberNumberSuffix": "02",
"validFrom": "2022-10-01",
"validTo": "2025-12-31",
"status": "CANCELLED"
"validTo": null,
"status": "ACTIVE"
},
{
"partner": { "partnerNumber": "P-10003" },
@ -133,8 +133,8 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle
"memberNumber": "M-1000101",
"memberNumberSuffix": "01",
"validFrom": "2022-10-01",
"validTo": "2024-12-30",
"status": "CANCELLED"
"validTo": null,
"status": "ACTIVE"
}
]
"""));
@ -161,8 +161,8 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle
"memberNumber": "M-1000202",
"memberNumberSuffix": "02",
"validFrom": "2022-10-01",
"validTo": "2025-12-31",
"status": "CANCELLED"
"validTo": null,
"status": "ACTIVE"
}
]
"""));
@ -177,7 +177,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle
void globalAdmin_canAddMembership() {
context.define("superuser-alex@hostsharing.net");
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("First").getFirst();
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("Third").get(0);
final var givenMemberSuffix = TEMP_MEMBER_NUMBER_SUFFIX;
final var expectedMemberNumber = Integer.parseInt(givenPartner.getPartnerNumber() + TEMP_MEMBER_NUMBER_SUFFIX);
@ -189,7 +189,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle
{
"partner.uuid": "%s",
"memberNumberSuffix": "%s",
"validFrom": "2025-02-13",
"validFrom": "2022-10-13",
"membershipFeeBillable": "true"
}
""".formatted(givenPartner.getUuid(), givenMemberSuffix))
@ -200,10 +200,10 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle
.statusCode(201)
.contentType(ContentType.JSON)
.body("uuid", isUuidValid())
.body("partner.partnerNumber", is("P-10001"))
.body("partner.partnerNumber", is("P-10003"))
.body("memberNumber", is("M-" + expectedMemberNumber))
.body("memberNumberSuffix", is(givenMemberSuffix))
.body("validFrom", is("2025-02-13"))
.body("validFrom", is("2022-10-13"))
.body("validTo", equalTo(null))
.header("Location", startsWith("http://localhost"))
.extract().header("Location"); // @formatter:on
@ -239,8 +239,8 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle
"memberNumber": "M-1000101",
"memberNumberSuffix": "01",
"validFrom": "2022-10-01",
"validTo": "2024-12-30",
"status": "CANCELLED"
"validTo": null,
"status": "ACTIVE"
}
""")); // @formatter:on
}
@ -297,13 +297,13 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle
context.define("superuser-alex@hostsharing.net");
final var givenMembership = givenSomeTemporaryMembershipBessler("First");
RestAssured // @formatter:off
final var location = RestAssured // @formatter:off
.given()
.header("current-subject", "superuser-alex@hostsharing.net")
.contentType(ContentType.JSON)
.body("""
{
"validTo": "2025-12-31",
"validTo": "2023-12-31",
"status": "CANCELLED"
}
""")
@ -316,8 +316,8 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle
.body("uuid", isUuidValid())
.body("partner.partnerNumber", is("P-" + givenMembership.getPartner().getPartnerNumber()))
.body("memberNumberSuffix", is(givenMembership.getMemberNumberSuffix()))
.body("validFrom", is("2025-02-01"))
.body("validTo", is("2025-12-31"))
.body("validFrom", is("2022-11-01"))
.body("validTo", is("2023-12-31"))
.body("status", is("CANCELLED"));
// @formatter:on
@ -326,7 +326,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle
.matches(mandate -> {
assertThat(mandate.getPartner().toShortString()).isEqualTo("P-10001");
assertThat(mandate.getMemberNumberSuffix()).isEqualTo(givenMembership.getMemberNumberSuffix());
assertThat(mandate.getValidity().asString()).isEqualTo("[2025-02-01,2026-01-01)");
assertThat(mandate.getValidity().asString()).isEqualTo("[2022-11-01,2024-01-01)");
assertThat(mandate.getStatus()).isEqualTo(CANCELLED);
return true;
});
@ -348,7 +348,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle
.contentType(ContentType.JSON)
.body("""
{
"validTo": "2025-12-31",
"validTo": "2024-01-01",
"status": "CANCELLED"
}
""")
@ -361,7 +361,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle
// finally, the Membership is actually updated
assertThat(membershipRepo.findByUuid(givenMembership.getUuid())).isPresent().get()
.matches(mandate -> {
assertThat(mandate.getValidity().asString()).isEqualTo("[2025-02-01,2026-01-01)");
assertThat(mandate.getValidity().asString()).isEqualTo("[2022-11-01,2024-01-02)");
assertThat(mandate.getStatus()).isEqualTo(CANCELLED);
return true;
});
@ -434,7 +434,7 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle
final var newMembership = HsOfficeMembershipEntity.builder()
.partner(givenPartner)
.memberNumberSuffix(TEMP_MEMBER_NUMBER_SUFFIX)
.validity(Range.closedInfinite(LocalDate.parse("2025-02-01")))
.validity(Range.closedInfinite(LocalDate.parse("2022-11-01")))
.status(ACTIVE)
.membershipFeeBillable(true)
.build();

View File

@ -4,20 +4,19 @@ import io.hypersistence.utils.hibernate.type.range.Range;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRealRepository;
import net.hostsharing.hsadminng.mapper.Array;
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
import net.hostsharing.hsadminng.rbac.grant.RawRbacGrantRepository;
import net.hostsharing.hsadminng.rbac.role.RawRbacRoleRepository;
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
import net.hostsharing.hsadminng.mapper.Array;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.postgresql.util.PSQLException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.context.annotation.Import;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
@ -32,7 +31,7 @@ import static net.hostsharing.hsadminng.rbac.test.JpaAttempt.attempt;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@Import({ Context.class, JpaAttempt.class })
@Import( { Context.class, JpaAttempt.class })
@Tag("officeIntegrationTest")
class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCleanup {
@ -71,16 +70,15 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("First").get(0);
// when
final var result = attempt(
em, () -> {
final var newMembership = HsOfficeMembershipEntity.builder()
.memberNumberSuffix("11")
.partner(givenPartner)
.validity(Range.closedInfinite(LocalDate.parse("2025-01-01")))
.membershipFeeBillable(true)
.build();
return toCleanup(membershipRepo.save(newMembership).load());
});
final var result = attempt(em, () -> {
final var newMembership = HsOfficeMembershipEntity.builder()
.memberNumberSuffix("11")
.partner(givenPartner)
.validity(Range.closedInfinite(LocalDate.parse("2020-01-01")))
.membershipFeeBillable(true)
.build();
return toCleanup(membershipRepo.save(newMembership).load());
});
// then
result.assertSuccessful();
@ -89,31 +87,6 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
assertThat(membershipRepo.count()).isEqualTo(count + 1);
}
@Test
public void creatingMembershipForSamePartnerIsDisallowedIfAnotherOneIsStillActive() {
// given
context("superuser-alex@hostsharing.net");
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("First").getFirst();
// when
final var result = attempt(
em, () -> {
final var newMembership = HsOfficeMembershipEntity.builder()
.memberNumberSuffix("11")
.partner(givenPartner)
.validity(Range.closedInfinite(LocalDate.parse("2024-01-01")))
.membershipFeeBillable(true)
.build();
return toCleanup(membershipRepo.save(newMembership).load());
});
// then
result.assertExceptionWithRootCauseMessage(
PSQLException.class,
"Membership validity ranges overlap for partnerUuid " + givenPartner.getUuid() +
", partnerNumber " + givenPartner.getPartnerNumber());
}
@Test
public void createsAndGrantsRoles() {
// given
@ -124,17 +97,16 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
.toList();
// when
attempt(
em, () -> {
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("First").get(0);
final var newMembership = HsOfficeMembershipEntity.builder()
.memberNumberSuffix("17")
.partner(givenPartner)
.validity(Range.closedInfinite(LocalDate.parse("2025-01-01")))
.membershipFeeBillable(true)
.build();
return toCleanup(membershipRepo.save(newMembership));
}).assertSuccessful();
attempt(em, () -> {
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("First").get(0);
final var newMembership = HsOfficeMembershipEntity.builder()
.memberNumberSuffix("17")
.partner(givenPartner)
.validity(Range.closedInfinite(LocalDate.parse("2020-01-01")))
.membershipFeeBillable(true)
.build();
return toCleanup(membershipRepo.save(newMembership));
}).assertSuccessful();
// then
final var all = rawRoleRepo.findAll();
@ -173,7 +145,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
private void assertThatMembershipIsPersisted(final HsOfficeMembershipEntity saved) {
final var found = membershipRepo.findByUuid(saved.getUuid());
assertThat(found).isNotEmpty().get().extracting(Object::toString).isEqualTo(saved.toString());
assertThat(found).isNotEmpty().get().extracting(Object::toString).isEqualTo(saved.toString()) ;
}
}
@ -191,8 +163,8 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
// then
exactlyTheseMembershipsAreReturned(
result,
"Membership(M-1000101, P-10001, [2022-10-01,2024-12-31), CANCELLED)",
"Membership(M-1000202, P-10002, [2022-10-01,2026-01-01), CANCELLED)",
"Membership(M-1000101, P-10001, [2022-10-01,), ACTIVE)",
"Membership(M-1000202, P-10002, [2022-10-01,), ACTIVE)",
"Membership(M-1000303, P-10003, [2022-10-01,), ACTIVE)");
}
@ -206,9 +178,8 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
final var result = membershipRepo.findMembershipsByPartnerUuid(givenPartner.getUuid());
// then
exactlyTheseMembershipsAreReturned(
result,
"Membership(M-1000101, P-10001, [2022-10-01,2024-12-31), CANCELLED)");
exactlyTheseMembershipsAreReturned(result,
"Membership(M-1000101, P-10001, [2022-10-01,), ACTIVE)");
}
@Test
@ -223,7 +194,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
assertThat(result)
.isNotNull()
.extracting(Object::toString)
.isEqualTo("Membership(M-1000202, P-10002, [2022-10-01,2026-01-01), CANCELLED)");
.isEqualTo("Membership(M-1000202, P-10002, [2022-10-01,), ACTIVE)");
}
@Test
@ -238,7 +209,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
assertThat(result)
.isNotNull()
.extracting(Object::toString)
.isEqualTo("Membership(M-1000202, P-10002, [2022-10-01,2026-01-01), CANCELLED)");
.isEqualTo("Membership(M-1000202, P-10002, [2022-10-01,), ACTIVE)");
}
@Test
@ -250,9 +221,8 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
final var result = membershipRepo.findMembershipsByPartnerNumber(10002);
// then
exactlyTheseMembershipsAreReturned(
result,
"Membership(M-1000202, P-10002, [2022-10-01,2026-01-01), CANCELLED)");
exactlyTheseMembershipsAreReturned(result,
"Membership(M-1000202, P-10002, [2022-10-01,), ACTIVE)");
}
}
@ -263,7 +233,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
public void globalAdmin_canUpdateValidityOfArbitraryMembership() {
// given
context("superuser-alex@hostsharing.net");
final var givenMembership = givenSomeTemporaryMembership("First", "11");
final var givenMembership = givenSomeTemporaryMembership("First", "11");
assertThatMembershipExistsAndIsAccessibleToCurrentContext(givenMembership);
final var newValidityEnd = LocalDate.now();
@ -303,8 +273,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
});
// then
result.assertExceptionWithRootCauseMessage(
JpaSystemException.class,
result.assertExceptionWithRootCauseMessage(JpaSystemException.class,
"[403] Subject ", " is not allowed to update hs_office.membership uuid");
}
@ -412,16 +381,14 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
"[creating Membership test-data, hs_office.membership, INSERT, 03]");
}
private HsOfficeMembershipEntity givenSomeTemporaryMembership(
final String partnerTradeName,
final String memberNumberSuffix) {
private HsOfficeMembershipEntity givenSomeTemporaryMembership(final String partnerTradeName, final String memberNumberSuffix) {
return jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net");
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike(partnerTradeName).get(0);
final var newMembership = HsOfficeMembershipEntity.builder()
.memberNumberSuffix(memberNumberSuffix)
.partner(givenPartner)
.validity(Range.closedInfinite(LocalDate.parse("2025-02-01")))
.validity(Range.closedInfinite(LocalDate.parse("2020-01-01")))
.membershipFeeBillable(true)
.build();

View File

@ -316,7 +316,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu
context.define("superuser-alex@hostsharing.net");
final var givenPartner = givenSomeTemporaryPartnerBessler(20011);
final var newPartnerPerson = personRealRepo.findPersonByOptionalNameLike("Winkler").getFirst();
final var givenPartnerRel = givenSomeTemporaryPartnerRel("Winkler", "third contact");
RestAssured // @formatter:off
.given()
@ -325,9 +325,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu
.body("""
{
"partnerNumber": "P-20011",
"partnerRel": {
"holder.uuid": "%s"
},
"partnerRel.uuid": "%s",
"details": {
"registrationOffice": "Temp Registergericht Aurich",
"registrationNumber": "222222",
@ -336,7 +334,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu
"dateOfDeath": "2022-01-12"
}
}
""".formatted(newPartnerPerson.getUuid()))
""".formatted(givenPartnerRel.getUuid()))
.port(port)
.when()
.patch("http://localhost/api/hs/office/partners/" + givenPartner.getUuid())
@ -350,7 +348,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu
"anchor": { "tradeName": "Hostsharing eG" },
"holder": { "familyName": "Winkler" },
"type": "PARTNER",
"contact": { "caption": "fourth contact" }
"contact": { "caption": "third contact" }
},
"details": {
"registrationOffice": "Temp Registergericht Aurich",
@ -370,7 +368,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu
.matches(partner -> {
assertThat(partner.getPartnerNumber()).isEqualTo(givenPartner.getPartnerNumber());
assertThat(partner.getPartnerRel().getHolder().getFamilyName()).isEqualTo("Winkler");
assertThat(partner.getPartnerRel().getContact().getCaption()).isEqualTo("fourth contact");
assertThat(partner.getPartnerRel().getContact().getCaption()).isEqualTo("third contact");
assertThat(partner.getDetails().getRegistrationOffice()).isEqualTo("Temp Registergericht Aurich");
assertThat(partner.getDetails().getRegistrationNumber()).isEqualTo("222222");
assertThat(partner.getDetails().getBirthName()).isEqualTo("Maja Schmidt");
@ -381,11 +379,11 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu
}
@Test
void patchingThePartnerPersonCreatesExPartnerRel() {
void patchingThePartnerRelCreatesExPartnerRel() {
context.define("superuser-alex@hostsharing.net");
final var givenPartner = givenSomeTemporaryPartnerBessler(20011);
final var newPartnerPerson = personRealRepo.findPersonByOptionalNameLike("Winkler").getFirst();
final var givenPartnerRel = givenSomeTemporaryPartnerRel("Winkler", "third contact");
RestAssured // @formatter:off
.given()
@ -393,11 +391,9 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu
.contentType(ContentType.JSON)
.body("""
{
"partnerRel": {
"holder.uuid": "%s"
}
"partnerRel.uuid": "%s"
}
""".formatted(newPartnerPerson.getUuid()))
""".formatted(givenPartnerRel.getUuid()))
.port(port)
.when()
.patch("http://localhost/api/hs/office/partners/" + givenPartner.getUuid())
@ -409,16 +405,16 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu
context.define("superuser-alex@hostsharing.net");
assertThat(partnerRepo.findByUuid(givenPartner.getUuid())).isPresent().get()
.matches(partner -> {
assertThat(partner.getPartnerRel().getHolder().getFamilyName()).isEqualTo("Winkler"); // updated
assertThat(partner.getPartnerRel().getContact().getCaption()).isEqualTo("fourth contact"); // unchanged
assertThat(partner.getPartnerRel().getHolder().getFamilyName()).isEqualTo("Winkler");
assertThat(partner.getPartnerRel().getContact().getCaption()).isEqualTo("third contact");
return true;
});
// and an ex-partner-relation got created
final var newPartnerPersonUuid = givenPartner.getPartnerRel().getHolder().getUuid();
assertThat(relationRepo.findRelationRelatedToPersonUuidRelationTypeMarkPersonAndContactData(newPartnerPersonUuid, EX_PARTNER, null, null, null))
final var anchorpartnerPersonUUid = givenPartner.getPartnerRel().getAnchor().getUuid();
assertThat(relationRepo.findRelationRelatedToPersonUuidRelationTypeMarkPersonAndContactData(anchorpartnerPersonUUid, EX_PARTNER, null, null, null))
.map(HsOfficeRelation::toShortString)
.contains("rel(anchor='NP Winkler, Paul', type='EX_PARTNER', holder='UF Erben Bessler')");
.contains("rel(anchor='LP Hostsharing eG', type='EX_PARTNER', holder='UF Erben Bessler')");
}
@Test

View File

@ -1,7 +1,6 @@
package net.hostsharing.hsadminng.hs.office.partner;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactFromResourceConverter;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRbacEntity;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
@ -39,7 +38,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(HsOfficePartnerController.class)
@Import({ StrictMapper.class, HsOfficeContactFromResourceConverter.class, DisableSecurityConfig.class})
@Import({StrictMapper.class, DisableSecurityConfig.class})
@ActiveProfiles("test")
class HsOfficePartnerControllerRestTest {
@ -109,6 +108,8 @@ class HsOfficePartnerControllerRestTest {
"holder.uuid": "%s",
"contact.uuid": "%s"
},
"person.uuid": "%s",
"contact.uuid": "%s",
"details": {
"registrationOffice": "Temp Registergericht Aurich",
"registrationNumber": "111111"
@ -117,6 +118,8 @@ class HsOfficePartnerControllerRestTest {
""".formatted(
GIVEN_MANDANTE_UUID,
GIVEN_INVALID_UUID,
GIVEN_CONTACT_UUID,
GIVEN_INVALID_UUID,
GIVEN_CONTACT_UUID))
.accept(MediaType.APPLICATION_JSON))
@ -142,6 +145,8 @@ class HsOfficePartnerControllerRestTest {
"holder.uuid": "%s",
"contact.uuid": "%s"
},
"person.uuid": "%s",
"contact.uuid": "%s",
"details": {
"registrationOffice": "Temp Registergericht Aurich",
"registrationNumber": "111111"
@ -150,6 +155,8 @@ class HsOfficePartnerControllerRestTest {
""".formatted(
GIVEN_MANDANTE_UUID,
GIVEN_PERSON_UUID,
GIVEN_INVALID_UUID,
GIVEN_PERSON_UUID,
GIVEN_INVALID_UUID))
.accept(MediaType.APPLICATION_JSON))

View File

@ -1,25 +1,20 @@
package net.hostsharing.hsadminng.hs.office.partner;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerDetailsPatchResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerPatchResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeRelationPatchResource;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.openapitools.jackson.nullable.JsonNullable;
import java.time.LocalDate;
import jakarta.persistence.EntityManager;
import java.util.UUID;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
@ -27,17 +22,19 @@ import static org.mockito.Mockito.lenient;
@TestInstance(PER_CLASS)
@ExtendWith(MockitoExtension.class)
// This test class does not subclass PatchUnitTestBase because it has no directly patchable properties.
// But the factory-structure is kept, so PatchUnitTestBase could easily be plugged back in if needed.
class HsOfficePartnerEntityPatcherUnitTest {
class HsOfficePartnerEntityPatcherUnitTest extends PatchUnitTestBase<
HsOfficePartnerPatchResource,
HsOfficePartnerRbacEntity
> {
private static final UUID INITIAL_PARTNER_UUID = UUID.randomUUID();
private static final UUID INITIAL_CONTACT_UUID = UUID.randomUUID();
private static final UUID INITIAL_PARTNER_PERSON_UUID = UUID.randomUUID();
private static final UUID INITIAL_PERSON_UUID = UUID.randomUUID();
private static final UUID INITIAL_DETAILS_UUID = UUID.randomUUID();
private static final UUID PATCHED_PARTNER_ROLE_UUID = UUID.randomUUID();
private final HsOfficePersonRealEntity givenInitialPartnerPerson = HsOfficePersonRealEntity.builder()
.uuid(INITIAL_PARTNER_PERSON_UUID)
private final HsOfficePersonRealEntity givenInitialPerson = HsOfficePersonRealEntity.builder()
.uuid(INITIAL_PERSON_UUID)
.build();
private final HsOfficeContactRealEntity givenInitialContact = HsOfficeContactRealEntity.builder()
.uuid(INITIAL_CONTACT_UUID)
@ -46,74 +43,22 @@ class HsOfficePartnerEntityPatcherUnitTest {
private final HsOfficePartnerDetailsEntity givenInitialDetails = HsOfficePartnerDetailsEntity.builder()
.uuid(INITIAL_DETAILS_UUID)
.build();
@Mock
private EntityManagerWrapper emw;
private StrictMapper mapper = new StrictMapper(emw);
private EntityManager em;
@BeforeEach
void initMocks() {
lenient().when(emw.getReference(eq(HsOfficePersonRealEntity.class), any())).thenAnswer(invocation ->
HsOfficePersonRealEntity.builder().uuid(invocation.getArgument(1)).build());
lenient().when(emw.getReference(eq(HsOfficeContactRealEntity.class), any())).thenAnswer(invocation ->
HsOfficeContactRealEntity.builder().uuid(invocation.getArgument(1)).build());
}
@Test
void patchPartnerPerson() {
// given
final var patchResource = newPatchResource();
final var newHolderUuid = UUID.randomUUID();
patchResource.setPartnerRel(new HsOfficeRelationPatchResource());
patchResource.getPartnerRel().setHolderUuid(JsonNullable.of(newHolderUuid));
final var entity = newInitialEntity();
// when
createPatcher(entity).apply(patchResource);
// then
assertThat(entity.getPartnerRel().getHolder().getUuid()).isEqualTo(newHolderUuid);
}
@Test
void patchPartnerContact() {
// given
final var patchResource = newPatchResource();
final var newContactUuid = UUID.randomUUID();
patchResource.setPartnerRel(new HsOfficeRelationPatchResource());
patchResource.getPartnerRel().setContactUuid(JsonNullable.of(newContactUuid));
final var entity = newInitialEntity();
// when
createPatcher(entity).apply(patchResource);
// then
assertThat(entity.getPartnerRel().getContact().getUuid()).isEqualTo(newContactUuid);
}
@Test
void patchPartnerDetails() {
// given
final var patchResource = newPatchResource();
final var newDateOfBirth = LocalDate.now();
patchResource.setDetails(new HsOfficePartnerDetailsPatchResource());
patchResource.getDetails().setDateOfDeath(JsonNullable.of(newDateOfBirth));
final var entity = newInitialEntity();
// when
createPatcher(entity).apply(patchResource);
// then
assertThat(entity.getDetails().getDateOfDeath()).isEqualTo(newDateOfBirth);
lenient().when(em.getReference(eq(HsOfficeRelationRealEntity.class), any())).thenAnswer(invocation ->
HsOfficeRelationRealEntity.builder().uuid(invocation.getArgument(1)).build());
}
@Override
protected HsOfficePartnerRbacEntity newInitialEntity() {
final var entity = HsOfficePartnerRbacEntity.builder()
.uuid(INITIAL_PARTNER_UUID)
.partnerNumber(12345)
.partnerRel(HsOfficeRelationRealEntity.builder()
.holder(givenInitialPartnerPerson)
.holder(givenInitialPerson)
.contact(givenInitialContact)
.build())
.details(givenInitialDetails)
@ -121,11 +66,32 @@ class HsOfficePartnerEntityPatcherUnitTest {
return entity;
}
@Override
protected HsOfficePartnerPatchResource newPatchResource() {
return new HsOfficePartnerPatchResource();
}
@Override
protected HsOfficePartnerEntityPatcher createPatcher(final HsOfficePartnerRbacEntity partner) {
return new HsOfficePartnerEntityPatcher(mapper, emw, partner);
return new HsOfficePartnerEntityPatcher(em, partner);
}
@Override
protected Stream<Property> propertyTestDescriptors() {
return Stream.of(
new JsonNullableProperty<>(
"partnerRel",
HsOfficePartnerPatchResource::setPartnerRelUuid,
PATCHED_PARTNER_ROLE_UUID,
HsOfficePartnerRbacEntity::setPartnerRel,
newPartnerRel(PATCHED_PARTNER_ROLE_UUID))
.notNullable()
);
}
private static HsOfficeRelationRealEntity newPartnerRel(final UUID uuid) {
return HsOfficeRelationRealEntity.builder()
.uuid(uuid)
.build();
}
}

View File

@ -45,13 +45,13 @@ class HsOfficeRelationControllerAcceptanceTest extends ContextBasedTestWithClean
Context context;
@Autowired
HsOfficeRelationRealRepository realRelationRepo;
HsOfficeRelationRealRepository relationrealRepo;
@Autowired
HsOfficePersonRealRepository realPersonRepo;
HsOfficePersonRealRepository personRepo;
@Autowired
HsOfficeContactRealRepository realContactRepo;
HsOfficeContactRealRepository contactrealRepo;
@Autowired
JpaAttempt jpaAttempt;
@ -64,7 +64,7 @@ class HsOfficeRelationControllerAcceptanceTest extends ContextBasedTestWithClean
// given
context.define("superuser-alex@hostsharing.net");
final var givenPerson = realPersonRepo.findPersonByOptionalNameLike("Hostsharing eG").getFirst();
final var givenPerson = personRepo.findPersonByOptionalNameLike("Hostsharing eG").get(0);
RestAssured // @formatter:off
.given()
@ -122,7 +122,7 @@ class HsOfficeRelationControllerAcceptanceTest extends ContextBasedTestWithClean
// given
context.define("contact-admin@firstcontact.example.com");
final var givenPerson = realPersonRepo.findPersonByOptionalNameLike("First GmbH").getFirst();
final var givenPerson = personRepo.findPersonByOptionalNameLike("First GmbH").get(0);
RestAssured // @formatter:off
.given()
@ -229,9 +229,9 @@ class HsOfficeRelationControllerAcceptanceTest extends ContextBasedTestWithClean
void globalAdmin_withoutAssumedRole_canAddRelationWithHolderUuidAndContactUuid() {
context.define("superuser-alex@hostsharing.net");
final var givenAnchorPerson = realPersonRepo.findPersonByOptionalNameLike("Third").getFirst();
final var givenHolderPerson = realPersonRepo.findPersonByOptionalNameLike("Paul").getFirst();
final var givenContact = realContactRepo.findContactByOptionalCaptionLike("second").getFirst();
final var givenAnchorPerson = personRepo.findPersonByOptionalNameLike("Third").get(0);
final var givenHolderPerson = personRepo.findPersonByOptionalNameLike("Paul").get(0);
final var givenContact = contactrealRepo.findContactByOptionalCaptionLike("second").get(0);
final var location = RestAssured // @formatter:off
.given()
@ -276,7 +276,7 @@ class HsOfficeRelationControllerAcceptanceTest extends ContextBasedTestWithClean
void globalAdmin_withoutAssumedRole_canAddRelationWithHolderAndContactData() {
context.define("superuser-alex@hostsharing.net");
final var givenAnchorPerson = realPersonRepo.findPersonByOptionalNameLike("Third").getFirst();
final var givenAnchorPerson = personRepo.findPersonByOptionalNameLike("Third").get(0);
final var location = RestAssured // @formatter:off
.given()
@ -343,8 +343,8 @@ class HsOfficeRelationControllerAcceptanceTest extends ContextBasedTestWithClean
context.define("superuser-alex@hostsharing.net");
final var givenAnchorPersonUuid = GIVEN_NON_EXISTING_HOLDER_PERSON_UUID;
final var givenHolderPerson = realPersonRepo.findPersonByOptionalNameLike("Smith").getFirst();
final var givenContact = realContactRepo.findContactByOptionalCaptionLike("fourth").getFirst();
final var givenHolderPerson = personRepo.findPersonByOptionalNameLike("Smith").get(0);
final var givenContact = contactrealRepo.findContactByOptionalCaptionLike("fourth").get(0);
RestAssured // @formatter:off
.given()
@ -375,8 +375,8 @@ class HsOfficeRelationControllerAcceptanceTest extends ContextBasedTestWithClean
void globalAdmin_canNotAddRelation_ifHolderPersonDoesNotExist() {
context.define("superuser-alex@hostsharing.net");
final var givenAnchorPerson = realPersonRepo.findPersonByOptionalNameLike("Third").getFirst();
final var givenContact = realContactRepo.findContactByOptionalCaptionLike("fourth").getFirst();
final var givenAnchorPerson = personRepo.findPersonByOptionalNameLike("Third").get(0);
final var givenContact = contactrealRepo.findContactByOptionalCaptionLike("fourth").get(0);
final var location = RestAssured // @formatter:off
.given()
@ -407,8 +407,8 @@ class HsOfficeRelationControllerAcceptanceTest extends ContextBasedTestWithClean
void globalAdmin_canNotAddRelation_ifContactDoesNotExist() {
context.define("superuser-alex@hostsharing.net");
final var givenAnchorPerson = realPersonRepo.findPersonByOptionalNameLike("Third").getFirst();
final var givenHolderPerson = realPersonRepo.findPersonByOptionalNameLike("Paul").getFirst();
final var givenAnchorPerson = personRepo.findPersonByOptionalNameLike("Third").get(0);
final var givenHolderPerson = personRepo.findPersonByOptionalNameLike("Paul").get(0);
final var givenContactUuid = UUID.fromString("00000000-0000-0000-0000-000000000000");
final var location = RestAssured // @formatter:off
@ -506,9 +506,9 @@ class HsOfficeRelationControllerAcceptanceTest extends ContextBasedTestWithClean
private HsOfficeRelation findRelation(
final String anchorPersonName,
final String holderPersoneName) {
final var anchorPersonUuid = realPersonRepo.findPersonByOptionalNameLike(anchorPersonName).getFirst().getUuid();
final var holderPersonUuid = realPersonRepo.findPersonByOptionalNameLike(holderPersoneName).getFirst().getUuid();
final var givenRelation = realRelationRepo
final var anchorPersonUuid = personRepo.findPersonByOptionalNameLike(anchorPersonName).get(0).getUuid();
final var holderPersonUuid = personRepo.findPersonByOptionalNameLike(holderPersoneName).get(0).getUuid();
final var givenRelation = relationrealRepo
.findRelationRelatedToPersonUuid(anchorPersonUuid)
.stream()
.filter(r -> r.getHolder().getUuid().equals(holderPersonUuid))
@ -525,7 +525,7 @@ class HsOfficeRelationControllerAcceptanceTest extends ContextBasedTestWithClean
context.define("superuser-alex@hostsharing.net");
final var givenRelation = givenSomeTemporaryRelationBessler();
assertThat(givenRelation.getContact().getCaption()).isEqualTo("seventh contact");
final var givenContact = realContactRepo.findContactByOptionalCaptionLike("fourth").getFirst();
final var givenContact = contactrealRepo.findContactByOptionalCaptionLike("fourth").get(0);
RestAssured // @formatter:off
.given()
@ -551,7 +551,7 @@ class HsOfficeRelationControllerAcceptanceTest extends ContextBasedTestWithClean
// finally, the relation is actually updated
context.define("superuser-alex@hostsharing.net");
assertThat(realRelationRepo.findByUuid(givenRelation.getUuid())).isPresent().get()
assertThat(relationrealRepo.findByUuid(givenRelation.getUuid())).isPresent().get()
.matches(rel -> {
assertThat(rel.getAnchor().getTradeName()).contains("Bessler");
assertThat(rel.getHolder().getFamilyName()).contains("Winkler");
@ -580,7 +580,7 @@ class HsOfficeRelationControllerAcceptanceTest extends ContextBasedTestWithClean
.statusCode(204); // @formatter:on
// then the given relation is gone
assertThat(realRelationRepo.findByUuid(givenRelation.getUuid())).isEmpty();
assertThat(relationrealRepo.findByUuid(givenRelation.getUuid())).isEmpty();
}
@Test
@ -599,7 +599,7 @@ class HsOfficeRelationControllerAcceptanceTest extends ContextBasedTestWithClean
.statusCode(403); // @formatter:on
// then the given relation is still there
assertThat(realRelationRepo.findByUuid(givenRelation.getUuid())).isNotEmpty();
assertThat(relationrealRepo.findByUuid(givenRelation.getUuid())).isNotEmpty();
}
@Test
@ -618,16 +618,16 @@ class HsOfficeRelationControllerAcceptanceTest extends ContextBasedTestWithClean
.statusCode(404); // @formatter:on
// then the given relation is still there
assertThat(realRelationRepo.findByUuid(givenRelation.getUuid())).isNotEmpty();
assertThat(relationrealRepo.findByUuid(givenRelation.getUuid())).isNotEmpty();
}
}
private HsOfficeRelation givenSomeTemporaryRelationBessler() {
return jpaAttempt.transacted(() -> {
context.define("superuser-alex@hostsharing.net");
final var givenAnchorPerson = realPersonRepo.findPersonByOptionalNameLike("Erben Bessler").getFirst();
final var givenHolderPerson = realPersonRepo.findPersonByOptionalNameLike("Winkler").getFirst();
final var givenContact = realContactRepo.findContactByOptionalCaptionLike("seventh contact").getFirst();
final var givenAnchorPerson = personRepo.findPersonByOptionalNameLike("Erben Bessler").get(0);
final var givenHolderPerson = personRepo.findPersonByOptionalNameLike("Winkler").get(0);
final var givenContact = contactrealRepo.findContactByOptionalCaptionLike("seventh contact").get(0);
final var newRelation = HsOfficeRelationRealEntity.builder()
.type(HsOfficeRelationType.REPRESENTATIVE)
.anchor(givenAnchorPerson)
@ -635,7 +635,7 @@ class HsOfficeRelationControllerAcceptanceTest extends ContextBasedTestWithClean
.contact(givenContact)
.build();
assertThat(toCleanup(realRelationRepo.save(newRelation))).isEqualTo(newRelation);
assertThat(toCleanup(relationrealRepo.save(newRelation))).isEqualTo(newRelation);
return newRelation;
}).assertSuccessful().returnedValue();

View File

@ -1,38 +1,23 @@
package net.hostsharing.hsadminng.hs.office.relation;
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.model.HsOfficeContactInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonTypeResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeRelationPatchResource;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.openapitools.jackson.nullable.JsonNullable;
import jakarta.validation.ValidationException;
import java.util.Map;
import jakarta.persistence.EntityManager;
import java.util.UUID;
import java.util.stream.Stream;
import static net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType.LEGAL_PERSON;
import static net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType.NATURAL_PERSON;
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.PARTNER;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
import static org.assertj.core.api.Assumptions.assumeThat;
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@TestInstance(PER_CLASS)
@ExtendWith(MockitoExtension.class)
@ -41,84 +26,36 @@ class HsOfficeRelationPatcherUnitTest extends PatchUnitTestBase<
HsOfficeRelation
> {
private static final UUID INITIAL_RELATION_UUID = UUID.randomUUID();
private static final UUID INITIAL_ANCHOR_UUID = UUID.randomUUID();
private static final UUID INITIAL_HOLDER_UUID = UUID.randomUUID();
private static final UUID INITIAL_CONTACT_UUID = UUID.randomUUID();
private static final UUID PATCHED_HOLDER_UUID = UUID.randomUUID();
private static HsOfficePersonInsertResource HOLDER_PATCH_RESOURCE = new HsOfficePersonInsertResource();
{
{
HOLDER_PATCH_RESOURCE.setPersonType(HsOfficePersonTypeResource.NATURAL_PERSON);
HOLDER_PATCH_RESOURCE.setFamilyName("Patched-Holder-Family-Name");
HOLDER_PATCH_RESOURCE.setGivenName("Patched-Holder-Given-Name");
}
};
private static HsOfficePersonRealEntity PATCHED_HOLDER = HsOfficePersonRealEntity.builder()
.uuid(PATCHED_HOLDER_UUID)
.personType(NATURAL_PERSON)
.familyName("Patched-Holder-Family-Name")
.givenName("Patched-Holder-Given-Name")
.build();
private static final UUID PATCHED_CONTACT_UUID = UUID.randomUUID();
private static HsOfficeContactInsertResource CONTACT_PATCH_RESOURCE = new HsOfficeContactInsertResource();
{
{
CONTACT_PATCH_RESOURCE.setCaption("Patched-Contact-Caption");
CONTACT_PATCH_RESOURCE.setEmailAddresses(Map.ofEntries(
Map.entry("main", "patched@example.org")
));
}
};
private static HsOfficeContactRealEntity PATCHED_CONTACT = HsOfficeContactRealEntity.builder()
.uuid(PATCHED_CONTACT_UUID)
.caption("Patched-Contact-Caption")
.emailAddresses(Map.ofEntries(
Map.entry("main", "patched@example.org")
))
.build();
static final UUID INITIAL_RELATION_UUID = UUID.randomUUID();
static final UUID PATCHED_CONTACT_UUID = UUID.randomUUID();
@Mock
private EntityManagerWrapper emw;
private StrictMapper mapper;
EntityManager em;
@BeforeEach
void init() {
mapper = new StrictMapper(emw); // emw is injected after the constructor got called
mapper.addConverter(
new HsOfficeContactFromResourceConverter<>(),
HsOfficeContactInsertResource.class, HsOfficeContactRealEntity.class);
lenient().when(emw.getReference(HsOfficePersonRealEntity.class, PATCHED_HOLDER_UUID)).thenAnswer(
p -> PATCHED_HOLDER);
lenient().when(emw.getReference(HsOfficeContactRealEntity.class, PATCHED_CONTACT_UUID)).thenAnswer(
p -> PATCHED_CONTACT);
void initMocks() {
lenient().when(em.getReference(eq(HsOfficeContactRealEntity.class), any())).thenAnswer(invocation ->
HsOfficeContactRealEntity.builder().uuid(invocation.getArgument(1)).build());
}
final HsOfficePersonRealEntity givenInitialAnchorPerson = HsOfficePersonRealEntity.builder()
.uuid(UUID.randomUUID())
.build();
final HsOfficePersonRealEntity givenInitialHolderPerson = HsOfficePersonRealEntity.builder()
.uuid(UUID.randomUUID())
.build();
final HsOfficeContactRealEntity givenInitialContact = HsOfficeContactRealEntity.builder()
.uuid(UUID.randomUUID())
.build();
@Override
protected HsOfficeRelation newInitialEntity() {
final var entity = new HsOfficeRelationRealEntity();
final var entity = new HsOfficeRelationRbacEntity();
entity.setUuid(INITIAL_RELATION_UUID);
entity.setType(PARTNER);
entity.setAnchor(HsOfficePersonRealEntity.builder()
.uuid(INITIAL_ANCHOR_UUID)
.personType(LEGAL_PERSON)
.tradeName("Initial-Anchor-Tradename")
.build());
entity.setHolder(HsOfficePersonRealEntity.builder()
.uuid(INITIAL_HOLDER_UUID)
.personType(NATURAL_PERSON)
.familyName("Initial-Holder-Family-Name")
.givenName("Initial-Holder-Given-Name")
.build());
entity.setContact(HsOfficeContactRealEntity.builder()
.uuid(INITIAL_CONTACT_UUID)
.caption("Initial-Contact-Caption")
.build());
entity.setType(HsOfficeRelationType.REPRESENTATIVE);
entity.setAnchor(givenInitialAnchorPerson);
entity.setHolder(givenInitialHolderPerson);
entity.setContact(givenInitialContact);
return entity;
}
@ -128,114 +65,24 @@ class HsOfficeRelationPatcherUnitTest extends PatchUnitTestBase<
}
@Override
protected HsOfficeRelationPatcher createPatcher(final HsOfficeRelation relation) {
return new HsOfficeRelationPatcher(mapper, emw, relation);
protected HsOfficeRelationEntityPatcher createPatcher(final HsOfficeRelation relation) {
return new HsOfficeRelationEntityPatcher(em, relation);
}
@Override
protected Stream<Property> propertyTestDescriptors() {
return Stream.of(
new JsonNullableProperty<>(
"holderUuid",
HsOfficeRelationPatchResource::setHolderUuid,
PATCHED_HOLDER_UUID,
HsOfficeRelation::setHolder,
PATCHED_HOLDER),
new SimpleProperty<>(
"holder",
HsOfficeRelationPatchResource::setHolder,
HOLDER_PATCH_RESOURCE,
HsOfficeRelation::setHolder,
withoutUuid(PATCHED_HOLDER))
.notNullable(),
new JsonNullableProperty<>(
"contactUuid",
"contact",
HsOfficeRelationPatchResource::setContactUuid,
PATCHED_CONTACT_UUID,
HsOfficeRelation::setContact,
PATCHED_CONTACT),
new SimpleProperty<>(
"contact",
HsOfficeRelationPatchResource::setContact,
CONTACT_PATCH_RESOURCE,
HsOfficeRelation::setContact,
withoutUuid(PATCHED_CONTACT))
newContact(PATCHED_CONTACT_UUID))
.notNullable()
);
}
@Override
protected void willPatchAllProperties() {
// this generic test does not work because either holder or holder.uuid can be set
assumeThat(true).isFalse();
}
@Test
void willThrowExceptionIfHolderAndHolderUuidAreGiven() {
// given
final var givenEntity = newInitialEntity();
final var patchResource = newPatchResource();
patchResource.setHolderUuid(JsonNullable.of(PATCHED_HOLDER_UUID));
patchResource.setHolder(HOLDER_PATCH_RESOURCE);
// when
final var exception = catchThrowable(() -> createPatcher(givenEntity).apply(patchResource));
// then
assertThat(exception).isInstanceOf(ValidationException.class)
.hasMessage("either \"holder\" or \"holder.uuid\" can be given, not both");
}
@Test
void willThrowExceptionIfContactAndContactUuidAreGiven() {
// given
final var givenEntity = newInitialEntity();
final var patchResource = newPatchResource();
patchResource.setContactUuid(JsonNullable.of(PATCHED_CONTACT_UUID));
patchResource.setContact(CONTACT_PATCH_RESOURCE);
// when
final var exception = catchThrowable(() -> createPatcher(givenEntity).apply(patchResource));
// then
assertThat(exception).isInstanceOf(ValidationException.class)
.hasMessage("either \"contact\" or \"contact.uuid\" can be given, not both");
}
@Test
void willPersistNewHolder() {
// given
final var givenEntity = newInitialEntity();
final var patchResource = newPatchResource();
patchResource.setHolder(HOLDER_PATCH_RESOURCE);
// when
createPatcher(givenEntity).apply(patchResource);
// then
verify(emw, times(1)).persist(givenEntity.getHolder());
}
@Test
void willPersistNewContact() {
// given
final var givenEntity = newInitialEntity();
final var patchResource = newPatchResource();
patchResource.setContact(CONTACT_PATCH_RESOURCE);
// when
createPatcher(givenEntity).apply(patchResource);
// then
verify(emw, times(1)).persist(givenEntity.getContact());
}
private HsOfficePersonRealEntity withoutUuid(final HsOfficePersonRealEntity givenWithUuid) {
return givenWithUuid.toBuilder().uuid(null).build();
}
private HsOfficeContactRealEntity withoutUuid(final HsOfficeContactRealEntity givenWithUuid) {
return givenWithUuid.toBuilder().uuid(null).build();
static HsOfficeContactRealEntity newContact(final UUID uuid) {
return HsOfficeContactRealEntity.builder().uuid(uuid).build();
}
}

View File

@ -28,7 +28,6 @@ import static net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType.NATU
import static net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType.UNINCORPORATED_FIRM;
import static net.hostsharing.hsadminng.rbac.grant.RawRbacGrantEntity.distinctGrantDisplaysOf;
import static net.hostsharing.hsadminng.rbac.role.RawRbacRoleEntity.distinctRoleNamesOf;
import static net.hostsharing.hsadminng.rbac.role.RbacRoleType.ADMIN;
import static net.hostsharing.hsadminng.rbac.test.JpaAttempt.attempt;
import static org.assertj.core.api.Assertions.assertThat;
@ -283,40 +282,8 @@ class HsOfficeRelationRepositoryIntegrationTest extends ContextBasedTestWithClea
assertThatRelationIsNotVisibleForUserWithRole(
result.returnedValue(),
"hs_office.contact#fifthcontact:ADMIN");
}
@Test
public void hostsharingAdmin_withoutAssumedRole_canUpdateHolderOfArbitraryRelation() {
// given
context("superuser-alex@hostsharing.net");
final var givenRelation = givenSomeTemporaryRelationBessler(
"Bert", "fifth contact");
final var oldHolderPerson = givenRelation.getHolder();
final var newHolderPerson = personRepo.findPersonByOptionalNameLike("Paul").getFirst();
assertThatRelationActuallyInDatabase(givenRelation);
assertThatRelationIsVisibleForUserWithRole(
givenRelation,
givenRelation.getHolder().roleId(ADMIN));
// when
final var result = jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net");
givenRelation.setHolder(newHolderPerson);
return toCleanup(relationRbacRepo.save(givenRelation).load());
});
// then
result.assertSuccessful();
assertThat(result.returnedValue().getHolder().getGivenName()).isEqualTo("Paul");
assertThatRelationIsVisibleForUserWithRole(
result.returnedValue(),
"rbac.global#global:ADMIN");
assertThatRelationIsVisibleForUserWithRole(
result.returnedValue(),
newHolderPerson.roleId(ADMIN));
assertThatRelationIsNotVisibleForUserWithRole(
result.returnedValue(),
oldHolderPerson.roleId(ADMIN));
relationRbacRepo.deleteByUuid(givenRelation.getUuid());
}
@Test
@ -329,17 +296,13 @@ class HsOfficeRelationRepositoryIntegrationTest extends ContextBasedTestWithClea
givenRelation,
"hs_office.relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerAnita:AGENT");
assertThatRelationActuallyInDatabase(givenRelation);
final var givenContact = contactRealRepo.findContactByOptionalCaptionLike("sixth contact")
.stream()
.findFirst()
.orElseThrow();
// when
final var result = jpaAttempt.transacted(() -> {
context(
"superuser-alex@hostsharing.net",
"hs_office.relation#ErbenBesslerMelBessler-with-REPRESENTATIVE-BesslerAnita:AGENT");
givenRelation.setContact(givenContact);
givenRelation.setContact(null);
return relationRbacRepo.save(givenRelation);
});
@ -492,9 +455,9 @@ class HsOfficeRelationRepositoryIntegrationTest extends ContextBasedTestWithClea
private HsOfficeRelationRbacEntity givenSomeTemporaryRelationBessler(final String holderPerson, final String contact) {
return jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net");
final var givenAnchorPerson = personRepo.findPersonByOptionalNameLike("Erben Bessler").getFirst();
final var givenHolderPerson = personRepo.findPersonByOptionalNameLike(holderPerson).getFirst();
final var givenContact = contactRealRepo.findContactByOptionalCaptionLike(contact).getFirst();
final var givenAnchorPerson = personRepo.findPersonByOptionalNameLike("Erben Bessler").get(0);
final var givenHolderPerson = personRepo.findPersonByOptionalNameLike(holderPerson).get(0);
final var givenContact = contactRealRepo.findContactByOptionalCaptionLike(contact).get(0);
final var newRelation = HsOfficeRelationRbacEntity.builder()
.type(HsOfficeRelationType.REPRESENTATIVE)
.anchor(givenAnchorPerson)

View File

@ -7,7 +7,6 @@ import net.hostsharing.hsadminng.hs.office.scenarios.contact.RemovePhoneNumberFr
import net.hostsharing.hsadminng.hs.office.scenarios.contact.ReplaceContactData;
import net.hostsharing.hsadminng.hs.office.scenarios.debitor.CreateExternalDebitorForPartner;
import net.hostsharing.hsadminng.hs.office.scenarios.debitor.CreateSelfDebitorForPartner;
import net.hostsharing.hsadminng.hs.office.scenarios.debitor.CreateSelfDebitorForPartnerWithIdenticalContactData;
import net.hostsharing.hsadminng.hs.office.scenarios.debitor.CreateSepaMandateForDebitor;
import net.hostsharing.hsadminng.hs.office.scenarios.debitor.DeleteDebitor;
import net.hostsharing.hsadminng.hs.office.scenarios.debitor.DontDeleteDefaultDebitor;
@ -266,27 +265,11 @@ class HsOfficeScenarioTests extends ScenarioTest {
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class DebitorScenarios {
@Test
@Order(2000)
@Requires("Partner: P-31011 - Michelle Matthieu")
@Produces("Debitor: D-3101100 - Michelle Matthieu")
void shouldCreateSelfDebitorForPartnerWithIdenticalContactData() {
new CreateSelfDebitorForPartnerWithIdenticalContactData(scenarioTest)
.given("partnerNumber", "P-31011")
.given("debitorNumberSuffix", "00") // TODO.impl: could be assigned automatically, but is not yet
.given("billable", true)
.given("vatBusiness", false)
.given("vatReverseCharge", false)
.given("defaultPrefix", "mim")
.doRun()
.keep();
}
@Test
@Order(2010)
@Requires("Partner: P-31010 - Test AG")
@Produces("Debitor: D-3101000 - Test AG - main debitor")
void shouldCreateSelfDebitorForPartnerWithDistinctContactData() {
void shouldCreateSelfDebitorForPartner() {
new CreateSelfDebitorForPartner(scenarioTest)
.given("partnerPersonTradeName", "Test AG")
.given("billingContactCaption", "Test AG - billing department")
@ -304,12 +287,12 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test
@Order(2011)
@Requires("Debitor: D-3101000 - Test AG - main debitor")
@Produces("Debitor: D-3101001 - Test AG - additional debitor")
void shouldCreateAdditionDebitorForPartner() {
new CreateSelfDebitorForPartner(scenarioTest)
@Requires("Person: Test AG")
@Produces("Debitor: D-3101001 - Test AG - main debitor")
void shouldCreateExternalDebitorForPartner() {
new CreateExternalDebitorForPartner(scenarioTest)
.given("partnerPersonTradeName", "Test AG")
.given("billingContactCaption", "Test AG - billing department")
.given("billingContactCaption", "Billing GmbH - billing department")
.given("billingContactEmailAddress", "billing@test-ag.example.org")
.given("debitorNumberSuffix", "01")
.given("billable", true)
@ -322,30 +305,10 @@ class HsOfficeScenarioTests extends ScenarioTest {
.keep();
}
@Test
@Order(2012)
@Requires("Person: Test AG")
@Produces("Debitor: D-3101002 - Test AG - external debitor")
void shouldCreateExternalDebitorForPartner() {
new CreateExternalDebitorForPartner(scenarioTest)
.given("partnerPersonTradeName", "Test AG")
.given("billingContactCaption", "Billing GmbH - billing department")
.given("billingContactEmailAddress", "billing@test-ag.example.org")
.given("debitorNumberSuffix", "02")
.given("billable", true)
.given("vatId", "VAT123456")
.given("vatCountryCode", "DE")
.given("vatBusiness", true)
.given("vatReverseCharge", false)
.given("defaultPrefix", "tsy")
.doRun()
.keep();
}
@Test
@Order(2020)
@Requires("Person: Test AG")
@Produces(explicitly = "Debitor: D-3101002 - Test AG - delete debitor", permanent = false)
@Produces(explicitly = "Debitor: D-3101000 - Test AG - delete debitor", permanent = false)
void shouldDeleteDebitor() {
new DeleteDebitor(scenarioTest)
.given("partnerNumber", "P-31020")
@ -354,7 +317,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
}
@Test
@Order(2021)
@Order(2020)
@Requires("Debitor: D-3101000 - Test AG - main debitor")
@Disabled("see TODO.spec in DontDeleteDefaultDebitor")
void shouldNotDeleteDefaultDebitor() {
@ -424,39 +387,22 @@ class HsOfficeScenarioTests extends ScenarioTest {
void shouldCreateMembershipForPartner() {
new CreateMembership(scenarioTest)
.given("partnerName", "Test AG")
.given("validFrom", "2020-10-15")
.given("validFrom", "2024-10-15")
.given("newStatus", "ACTIVE")
.given("membershipFeeBillable", "true")
.doRun()
.keep();
}
@Test
@Order(4080)
@Requires("Membership: M-3101000 - Test AG")
@Produces("Membership: M-3101000 - Test AG - cancelled")
void shouldCancelMembershipOfPartner() {
new CancelMembership(scenarioTest)
.given("memberNumber", "M-3101000")
.given("validTo", "2023-12-31")
.given("newStatus", "CANCELLED")
.doRun()
.keep();
}
@Test
@Order(4090)
@Requires("Membership: M-3101000 - Test AG - cancelled")
@Produces("Membership: M-3101001 - Test AG")
void shouldCreateSubsequentMembershipOfPartner() {
new CreateMembership(scenarioTest)
.given("partnerName", "Test AG")
.given("memberNumberSuffix", "01")
.given("validFrom", "2025-02-24")
.given("newStatus", "ACTIVE")
.given("membershipFeeBillable", "true")
.doRun()
.keep();
@Requires("Membership: M-3101000 - Test AG")
void shouldCancelMembershipOfPartner() {
new CancelMembership(scenarioTest)
.given("memberNumber", "M-3101000")
.given("validTo", "2025-12-30")
.given("newStatus", "CANCELLED")
.doRun();
}
}
@ -671,7 +617,7 @@ class HsOfficeScenarioTests extends ScenarioTest {
@Test
@Order(6010)
@Requires("Debitor: D-3101100 - Michelle Matthieu") // which should also get updated
@Requires("Partner: P-31011 - Michelle Matthieu")
void shouldReplaceDeceasedPartnerByCommunityOfHeirs() {
new ReplaceDeceasedPartnerWithCommunityOfHeirs(scenarioTest)
.given("partnerNumber", "P-31011")

View File

@ -1,45 +0,0 @@
package net.hostsharing.hsadminng.hs.office.scenarios.debitor;
import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
import net.hostsharing.hsadminng.hs.scenarios.UseCase;
import org.springframework.http.HttpStatus;
import static io.restassured.http.ContentType.JSON;
import static org.springframework.http.HttpStatus.CREATED;
public class CreateSelfDebitorForPartnerWithIdenticalContactData
extends UseCase<CreateSelfDebitorForPartnerWithIdenticalContactData> {
public CreateSelfDebitorForPartnerWithIdenticalContactData(final ScenarioTest testSuite) {
super(testSuite);
}
@Override
protected HttpResponse run() {
withTitle("Determine Partner-Person UUID", () ->
httpGet("/api/hs/office/partners/" + uriEncoded("%{partnerNumber}"))
.reportWithResponse().expecting(HttpStatus.OK).expecting(JSON)
.extractUuidAlias("partnerRel.holder.uuid", "partnerPersonUuid")
.extractUuidAlias("partnerRel.contact.uuid", "partnerContactUuid")
);
return httpPost("/api/hs/office/debitors", usingJsonBody("""
{
"debitorRel": {
"anchor.uuid": ${partnerPersonUuid},
"holder.uuid": ${partnerPersonUuid},
"contact.uuid": ${partnerContactUuid}
},
"debitorNumberSuffix": ${debitorNumberSuffix},
"billable": ${billable},
"vatId": ${vatId???},
"vatCountryCode": ${vatCountryCode???},
"vatBusiness": ${vatBusiness},
"vatReverseCharge": ${vatReverseCharge},
"defaultPrefix": ${defaultPrefix}
}
"""))
.expecting(CREATED).expecting(JSON);
}
}

View File

@ -19,7 +19,7 @@ public class DeleteDebitor extends UseCase<DeleteDebitor> {
.given("vatCountryCode", "DE")
.given("vatBusiness", true)
.given("vatReverseCharge", false)
.given("defaultPrefix", "tsz"));
.given("defaultPrefix", "tsy"));
}
@Override

View File

@ -12,81 +12,101 @@ public class ReplaceDeceasedPartnerWithCommunityOfHeirs extends UseCase<ReplaceD
public ReplaceDeceasedPartnerWithCommunityOfHeirs(final ScenarioTest testSuite) {
super(testSuite);
}
@Override
protected HttpResponse run() {
obtain("Partner: %{partnerNumber}",
() -> httpGet("/api/hs/office/partners/%{partnerNumber}")
.reportWithResponse().expecting(OK).expecting(JSON),
obtain("Person: Hostsharing eG", () ->
httpGet("/api/hs/office/persons?name=Hostsharing+eG")
.expecting(OK).expecting(JSON),
response -> response.expectArrayElements(1).getFromBody("[0].uuid"),
"Even in production data we expect this query to return just a single result." // TODO.impl: add constraint?
);
obtain("Partner: %{partnerNumber}", () ->
httpGet("/api/hs/office/partners/%{partnerNumber}")
.reportWithResponse().expecting(OK).expecting(JSON),
response -> response.getFromBody("uuid"),
"Even in production data we expect this query to return just a single result."
// TODO.impl: add constraint?
"Even in production data we expect this query to return just a single result." // TODO.impl: add constraint?
)
.extractValue("partnerRel.holder.familyName", "familyNameOfDeceasedPerson")
.extractValue("partnerRel.holder.givenName", "givenNameOfDeceasedPerson")
.extractUuidAlias(
"partnerRel.holder.uuid",
"Person: %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}");
.extractUuidAlias("partnerRel.holder.uuid", "Person: %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}");
withTitle("New Partner-Person+Contact: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}",
() -> httpPatch("/api/hs/office/partners/%{Partner: %{partnerNumber}}",
usingJsonBody("""
{
"wrong1": false,
"partnerRel": {
"wrong2": false,
"holder": {
"personType": "UNINCORPORATED_FIRM",
"tradeName": "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}",
},
"contact": {
"wrong3": false,
"caption": "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}",
"postalAddress": {
"wrong4": false,
"name": "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}",
"co": "%{representativeGivenName} %{representativeFamilyName}",
%{communityOfHeirsPostalAddress}
},
"phoneNumbers": {
"office": ${communityOfHeirsOfficePhoneNumber}
},
"emailAddresses": {
"main": ${communityOfHeirsEmailAddress}
}
}
}
}
"""))
.reportWithResponse().expecting(HttpStatus.OK).expecting(JSON)
.extractUuidAlias(
"partnerRel.holder.uuid",
"Person: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}")
.extractUuidAlias(
"partnerRel.contact.uuid",
"Contact: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}")
obtain("Partner-Relation: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}", () ->
httpPost("/api/hs/office/relations", usingJsonBody("""
{
"type": "PARTNER",
"anchor.uuid": ${Person: Hostsharing eG},
"holder": {
"personType": "UNINCORPORATED_FIRM",
"tradeName": "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}",
},
"contact": {
"caption": "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}",
"postalAddress": {
"name": "Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}",
"co": "%{representativeGivenName} %{representativeFamilyName}",
%{communityOfHeirsPostalAddress}
},
"phoneNumbers": {
"office": ${communityOfHeirsOfficePhoneNumber}
},
"emailAddresses": {
"main": ${communityOfHeirsEmailAddress}
}
}
}
"""))
.reportWithResponse().expecting(CREATED).expecting(JSON)
)
.extractUuidAlias("contact.uuid", "Contact: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}")
.extractUuidAlias("holder.uuid", "Person: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}");
obtain("Representative-Relation: %{representativeGivenName} %{representativeFamilyName} for Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}", () ->
httpPost("/api/hs/office/relations", usingJsonBody("""
{
"type": "REPRESENTATIVE",
"anchor.uuid": ${Person: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}},
"holder": {
"personType": "NATURAL_PERSON",
"givenName": ${representativeGivenName},
"familyName": ${representativeFamilyName}
},
"contact.uuid": ${Contact: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}}
}
"""))
.reportWithResponse().expecting(CREATED).expecting(JSON)
).extractUuidAlias("holder.uuid", "Person: %{representativeGivenName} %{representativeFamilyName}");
obtain("Partner: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}", () ->
httpPatch("/api/hs/office/partners/%{Partner: %{partnerNumber}}", usingJsonBody("""
{
"partnerRel.uuid": ${Partner-Relation: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}}
}
"""))
.expecting(HttpStatus.OK)
);
obtain(
"Representative-Relation: %{representativeGivenName} %{representativeFamilyName} for Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}",
() -> httpPost("/api/hs/office/relations",
usingJsonBody("""
{
"type": "REPRESENTATIVE",
"anchor.uuid": ${Person: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}},
"holder": {
"personType": "NATURAL_PERSON",
"givenName": ${representativeGivenName},
"familyName": ${representativeFamilyName}
},
"contact.uuid": ${Contact: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}}
}
"""))
.reportWithResponse().expecting(CREATED).expecting(JSON)
)
.extractUuidAlias("holder.uuid", "Person: %{representativeGivenName} %{representativeFamilyName}");
// TODO.test: missing steps Debitor, Membership, Coop-Shares+Assets
// Debitors
// die Erbengemeinschaft wird als Anchor-Person (Partner) in die Debitor-Relations eingetragen
// der neue Rechnungsempfänger (z.B. auch ggf. Rechtsanwalt) wird als Holder-Person (Debitor-Person) in die Debitor-Relations eingetragen -- oder neu?
// Membership
// intro: die Mitgliedschaft geht juristisch gesehen auf die Erbengemeinschaft über
// die bisherige Mitgliedschaft als DECEASED mit Ende-Datum=Todesdatum markieren
// eine neue Mitgliedschaft (-00) mit dem Start-Datum=Todesdatum+1 anlegen
// die Geschäftsanteile per share-tx: TRANSFERADOPT an die Erbengemeinschaft übertragen
// die Geschäftsguthaben per asset-tx: TRANSFERADOPT an die Erbengemeinschaft übertragen
// outro: die Erbengemeinschaft hat eine Frist von 6 Monaten, um die Mitgliedschaft einer Person zu übertragen
// nächster "Drecksfall"
@ -100,44 +120,20 @@ public class ReplaceDeceasedPartnerWithCommunityOfHeirs extends UseCase<ReplaceD
"Verify the Updated Partner",
() -> httpGet("/api/hs/office/partners/%{partnerNumber}")
.expecting(OK).expecting(JSON).expectObject(),
path("partnerRel.holder.tradeName").contains(
"Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}"),
path("partnerRel.contact.postalAddress").lenientlyContainsJson("§{communityOfHeirsPostalAddress}"),
path("partnerRel.contact.phoneNumbers.office").contains("%{communityOfHeirsOfficePhoneNumber}"),
path("partnerRel.contact.emailAddresses.main").contains("%{communityOfHeirsEmailAddress}")
path("partnerRel.holder.tradeName").contains("Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}")
);
verify(
"Verify the Ex-Partner-Relation",
() -> httpGet(
"/api/hs/office/relations?relationType=EX_PARTNER&personUuid=%{Person: %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}}")
.expecting(OK).expecting(JSON).expectArrayElements(1),
path("[0].anchor.tradeName").contains(
"Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}")
);
// TODO.test: Verify the EX_PARTNER-Relation, once we fixed the anchor problem, see HsOfficePartnerController
// (net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerController.optionallyCreateExPartnerRelation)
verify(
"Verify the Representative-Relation",
() -> httpGet(
"/api/hs/office/relations?relationType=REPRESENTATIVE&personUuid=%{Person: Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}}")
() -> httpGet("/api/hs/office/relations?relationType=REPRESENTATIVE&personUuid=%{Person: %{representativeGivenName} %{representativeFamilyName}}")
.expecting(OK).expecting(JSON).expectArrayElements(1),
path("[0].anchor.tradeName").contains(
"Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}"),
path("[0].holder.familyName").contains("%{representativeFamilyName}"),
path("[0].contact.postalAddress").lenientlyContainsJson("§{communityOfHeirsPostalAddress}"),
path("[0].contact.phoneNumbers.office").contains("%{communityOfHeirsOfficePhoneNumber}"),
path("[0].contact.emailAddresses.main").contains("%{communityOfHeirsEmailAddress}")
path("[0].anchor.tradeName").contains("Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}"),
path("[0].holder.familyName").contains("%{representativeFamilyName}")
);
verify(
"Verify the Debitor-Relation",
() -> httpGet(
"/api/hs/office/debitors?partnerNumber=%{partnerNumber}")
.expecting(OK).expecting(JSON).expectArrayElements(1),
path("[0].debitorRel.anchor.tradeName").contains(
"Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}"),
path("[0].debitorRel.holder.tradeName").contains(
"Erbengemeinschaft %{givenNameOfDeceasedPerson} %{familyNameOfDeceasedPerson}")
);
// TODO.test: Verify Debitor, Membership, Coop-Shares and Coop-Assets once implemented
}
}

View File

@ -5,7 +5,6 @@ import net.hostsharing.hsadminng.hs.scenarios.UseCase.HttpResponse;
import java.util.function.Consumer;
import static net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver.DROP_COMMENTS;
import static net.hostsharing.hsadminng.test.JsonMatcher.lenientlyEquals;
import static org.junit.jupiter.api.Assertions.fail;
public class PathAssertion {
@ -28,18 +27,6 @@ public class PathAssertion {
};
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public Consumer<UseCase.HttpResponse> lenientlyContainsJson(final String resolvableValue) {
return response -> {
try {
lenientlyEquals(ScenarioTest.resolve(resolvableValue, DROP_COMMENTS)).matches(response.getFromBody(path)) ;
} catch (final AssertionError e) {
// without this, the error message is often lacking important context
fail(e.getMessage() + " in `path(\"" + path + "\").contains(\"" + resolvableValue + "\")`" );
}
};
}
public Consumer<HttpResponse> doesNotExist() {
return response -> {
try {

View File

@ -32,12 +32,6 @@ public class TemplateResolver {
return jsonQuoted(value);
}
},
JSON_OBJECT('§'){
@Override
String convert(final Object value, final Resolver resolver) {
return jsonObject(value);
}
},
URI_ENCODED('&'){
@Override
String convert(final Object value, final Resolver resolver) {
@ -219,12 +213,4 @@ public class TemplateResolver {
default -> "\"" + value + "\"";
};
}
private static String jsonObject(final Object value) {
return switch (value) {
case null -> null;
case String string -> "{" + string.replace("\n", " ") + "}";
default -> throw new IllegalArgumentException("can not format " + value.getClass() + " (" + value + ") as JSON object");
};
}
}

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