rbac-optimization (#80)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #80
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
Michael Hoennig 2024-07-27 10:18:07 +02:00
parent 4d27a98c9a
commit e1fda412ae
43 changed files with 639 additions and 186 deletions

10
Dockerfile Normal file
View File

@ -0,0 +1,10 @@
# build using:
# docker build -t postgres-with-contrib:15.5-bookworm .
FROM postgres:15.5-bookworm
RUN apt-get update && \
apt-get install -y postgresql-contrib && \
apt-get clean
COPY etc/postgresql-log-slow-queries.conf /etc/postgresql/postgresql.conf

View File

@ -0,0 +1,301 @@
# RBAC Performance Analysis
This describes the analysis of the legacy-data-import which took way too long, which turned out to be a problem in the RBAC-access-rights-check.
## Our Performance-Problem
During the legacy data import for hosting assets we noticed massive performance problems. The import of about 2200 hosting-assets (IP-numbers, managed-webspaces, managed- and cloud-servers) as well as the creation of booking-items and booking-projects as well as necessary office-data entities (persons, contacts, partners, debitors, relations) **took 10-25 minutes**.
We could not find a pattern, why the import mostly took about 25 minutes, but sometimes took *just* 10 minutes. The impression that it had to do with too many other parallel processes, e.g. browser with BBB or IntelliJ IDEA was proved wrong, but stopping all unnecessary processes and performing the import again.
## Preparation
### Configuring PostgreSQL
The pg_stat_statements PostgreSQL-Extension can be used to measure how long queries take and how often they are called.
The module auto_explain can be used to automatically run EXPLAIN on long-running queries.
To use this extension and module, we extended the PostgreSQL-Docker-image:
```Dockerfile
FROM postgres:15.5-bookworm
RUN apt-get update && \
apt-get install -y postgresql-contrib && \
apt-get clean
COPY etc/postgresql-log-slow-queries.conf /etc/postgresql/postgresql.conf
```
And create an image from it:
```sh
docker build -t postgres-with-contrib:15.5-bookworm .
```
Then we created a config file for PostgreSQL in `etc/postgresql-log-slow-queries.conf`:
```
shared_preload_libraries = 'pg_stat_statements,auto_explain'
log_min_duration_statement = 1000
log_statement = 'all'
log_duration = on
pg_stat_statements.track = all
auto_explain.log_min_duration = '1s' # Logs queries taking longer than 1 second
auto_explain.log_analyze = on # Include actual run times
auto_explain.log_buffers = on # Include buffer usage statistics
auto_explain.log_format = 'json' # Format the log output in JSON
listen_addresses = '*'
```
And a Docker-Compose config in 'docker-compose.yml':
```
version: '3.8'
services:
postgres:
image: postgres-with-contrib:15.5-bookworm
container_name: custom-postgres
environment:
POSTGRES_PASSWORD: password
volumes:
- /home/mi/Projekte/Hostsharing/hsadmin-ng/etc/postgresql-log-slow-queries.conf:/etc/postgresql/postgresql.conf
ports:
- "5432:5432"
command:
- bash
- -c
- >
apt-get update &&
apt-get install -y postgresql-contrib &&
docker-entrypoint.sh postgres -c config_file=/etc/postgresql/postgresql.conf
```
### Activate the pg_stat_statements Extension
The pg_stat_statements extension was activated in our Liquibase-scripts:
```
create extension if not exists "pg_stat_statements";
```
### Running the Tweaked PostgreSQL
Now we can run PostgreSQL with activated slow-query-logging:
```shell
docker-compose up -d
```
### Running the Import
Using an environment like this:
```shell
export HSADMINNG_POSTGRES_JDBC_URL=jdbc:postgresql://localhost:5432/postgres
export HSADMINNG_POSTGRES_ADMIN_USERNAME=postgres
export HSADMINNG_POSTGRES_ADMIN_PASSWORD=password
export HSADMINNG_POSTGRES_RESTRICTED_USERNAME=restricted
export HSADMINNG_SUPERUSER=superuser-alex@hostsharing.net
```
We can now run the hosting-assets-import:
```shell
time gw-importHostingAssets
```
### Fetch the Query Statistics
And afterward we can query the statistics in PostgreSQL:
```SQL
SELECT pg_stat_statements_reset();
```
## Analysis Result
### RBAC-Access-Rights Detection query
This CTE query was run over 4000 times during a single import and takes in total the whole execution time of the import process:
```SQL
WITH RECURSIVE grants AS (
SELECT descendantUuid, ascendantUuid, $5 AS level
FROM RbacGrants
WHERE assumed
AND ascendantUuid = any(subjectIds)
UNION ALL
SELECT g.descendantUuid, g.ascendantUuid, grants.level + $6 AS level
FROM RbacGrants g
INNER JOIN grants ON grants.descendantUuid = g.ascendantUuid
WHERE g.assumed
),
granted AS (
SELECT DISTINCT descendantUuid
FROM grants
)
SELECT DISTINCT perm.objectUuid
FROM granted
JOIN RbacPermission perm ON granted.descendantUuid = perm.uuid
JOIN RbacObject obj ON obj.uuid = perm.objectUuid
WHERE (requiredOp = $7 OR perm.op = requiredOp)
AND obj.objectTable = forObjectTable
LIMIT maxObjects+$8
```
That query is used to determine access rights of the currently active RBAC-subject(s).
We used `EXPLAIN` with a concrete version (parameters substituted with real values) of that query and got this result:
```
QUERY PLAN
Limit (cost=6549.08..6549.35 rows=54 width=16)
CTE grants
-> Recursive Union (cost=4.32..5845.97 rows=1103 width=36)
-> Bitmap Heap Scan on rbacgrants (cost=4.32..15.84 rows=3 width=36)
Recheck Cond: (ascendantuuid = ANY ('{ad1133dc-fbb7-43c9-8c20-0da3f89a2388}'::uuid[]))
Filter: assumed
-> Bitmap Index Scan on rbacgrants_ascendantuuid_idx (cost=0.00..4.32 rows=3 width=0)
Index Cond: (ascendantuuid = ANY ('{ad1133dc-fbb7-43c9-8c20-0da3f89a2388}'::uuid[]))
-> Nested Loop (cost=0.29..580.81 rows=110 width=36)
-> WorkTable Scan on grants grants_1 (cost=0.00..0.60 rows=30 width=20)
-> Index Scan using rbacgrants_ascendantuuid_idx on rbacgrants g (cost=0.29..19.29 rows=4 width=32)
Index Cond: (ascendantuuid = grants_1.descendantuuid)
Filter: assumed
-> Unique (cost=703.11..703.38 rows=54 width=16)
-> Sort (cost=703.11..703.25 rows=54 width=16)
Sort Key: perm.objectuuid
-> Nested Loop (cost=31.60..701.56 rows=54 width=16)
-> Hash Join (cost=31.32..638.78 rows=200 width=16)
Hash Cond: (perm.uuid = grants.descendantuuid)
-> Seq Scan on rbacpermission perm (cost=0.00..532.92 rows=28392 width=32)
-> Hash (cost=28.82..28.82 rows=200 width=16)
-> HashAggregate (cost=24.82..26.82 rows=200 width=16)
Group Key: grants.descendantuuid
-> CTE Scan on grants (cost=0.00..22.06 rows=1103 width=16)
-> Index Only Scan using rbacobject_objecttable_uuid_key on rbacobject obj (cost=0.28..0.31 rows=1 width=16)
Index Cond: ((objecttable = 'hs_hosting_asset'::text) AND (uuid = perm.objectuuid))
```
### Office-Relation-Query
```SQL
SELECT hore1_0.uuid,a1_0.uuid,a1_0.familyname,a1_0.givenname,a1_0.persontype,a1_0.salutation,a1_0.title,a1_0.tradename,a1_0.version,c1_0.uuid,c1_0.caption,c1_0.emailaddresses,c1_0.phonenumbers,c1_0.postaladdress,c1_0.version,h1_0.uuid,h1_0.familyname,h1_0.givenname,h1_0.persontype,h1_0.salutation,h1_0.title,h1_0.tradename,h1_0.version,hore1_0.mark,hore1_0.type,hore1_0.version
FROM hs_office_relation_rv hore1_0
LEFT JOIN hs_office_person_rv a1_0 ON a1_0.uuid=hore1_0.anchoruuid
LEFT JOIN hs_office_contact_rv c1_0 ON c1_0.uuid=hore1_0.contactuuid
LEFT JOIN hs_office_person_rv h1_0 ON h1_0.uuid=hore1_0.holderuuid
WHERE hore1_0.uuid=$1
```
That query on the `hs_office_relation_rv`-table joins the three references anchor-person, holder-person and contact.
### Total-Query-Time > Total-Import-Runtime
That both queries total up to more than the runtime of the import-process is most likely due to internal parallel query processing.
## Attempts to Mitigate the Problem
### VACUUM ANALYZE
In the middle of the import, we updated the PostgreSQL statistics to recalibrate the query optimizer:
```SQL
VACUUM ANALYZE;
```
This did not improve the performance.
### Improving Joins + Indexes
We were suspicious about the sequential scan over all `rbacpermission` rows which was done by PostgreSQL to execute a HashJoin strategy. Turning off that strategy by
```SQL
ALTER FUNCTION queryAccessibleObjectUuidsOfSubjectIds SET enable_hashjoin = off;
```
did not improve the performance though. The HashJoin was actually still applied, but no full table scan anymore:
```
[...]
QUERY PLAN
-> Hash Join (cost=36.02..40.78 rows=1 width=16)
Hash Cond: (grants.descendantuuid = perm.uuid)
-> HashAggregate (cost=13.32..15.32 rows=200 width=16)
Group Key: grants.descendantuuid
-> CTE Scan on grants (cost=0.00..11.84 rows=592 width=16)
[...]
```
The HashJoin strategy could be great if the hash-map could be kept for multiple invocations. But during an import process, of course, there are always new rows in the underlying table and the hash-map would be outdated immediately.
Also creating indexes which should suppor the RBAC query, like the following, did not improve performance:
```SQL
create index on RbacPermission (objectUuid, op);
create index on RbacPermission (opTableName, op);
```
### LAZY loading for Relation.anchorPerson/.holderPerson/
At this point, the import took 21mins with these statistics:
| query | calls | total_m | mean_ms |
|-------|-------|---------|---------|
| select hore1_0.uuid,a1_0.uuid,a1_0.familyname,a1_0.givenname,a1_0.persontype,a1_0.salutation,a1_0.title,a1_0.tradename,a1_0.version,c1_0.uuid,c1_0.caption,c1_0.emailaddresses,c1_0.phonenumbers,c1_0.postaladdress, c1_0.version,h1_0.uuid,h1_0.familyname,h1_0.givenname,h1_0.persontype,h1_0.salutation,h1_0.title,h1_0.tradename,h1_0.version,hore1_0.mark,hore1_0.type,hore1_0.version from public.hs_office_relation_rv hore1_0 left join public.hs_office_person_rv a1_0 on a1_0.uuid=hore1_0.anchoruuid left join public.hs_office_contact_rv c1_0 on c1_0.uuid=hore1_0.contactuuid left join public.hs_office_person_rv h1_0 on h1_0.uuid=hore1_0.holderuuid where hore1_0.uuid=$1 | 517 | 11 | 1282 |
| select hope1_0.uuid,hope1_0.familyname,hope1_0.givenname,hope1_0.persontype,hope1_0.salutation,hope1_0.title,hope1_0.tradename,hope1_0.version from public.hs_office_person_rv hope1_0 where hope1_0.uuid=$1 | 973 | 4 | 254 |
| select hoce1_0.uuid,hoce1_0.caption,hoce1_0.emailaddresses,hoce1_0.phonenumbers,hoce1_0.postaladdress,hoce1_0.version from public.hs_office_contact_rv hoce1_0 where hoce1_0.uuid=$1 | 973 | 4 | 253 |
| call grantRoleToRole(roleUuid, superRoleUuid, superRoleDesc.assumed) | 31316 | 0 | 1 |
| call buildRbacSystemForHsHostingAsset(NEW) | 2258 | 0 | 7 |
| select * from isGranted(array[granteeId], grantedId) | 44613 | 0 | 0 |
| insert into public.hs_hosting_asset_rv (alarmcontactuuid,assignedtoassetuuid,bookingitemuuid,caption,config,identifier,parentassetuuid,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) | 2207 | 0 | 7 |
| insert into hs_hosting_asset (alarmcontactuuid, version, bookingitemuuid, type, parentassetuuid, assignedtoassetuuid, config, uuid, identifier, caption) values (new.alarmcontactuuid, new. version, new. bookingitemuuid, new. type, new. parentassetuuid, new. assignedtoassetuuid, new. config, new. uuid, new. identifier, new. caption) returning * | 2207 | 0 | 7 |
| insert into public.hs_office_relation_rv (anchoruuid,contactuuid,holderuuid,mark,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7) | 1261 | 0 | 9 |
| insert into hs_office_relation (uuid, version, anchoruuid, holderuuid, contactuuid, type, mark) values (new.uuid, new. version, new. anchoruuid, new. holderuuid, new. contactuuid, new. type, new. mark) returning * | 1261 | 0 | 9 |
| call buildRbacSystemForHsOfficeRelation(NEW) | 1276 | 0 | 8 |
| with recursive grants as ( select descendantUuid, ascendantUuid from RbacGrants where descendantUuid = grantedId union all select ""grant"".descendantUuid, ""grant"".ascendantUuid from RbacGrants ""grant"" inner join grants recur on recur.ascendantUuid = ""grant"".descendantUuid ) select exists ( select $3 from grants where ascendantUuid = any(granteeIds) ) or grantedId = any(granteeIds) | 47540 | 0 | 0 |
| insert into RbacGrants (grantedByTriggerOf, ascendantuuid, descendantUuid, assumed) values (currentTriggerObjectUuid(), superRoleId, subRoleId, doAssume) on conflict do nothing" | 40472 | 0 | 0 |
| insert into public.hs_booking_item_rv (caption,parentitemuuid,projectuuid,resources,type,validity,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8) | 926 | 0 | 7 |
| insert into hs_booking_item (resources, version, projectuuid, type, parentitemuuid, validity, uuid, caption) values (new.resources, new. version, new. projectuuid, new. type, new. parentitemuuid, new. validity, new. uuid, new. caption) returning * | 926 | 0 | 7 |
The slowest query now was fetching Relations joined with Contact, Anchor-Person and Holder-Person, for all tables using the restricted (RBAC) views (_rv).
We changed these mappings from `EAGER` (default) to `LAZY` to `@ManyToOne(fetch = FetchType.LAZY)` and got this result:
| query | calls | total (min) | mean (ms) |
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------|-------------|----------|
| select hope1_0.uuid,hope1_0.familyname,hope1_0.givenname,hope1_0.persontype,hope1_0.salutation,hope1_0.title,hope1_0.tradename,hope1_0.version from public.hs_office_person_rv hope1_0 where hope1_0.uuid=$1 | 1015 | 4 | 238 |
| select hore1_0.uuid,hore1_0.anchoruuid,hore1_0.contactuuid,hore1_0.holderuuid,hore1_0.mark,hore1_0.type,hore1_0.version from public.hs_office_relation_rv hore1_0 where hore1_0.uuid=$1 | 517 | 4 | 439 |
| select hoce1_0.uuid,hoce1_0.caption,hoce1_0.emailaddresses,hoce1_0.phonenumbers,hoce1_0.postaladdress,hoce1_0.version from public.hs_office_contact_rv hoce1_0 where hoce1_0.uuid=$1 | 497 | 2 | 213 |
| call grantRoleToRole(roleUuid, superRoleUuid, superRoleDesc.assumed) | 31316 | 0 | 1 |
| select * from isGranted(array[granteeId], grantedId) | 44613 | 0 | 0 |
| call buildRbacSystemForHsHostingAsset(NEW) | 2258 | 0 | 7 |
| insert into public.hs_hosting_asset_rv (alarmcontactuuid,assignedtoassetuuid,bookingitemuuid,caption,config,identifier,parentassetuuid,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) | 2207 | 0 | 7 |
| insert into hs_hosting_asset (alarmcontactuuid, version, bookingitemuuid, type, parentassetuuid, assignedtoassetuuid, config, uuid, identifier, caption) values (new.alarmcontactuuid, new. version, new. bookingitemuuid, new. type, new. parentassetuuid, new. assignedtoassetuuid, new. config, new. uuid, new. identifier, new. caption) returning * | 2207 | 0 | 7 |
| with recursive grants as ( select descendantUuid, ascendantUuid from RbacGrants where descendantUuid = grantedId union all select ""grant"".descendantUuid, ""grant"".ascendantUuid from RbacGrants ""grant"" inner join grants recur on recur.ascendantUuid = ""grant"".descendantUuid ) select exists ( select $3 from grants where ascendantUuid = any(granteeIds) ) or grantedId = any(granteeIds) | 47538 | 0 | 0 |
insert into public.hs_office_relation_rv (anchoruuid,contactuuid,holderuuid,mark,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7) | 1261 | 0 | 8 |
| insert into hs_office_relation (uuid, version, anchoruuid, holderuuid, contactuuid, type, mark) values (new.uuid, new. version, new. anchoruuid, new. holderuuid, new. contactuuid, new. type, new. mark) returning * | 1261 | 0 | 8 |
| call buildRbacSystemForHsOfficeRelation(NEW) | 1276 | 0 | 7 |
| insert into public.hs_booking_item_rv (caption,parentitemuuid,projectuuid,resources,type,validity,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8) | 926 | 0 | 7 |
| insert into hs_booking_item (resources, version, projectuuid, type, parentitemuuid, validity, uuid, caption) values (new.resources, new. version, new. projectuuid, new. type, new. parentitemuuid, new. validity, new. uuid, new. caption) returning * | 926 | 0 | 7 |
insert into RbacGrants (grantedByTriggerOf, ascendantuuid, descendantUuid, assumed) values (currentTriggerObjectUuid(), superRoleId, subRoleId, doAssume) on conflict do nothing | 40472 | 0 | 0 |
Now, finally, the total runtime of the import was down to 12 minutes. This is repeatable, where originally, the import took about 25mins in most cases and just rarely - and for unknown reasons - 10min.
## Summary
That the import runtime is down to about 12min is repeatable, where originally, the import took about 25mins in most cases and just rarely - and for unknown reasons - just 10min.
Merging the recursive CTE query to determine the RBAC SELECT-permission, made it more clear which business-queries take the time.
Avoiding EAGER-loading where not neccessary, reduced the total runtime of the import to about the half.

19
docker-compose.yml Normal file
View File

@ -0,0 +1,19 @@
version: '3.8'
services:
postgres:
image: postgres-with-contrib:15.5-bookworm
container_name: custom-postgres
environment:
POSTGRES_PASSWORD: password
volumes:
- /home/mi/Projekte/Hostsharing/hsadmin-ng/etc/postgresql-log-slow-queries.conf:/etc/postgresql/postgresql.conf
ports:
- "5432:5432"
command:
- bash
- -c
- >
apt-get update &&
apt-get install -y postgresql-contrib &&
docker-entrypoint.sh postgres -c config_file=/etc/postgresql/postgresql.conf

View File

@ -0,0 +1,10 @@
shared_preload_libraries = 'pg_stat_statements,auto_explain'
log_min_duration_statement = 1000
log_statement = 'all'
log_duration = on
pg_stat_statements.track = all
auto_explain.log_min_duration = '1s' # Logs queries taking longer than 1 second
auto_explain.log_analyze = on # Include actual run times
auto_explain.log_buffers = on # Include buffer usage statistics
auto_explain.log_format = 'json' # Format the log output in JSON
listen_addresses = '*'

View File

@ -70,7 +70,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class HsBookingItemEntity implements Stringifyable, RbacObject, PropertiesProvider { public class HsBookingItemEntity implements Stringifyable, RbacObject<HsBookingItemEntity>, PropertiesProvider {
private static Stringify<HsBookingItemEntity> stringify = stringify(HsBookingItemEntity.class) private static Stringify<HsBookingItemEntity> stringify = stringify(HsBookingItemEntity.class)
.withProp(HsBookingItemEntity::getProject) .withProp(HsBookingItemEntity::getProject)

View File

@ -34,7 +34,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class HsBookingProjectEntity implements Stringifyable, RbacObject { public class HsBookingProjectEntity implements Stringifyable, RbacObject<HsBookingProjectEntity> {
private static Stringify<HsBookingProjectEntity> stringify = stringify(HsBookingProjectEntity.class) private static Stringify<HsBookingProjectEntity> stringify = stringify(HsBookingProjectEntity.class)
.withProp(HsBookingProjectEntity::getDebitor) .withProp(HsBookingProjectEntity::getDebitor)

View File

@ -70,7 +70,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class HsHostingAssetEntity implements Stringifyable, RbacObject, PropertiesProvider { public class HsHostingAssetEntity implements Stringifyable, RbacObject<HsHostingAssetEntity>, PropertiesProvider {
private static Stringify<HsHostingAssetEntity> stringify = stringify(HsHostingAssetEntity.class) private static Stringify<HsHostingAssetEntity> stringify = stringify(HsHostingAssetEntity.class)
.withProp(HsHostingAssetEntity::getType) .withProp(HsHostingAssetEntity::getType)

View File

@ -27,7 +27,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@AllArgsConstructor @AllArgsConstructor
@FieldNameConstants @FieldNameConstants
@DisplayName("BankAccount") @DisplayName("BankAccount")
public class HsOfficeBankAccountEntity implements RbacObject, Stringifyable { public class HsOfficeBankAccountEntity implements RbacObject<HsOfficeBankAccountEntity>, Stringifyable {
private static Stringify<HsOfficeBankAccountEntity> toString = stringify(HsOfficeBankAccountEntity.class, "bankAccount") private static Stringify<HsOfficeBankAccountEntity> toString = stringify(HsOfficeBankAccountEntity.class, "bankAccount")
.withIdProp(HsOfficeBankAccountEntity::getIban) .withIdProp(HsOfficeBankAccountEntity::getIban)

View File

@ -35,7 +35,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@AllArgsConstructor @AllArgsConstructor
@FieldNameConstants @FieldNameConstants
@DisplayName("Contact") @DisplayName("Contact")
public class HsOfficeContactEntity implements Stringifyable, RbacObject { public class HsOfficeContactEntity implements Stringifyable, RbacObject<HsOfficeContactEntity> {
private static Stringify<HsOfficeContactEntity> toString = stringify(HsOfficeContactEntity.class, "contact") private static Stringify<HsOfficeContactEntity> toString = stringify(HsOfficeContactEntity.class, "contact")
.withProp(Fields.caption, HsOfficeContactEntity::getCaption) .withProp(Fields.caption, HsOfficeContactEntity::getCaption)

View File

@ -41,7 +41,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@DisplayName("CoopAssetsTransaction") @DisplayName("CoopAssetsTransaction")
public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, RbacObject { public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, RbacObject<HsOfficeCoopAssetsTransactionEntity> {
private static Stringify<HsOfficeCoopAssetsTransactionEntity> stringify = stringify(HsOfficeCoopAssetsTransactionEntity.class) private static Stringify<HsOfficeCoopAssetsTransactionEntity> stringify = stringify(HsOfficeCoopAssetsTransactionEntity.class)
.withIdProp(HsOfficeCoopAssetsTransactionEntity::getTaggedMemberNumber) .withIdProp(HsOfficeCoopAssetsTransactionEntity::getTaggedMemberNumber)
@ -105,6 +105,13 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, RbacO
@OneToOne(mappedBy = "adjustedAssetTx") @OneToOne(mappedBy = "adjustedAssetTx")
private HsOfficeCoopAssetsTransactionEntity adjustmentAssetTx; private HsOfficeCoopAssetsTransactionEntity adjustmentAssetTx;
@Override
public HsOfficeCoopAssetsTransactionEntity load() {
RbacObject.super.load();
membership.load();
return this;
}
public String getTaggedMemberNumber() { public String getTaggedMemberNumber() {
return ofNullable(membership).map(HsOfficeMembershipEntity::toShortString).orElse("M-???????"); return ofNullable(membership).map(HsOfficeMembershipEntity::toShortString).orElse("M-???????");
} }

View File

@ -39,7 +39,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@DisplayName("CoopShareTransaction") @DisplayName("CoopShareTransaction")
public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, RbacObject { public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, RbacObject<HsOfficeCoopSharesTransactionEntity> {
private static Stringify<HsOfficeCoopSharesTransactionEntity> stringify = stringify(HsOfficeCoopSharesTransactionEntity.class) private static Stringify<HsOfficeCoopSharesTransactionEntity> stringify = stringify(HsOfficeCoopSharesTransactionEntity.class)
.withIdProp(HsOfficeCoopSharesTransactionEntity::getMemberNumberTagged) .withIdProp(HsOfficeCoopSharesTransactionEntity::getMemberNumberTagged)
@ -102,6 +102,13 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, RbacO
@OneToOne(mappedBy = "adjustedShareTx") @OneToOne(mappedBy = "adjustedShareTx")
private HsOfficeCoopSharesTransactionEntity adjustmentShareTx; private HsOfficeCoopSharesTransactionEntity adjustmentShareTx;
@Override
public HsOfficeCoopSharesTransactionEntity load() {
RbacObject.super.load();
membership.load();
return this;
}
@Override @Override
public String toString() { public String toString() {
return stringify.apply(this); return stringify.apply(this);

View File

@ -21,6 +21,7 @@ import org.hibernate.annotations.NotFoundAction;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumn;
@ -57,7 +58,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@DisplayName("Debitor") @DisplayName("Debitor")
public class HsOfficeDebitorEntity implements RbacObject, Stringifyable { public class HsOfficeDebitorEntity implements RbacObject<HsOfficeDebitorEntity>, Stringifyable {
public static final String DEBITOR_NUMBER_TAG = "D-"; public static final String DEBITOR_NUMBER_TAG = "D-";
public static final String TWO_DECIMAL_DIGITS = "^([0-9]{2})$"; public static final String TWO_DECIMAL_DIGITS = "^([0-9]{2})$";
@ -77,7 +78,7 @@ public class HsOfficeDebitorEntity implements RbacObject, Stringifyable {
@Version @Version
private int version; private int version;
@ManyToOne @ManyToOne(fetch = FetchType.LAZY)
@JoinFormula( @JoinFormula(
referencedColumnName = "uuid", referencedColumnName = "uuid",
value = """ value = """
@ -91,14 +92,14 @@ public class HsOfficeDebitorEntity implements RbacObject, Stringifyable {
WHERE pRel.holderUuid = dRel.anchorUuid WHERE pRel.holderUuid = dRel.anchorUuid
) )
""") """)
@NotFound(action = NotFoundAction.IGNORE) @NotFound(action = NotFoundAction.IGNORE) // TODO.impl: map a simplified raw-PartnerEntity, just for the partner-number
private HsOfficePartnerEntity partner; private HsOfficePartnerEntity partner;
@Column(name = "debitornumbersuffix", length = 2) @Column(name = "debitornumbersuffix", length = 2)
@Pattern(regexp = TWO_DECIMAL_DIGITS) @Pattern(regexp = TWO_DECIMAL_DIGITS)
private String debitorNumberSuffix; private String debitorNumberSuffix;
@ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = false) @ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "debitorreluuid", nullable = false) @JoinColumn(name = "debitorreluuid", nullable = false)
private HsOfficeRelationEntity debitorRel; private HsOfficeRelationEntity debitorRel;
@ -117,13 +118,27 @@ public class HsOfficeDebitorEntity implements RbacObject, Stringifyable {
@Column(name = "vatreversecharge") @Column(name = "vatreversecharge")
private boolean vatReverseCharge; private boolean vatReverseCharge;
@ManyToOne @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "refundbankaccountuuid") @JoinColumn(name = "refundbankaccountuuid")
@NotFound(action = NotFoundAction.IGNORE)
private HsOfficeBankAccountEntity refundBankAccount; private HsOfficeBankAccountEntity refundBankAccount;
@Column(name = "defaultprefix", columnDefinition = "char(3) not null") @Column(name = "defaultprefix", columnDefinition = "char(3) not null")
private String defaultPrefix; private String defaultPrefix;
@Override
public HsOfficeDebitorEntity load() {
RbacObject.super.load();
if (partner != null) {
partner.load();
}
debitorRel.load();
if (refundBankAccount != null) {
refundBankAccount.load();
}
return this;
}
private String getDebitorNumberString() { private String getDebitorNumberString() {
return ofNullable(partner) return ofNullable(partner)
.filter(partner -> debitorNumberSuffix != null) .filter(partner -> debitorNumberSuffix != null)

View File

@ -21,6 +21,7 @@ import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.EnumType; import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated; import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumn;
@ -61,7 +62,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@DisplayName("Membership") @DisplayName("Membership")
public class HsOfficeMembershipEntity implements RbacObject, Stringifyable { public class HsOfficeMembershipEntity implements RbacObject<HsOfficeMembershipEntity>, Stringifyable {
public static final String MEMBER_NUMBER_TAG = "M-"; public static final String MEMBER_NUMBER_TAG = "M-";
public static final String TWO_DECIMAL_DIGITS = "^([0-9]{2})$"; public static final String TWO_DECIMAL_DIGITS = "^([0-9]{2})$";
@ -80,7 +81,7 @@ public class HsOfficeMembershipEntity implements RbacObject, Stringifyable {
@Version @Version
private int version; private int version;
@ManyToOne @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "partneruuid") @JoinColumn(name = "partneruuid")
private HsOfficePartnerEntity partner; private HsOfficePartnerEntity partner;
@ -99,6 +100,13 @@ public class HsOfficeMembershipEntity implements RbacObject, Stringifyable {
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
private HsOfficeMembershipStatus status; private HsOfficeMembershipStatus status;
@Override
public HsOfficeMembershipEntity load() {
RbacObject.super.load();
partner.load();
return this;
}
public void setValidFrom(final LocalDate validFrom) { public void setValidFrom(final LocalDate validFrom) {
setValidity(toPostgresDateRange(validFrom, getValidTo())); setValidity(toPostgresDateRange(validFrom, getValidTo()));
} }

View File

@ -26,7 +26,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@DisplayName("PartnerDetails") @DisplayName("PartnerDetails")
public class HsOfficePartnerDetailsEntity implements RbacObject, Stringifyable { public class HsOfficePartnerDetailsEntity implements RbacObject<HsOfficePartnerDetailsEntity>, Stringifyable {
private static Stringify<HsOfficePartnerDetailsEntity> stringify = stringify( private static Stringify<HsOfficePartnerDetailsEntity> stringify = stringify(
HsOfficePartnerDetailsEntity.class, HsOfficePartnerDetailsEntity.class,

View File

@ -40,7 +40,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@DisplayName("Partner") @DisplayName("Partner")
public class HsOfficePartnerEntity implements Stringifyable, RbacObject { public class HsOfficePartnerEntity implements Stringifyable, RbacObject<HsOfficePartnerEntity> {
public static final String PARTNER_NUMBER_TAG = "P-"; public static final String PARTNER_NUMBER_TAG = "P-";
@ -66,15 +66,23 @@ public class HsOfficePartnerEntity implements Stringifyable, RbacObject {
@Column(name = "partnernumber", columnDefinition = "numeric(5) not null") @Column(name = "partnernumber", columnDefinition = "numeric(5) not null")
private Integer partnerNumber; private Integer partnerNumber;
@ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = false) @ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "partnerreluuid", nullable = false) @JoinColumn(name = "partnerreluuid", nullable = false)
private HsOfficeRelationEntity partnerRel; private HsOfficeRelationEntity partnerRel;
@ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = true) @ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = true, fetch = FetchType.LAZY)
@JoinColumn(name = "detailsuuid") @JoinColumn(name = "detailsuuid")
@NotFound(action = NotFoundAction.IGNORE) @NotFound(action = NotFoundAction.IGNORE)
private HsOfficePartnerDetailsEntity details; private HsOfficePartnerDetailsEntity details;
@Override
public HsOfficePartnerEntity load() {
RbacObject.super.load();
partnerRel.load();
details.load();
return this;
}
public String getTaggedPartnerNumber() { public String getTaggedPartnerNumber() {
return PARTNER_NUMBER_TAG + partnerNumber; return PARTNER_NUMBER_TAG + partnerNumber;
} }

View File

@ -30,7 +30,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@AllArgsConstructor @AllArgsConstructor
@FieldNameConstants @FieldNameConstants
@DisplayName("Person") @DisplayName("Person")
public class HsOfficePersonEntity implements RbacObject, Stringifyable { public class HsOfficePersonEntity implements RbacObject<HsOfficePersonEntity>, Stringifyable {
private static Stringify<HsOfficePersonEntity> toString = stringify(HsOfficePersonEntity.class, "person") private static Stringify<HsOfficePersonEntity> toString = stringify(HsOfficePersonEntity.class, "person")
.withProp(Fields.personType, HsOfficePersonEntity::getPersonType) .withProp(Fields.personType, HsOfficePersonEntity::getPersonType)

View File

@ -56,15 +56,15 @@ public class HsOfficeRelationEntity implements RbacObject, Stringifyable {
@Version @Version
private int version; private int version;
@ManyToOne @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "anchoruuid") @JoinColumn(name = "anchoruuid")
private HsOfficePersonEntity anchor; private HsOfficePersonEntity anchor;
@ManyToOne @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "holderuuid") @JoinColumn(name = "holderuuid")
private HsOfficePersonEntity holder; private HsOfficePersonEntity holder;
@ManyToOne @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "contactuuid") @JoinColumn(name = "contactuuid")
private HsOfficeContactEntity contact; private HsOfficeContactEntity contact;
@ -75,6 +75,15 @@ public class HsOfficeRelationEntity implements RbacObject, Stringifyable {
@Column(name = "mark") @Column(name = "mark")
private String mark; private String mark;
@Override
public HsOfficeRelationEntity load() {
RbacObject.super.load();
anchor.load();
holder.load();
contact.load();
return this;
}
@Override @Override
public String toString() { public String toString() {
return toString.apply(this); return toString.apply(this);

View File

@ -40,7 +40,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@DisplayName("SEPA-Mandate") @DisplayName("SEPA-Mandate")
public class HsOfficeSepaMandateEntity implements Stringifyable, RbacObject { public class HsOfficeSepaMandateEntity implements Stringifyable, RbacObject<HsOfficeSepaMandateEntity> {
private static Stringify<HsOfficeSepaMandateEntity> stringify = stringify(HsOfficeSepaMandateEntity.class) private static Stringify<HsOfficeSepaMandateEntity> stringify = stringify(HsOfficeSepaMandateEntity.class)
.withProp(e -> e.getBankAccount().getIban()) .withProp(e -> e.getBankAccount().getIban())

View File

@ -1,10 +1,18 @@
package net.hostsharing.hsadminng.rbac.rbacobject; package net.hostsharing.hsadminng.rbac.rbacobject;
import org.hibernate.Hibernate;
import java.util.UUID; import java.util.UUID;
public interface RbacObject { public interface RbacObject<T extends RbacObject<?>> {
UUID getUuid(); UUID getUuid();
int getVersion(); int getVersion();
default T load() {
Hibernate.initialize(this);
//noinspection unchecked
return (T) this;
};
} }

View File

@ -24,7 +24,7 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class TestCustomerEntity implements RbacObject { public class TestCustomerEntity implements RbacObject<TestCustomerEntity> {
@Id @Id
@GeneratedValue @GeneratedValue

View File

@ -27,7 +27,7 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class TestDomainEntity implements RbacObject { public class TestDomainEntity implements RbacObject<TestDomainEntity> {
@Id @Id
@GeneratedValue @GeneratedValue

View File

@ -27,7 +27,7 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class TestPackageEntity implements RbacObject { public class TestPackageEntity implements RbacObject<TestPackageEntity> {
@Id @Id
@GeneratedValue @GeneratedValue

View File

@ -0,0 +1,13 @@
--liquibase formatted sql
-- ============================================================================
-- PG-STAT-STATEMENTS-EXTENSION
--changeset pg-stat-statements-extension:1 context:pg_stat_statements endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Makes improved uuid generation available.
*/
create extension if not exists "pg_stat_statements";
--//

View File

@ -372,6 +372,9 @@ create table RbacPermission
op RbacOp not null, op RbacOp not null,
opTableName varchar(60) opTableName varchar(60)
); );
-- TODO.perf: check if these indexes are really useful
create index on RbacPermission (objectUuid, op);
create index on RbacPermission (opTableName, op);
ALTER TABLE RbacPermission ALTER TABLE RbacPermission
ADD CONSTRAINT RbacPermission_uc UNIQUE NULLS NOT DISTINCT (objectUuid, op, opTableName); ADD CONSTRAINT RbacPermission_uc UNIQUE NULLS NOT DISTINCT (objectUuid, op, opTableName);
@ -495,78 +498,68 @@ create index on RbacGrants (ascendantUuid);
create index on RbacGrants (descendantUuid); create index on RbacGrants (descendantUuid);
call create_journal('RbacGrants'); call create_journal('RbacGrants');
create or replace function findGrantees(grantedId uuid) create or replace function findGrantees(grantedId uuid)
returns setof RbacReference returns setof RbacReference
returns null on null input returns null on null input
language sql as $$ language sql as $$
select reference.* with recursive grants as (
from (with recursive grants as (select descendantUuid, select descendantUuid, ascendantUuid
ascendantUuid from RbacGrants
from RbacGrants where descendantUuid = grantedId
where descendantUuid = grantedId union all
union all select g.descendantUuid, g.ascendantUuid
select "grant".descendantUuid, from RbacGrants g
"grant".ascendantUuid inner join grants on grants.ascendantUuid = g.descendantUuid
from RbacGrants "grant" )
inner join grants recur on recur.ascendantUuid = "grant".descendantUuid) select ref.*
select ascendantUuid from grants
from grants) as grantee join RbacReference ref on ref.uuid = grants.ascendantUuid;
join RbacReference reference on reference.uuid = grantee.ascendantUuid; $$;
create or replace function isGranted(granteeIds uuid[], grantedId uuid)
returns bool
returns null on null input
language sql as $$
with recursive grants as (
select descendantUuid, ascendantUuid
from RbacGrants
where descendantUuid = grantedId
union all
select "grant".descendantUuid, "grant".ascendantUuid
from RbacGrants "grant"
inner join grants recur on recur.ascendantUuid = "grant".descendantUuid
)
select exists (
select true
from grants
where ascendantUuid = any(granteeIds)
) or grantedId = any(granteeIds);
$$; $$;
create or replace function isGranted(granteeId uuid, grantedId uuid) create or replace function isGranted(granteeId uuid, grantedId uuid)
returns bool returns bool
returns null on null input returns null on null input
language sql as $$ language sql as $$
select granteeId = grantedId or granteeId in (with recursive grants as (select descendantUuid, ascendantUuid select * from isGranted(array[granteeId], grantedId);
from RbacGrants
where descendantUuid = grantedId
union all
select "grant".descendantUuid, "grant".ascendantUuid
from RbacGrants "grant"
inner join grants recur on recur.ascendantUuid = "grant".descendantUuid)
select ascendantUuid
from grants);
$$; $$;
create or replace function isGranted(granteeIds uuid[], grantedId uuid)
returns bool
returns null on null input
language plpgsql as $$
declare
granteeId uuid;
begin
-- TODO.perf: needs optimization
foreach granteeId in array granteeIds
loop
if isGranted(granteeId, grantedId) then
return true;
end if;
end loop;
return false;
end; $$;
create or replace function isPermissionGrantedToSubject(permissionId uuid, subjectId uuid) create or replace function isPermissionGrantedToSubject(permissionId uuid, subjectId uuid)
returns BOOL returns BOOL
stable -- leakproof stable -- leakproof
language sql as $$ language sql as $$
with recursive grants as (
select descendantUuid, ascendantUuid
from RbacGrants
where descendantUuid = permissionId
union all
select g.descendantUuid, g.ascendantUuid
from RbacGrants g
inner join grants on grants.ascendantUuid = g.descendantUuid
)
select exists( select exists(
select * select true
from RbacUser from grants
where uuid in (with recursive grants as (select descendantUuid, where ascendantUuid = subjectId
ascendantUuid );
from RbacGrants g
where g.descendantUuid = permissionId
union all
select g.descendantUuid,
g.ascendantUuid
from RbacGrants g
inner join grants recur on recur.ascendantUuid = g.descendantUuid)
select ascendantUuid
from grants
where ascendantUuid = subjectId)
);
$$; $$;
create or replace function hasInsertPermission(objectUuid uuid, tableName text ) create or replace function hasInsertPermission(objectUuid uuid, tableName text )
@ -708,14 +701,14 @@ begin
end; $$; end; $$;
-- ============================================================================ -- ============================================================================
--changeset rbac-base-QUERY-ACCESSIBLE-OBJECT-UUIDS:1 endDelimiter:--// --changeset rbac-base-QUERY-ACCESSIBLE-OBJECT-UUIDS:1 runOnChange=true endDelimiter:--//
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
/* /*
*/ */
create or replace function queryAccessibleObjectUuidsOfSubjectIds( create or replace function queryAccessibleObjectUuidsOfSubjectIds(
requiredOp RbacOp, requiredOp RbacOp,
forObjectTable varchar, -- reduces the result set, but is not really faster when used in restricted view forObjectTable varchar,
subjectIds uuid[], subjectIds uuid[],
maxObjects integer = 8000) maxObjects integer = 8000)
returns setof uuid returns setof uuid
@ -724,23 +717,29 @@ create or replace function queryAccessibleObjectUuidsOfSubjectIds(
declare declare
foundRows bigint; foundRows bigint;
begin begin
return query select distinct perm.objectUuid return query
from (with recursive grants as (select descendantUuid, ascendantUuid, 1 as level WITH RECURSIVE grants AS (
from RbacGrants SELECT descendantUuid, ascendantUuid, 1 AS level
where assumed FROM RbacGrants
and ascendantUuid = any (subjectIds) WHERE assumed
union AND ascendantUuid = any(subjectIds)
distinct UNION ALL
select "grant".descendantUuid, "grant".ascendantUuid, level + 1 as level SELECT g.descendantUuid, g.ascendantUuid, grants.level + 1 AS level
from RbacGrants "grant" FROM RbacGrants g
inner join grants recur on recur.descendantUuid = "grant".ascendantUuid INNER JOIN grants ON grants.descendantUuid = g.ascendantUuid
where assumed) WHERE g.assumed
select descendantUuid ),
from grants) as granted granted AS (
join RbacPermission perm SELECT DISTINCT descendantUuid
on granted.descendantUuid = perm.uuid and (requiredOp = 'SELECT' or perm.op = requiredOp) FROM grants
join RbacObject obj on obj.uuid = perm.objectUuid and obj.objectTable = forObjectTable )
limit maxObjects + 1; SELECT DISTINCT perm.objectUuid
FROM granted
JOIN RbacPermission perm ON granted.descendantUuid = perm.uuid
JOIN RbacObject obj ON obj.uuid = perm.objectUuid
WHERE (requiredOp = 'SELECT' OR perm.op = requiredOp)
AND obj.objectTable = forObjectTable
LIMIT maxObjects+1;
foundRows = lastRowCount(); foundRows = lastRowCount();
if foundRows > maxObjects then if foundRows > maxObjects then
@ -751,7 +750,6 @@ begin
end if; end if;
end; end;
$$; $$;
--// --//
-- ============================================================================ -- ============================================================================
@ -764,24 +762,23 @@ create or replace function queryPermissionsGrantedToSubjectId(subjectId uuid)
returns setof RbacPermission returns setof RbacPermission
strict strict
language sql as $$ language sql as $$
-- @formatter:off with recursive grants as (
select * select descendantUuid, ascendantUuid
from RbacPermission from RbacGrants
where uuid in ( where ascendantUuid = subjectId
with recursive grants as ( union all
select distinct descendantUuid, ascendantUuid select g.descendantUuid, g.ascendantUuid
from RbacGrants from RbacGrants g
where ascendantUuid = subjectId inner join grants on grants.descendantUuid = g.ascendantUuid
union all )
select "grant".descendantUuid, "grant".ascendantUuid select perm.*
from RbacGrants "grant" from RbacPermission perm
inner join grants recur on recur.descendantUuid = "grant".ascendantUuid where perm.uuid in (
) select descendantUuid
select descendantUuid from grants
from grants );
);
-- @formatter:on
$$; $$;
--// --//
-- ============================================================================ -- ============================================================================

View File

@ -175,16 +175,38 @@ begin
Creates a restricted view based on the 'SELECT' permission of the current subject. Creates a restricted view based on the 'SELECT' permission of the current subject.
*/ */
sql := format($sql$ sql := format($sql$
set session session authorization default; create or replace view %1$s_rv as
create view %1$s_rv as with accessible_%1$s_uuids as (
with accessibleObjects as (
select queryAccessibleObjectUuidsOfSubjectIds('SELECT', '%1$s', currentSubjectsUuids()) with recursive grants as (
select descendantUuid, ascendantUuid, 1 as level
from RbacGrants
where assumed
and ascendantUuid = any (currentSubjectsuUids())
union all
select g.descendantUuid, g.ascendantUuid, level + 1 as level
from RbacGrants g
inner join grants on grants.descendantUuid = g.ascendantUuid
where g.assumed
),
granted as (
select distinct descendantUuid
from grants
)
select distinct perm.objectUuid as objectUuid
from granted
join RbacPermission perm on granted.descendantUuid = perm.uuid
join RbacObject obj on obj.uuid = perm.objectUuid
where perm.op = 'SELECT'
and obj.objectTable = '%1$s'
limit 8001
) )
select target.* select target.*
from %1$s as target from %1$s as target
where target.uuid in (select * from accessibleObjects) where target.uuid in (select * from accessible_%1$s_uuids)
order by %2$s; order by %2$s;
grant all privileges on %1$s_rv to ${HSADMINNG_POSTGRES_RESTRICTED_USERNAME};
grant all privileges on %1$s_rv to ${HSADMINNG_POSTGRES_RESTRICTED_USERNAME};
$sql$, targetTable, orderBy); $sql$, targetTable, orderBy);
execute sql; execute sql;

View File

@ -21,6 +21,8 @@ databaseChangeLog:
file: db/changelog/0-basis/010-context.sql file: db/changelog/0-basis/010-context.sql
- include: - include:
file: db/changelog/0-basis/020-audit-log.sql file: db/changelog/0-basis/020-audit-log.sql
- include:
file: db/changelog/0-basis/090-log-slow-queries-extensions.sql
- include: - include:
file: db/changelog/1-rbac/1050-rbac-base.sql file: db/changelog/1-rbac/1050-rbac-base.sql
- include: - include:

View File

@ -287,7 +287,10 @@ class HsOfficeBankAccountControllerAcceptanceTest extends ContextBasedTestWithCl
.statusCode(204); // @formatter:on .statusCode(204); // @formatter:on
// then the given bankaccount is still there // then the given bankaccount is still there
assertThat(bankAccountRepo.findByUuid(givenBankAccount.getUuid())).isEmpty(); jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net", null);
assertThat(bankAccountRepo.findByUuid(givenBankAccount.getUuid())).isEmpty();
}).assertSuccessful();
} }
@Test @Test

View File

@ -123,7 +123,7 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTestWithC
private void assertThatBankAccountIsPersisted(final HsOfficeBankAccountEntity saved) { private void assertThatBankAccountIsPersisted(final HsOfficeBankAccountEntity saved) {
final var found = bankAccountRepo.findByUuid(saved.getUuid()); final var found = bankAccountRepo.findByUuid(saved.getUuid());
assertThat(found).isNotEmpty().get().usingRecursiveComparison().isEqualTo(saved); assertThat(found).isNotEmpty().get().extracting(Object::toString).isEqualTo(saved.toString());
} }
} }

View File

@ -309,7 +309,10 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu
.statusCode(204); // @formatter:on .statusCode(204); // @formatter:on
// then the given contact is gone // then the given contact is gone
assertThat(contactRepo.findByUuid(givenContact.getUuid())).isEmpty(); jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net", null);
assertThat(contactRepo.findByUuid(givenContact.getUuid())).isEmpty();
}).assertSuccessful();
} }
@Test @Test
@ -326,7 +329,10 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu
.statusCode(204); // @formatter:on .statusCode(204); // @formatter:on
// then the given contact is still there // then the given contact is still there
assertThat(contactRepo.findByUuid(givenContact.getUuid())).isEmpty(); jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net", null);
assertThat(contactRepo.findByUuid(givenContact.getUuid())).isEmpty();
}).assertSuccessful();
} }
@Test @Test

View File

@ -122,7 +122,7 @@ class HsOfficeContactRepositoryIntegrationTest extends ContextBasedTestWithClean
private void assertThatContactIsPersisted(final HsOfficeContactEntity saved) { private void assertThatContactIsPersisted(final HsOfficeContactEntity saved) {
final var found = contactRepo.findByUuid(saved.getUuid()); final var found = contactRepo.findByUuid(saved.getUuid());
assertThat(found).isNotEmpty().get().usingRecursiveComparison().isEqualTo(saved); assertThat(found).isNotEmpty().get().extracting(Object::toString).isEqualTo(saved.toString());
} }
} }

View File

@ -62,7 +62,7 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase
// given // given
context("superuser-alex@hostsharing.net"); context("superuser-alex@hostsharing.net");
final var count = coopAssetsTransactionRepo.count(); final var count = coopAssetsTransactionRepo.count();
final var givenMembership = membershipRepo.findMembershipByMemberNumber(1000101); final var givenMembership = membershipRepo.findMembershipByMemberNumber(1000101).load();
// when // when
final var result = attempt(em, () -> { final var result = attempt(em, () -> {
@ -119,7 +119,7 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase
private void assertThatCoopAssetsTransactionIsPersisted(final HsOfficeCoopAssetsTransactionEntity saved) { private void assertThatCoopAssetsTransactionIsPersisted(final HsOfficeCoopAssetsTransactionEntity saved) {
final var found = coopAssetsTransactionRepo.findByUuid(saved.getUuid()); final var found = coopAssetsTransactionRepo.findByUuid(saved.getUuid());
assertThat(found).isNotEmpty().get().usingRecursiveComparison().isEqualTo(saved); assertThat(found).isNotEmpty().get().extracting(HsOfficeCoopAssetsTransactionEntity::toString).isEqualTo(saved.toString());
} }
} }

View File

@ -61,7 +61,7 @@ class HsOfficeCoopSharesTransactionRepositoryIntegrationTest extends ContextBase
// given // given
context("superuser-alex@hostsharing.net"); context("superuser-alex@hostsharing.net");
final var count = coopSharesTransactionRepo.count(); final var count = coopSharesTransactionRepo.count();
final var givenMembership = membershipRepo.findMembershipByMemberNumber(1000101); final var givenMembership = membershipRepo.findMembershipByMemberNumber(1000101).load();
// when // when
final var result = attempt(em, () -> { final var result = attempt(em, () -> {
@ -118,7 +118,7 @@ class HsOfficeCoopSharesTransactionRepositoryIntegrationTest extends ContextBase
private void assertThatCoopSharesTransactionIsPersisted(final HsOfficeCoopSharesTransactionEntity saved) { private void assertThatCoopSharesTransactionIsPersisted(final HsOfficeCoopSharesTransactionEntity saved) {
final var found = coopSharesTransactionRepo.findByUuid(saved.getUuid()); final var found = coopSharesTransactionRepo.findByUuid(saved.getUuid());
assertThat(found).isNotEmpty().get().usingRecursiveComparison().isEqualTo(saved); assertThat(found).isNotEmpty().get().extracting(HsOfficeCoopSharesTransactionEntity::toString).isEqualTo(saved.toString());
} }
} }

View File

@ -609,22 +609,24 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
"defaultPrefix": "for" "defaultPrefix": "for"
} }
""" """
.replace("${debitorNumberSuffix}", givenDebitor.getDebitorNumberSuffix().toString())) .replace("${debitorNumberSuffix}", givenDebitor.getDebitorNumberSuffix()))
); );
// @formatter:on // @formatter:on
// finally, the debitor is actually updated // finally, the debitor is actually updated
context.define("superuser-alex@hostsharing.net"); jpaAttempt.transacted(() -> {
assertThat(debitorRepo.findByUuid(givenDebitor.getUuid())).isPresent().get() context.define("superuser-alex@hostsharing.net");
.matches(debitor -> { assertThat(debitorRepo.findByUuid(givenDebitor.getUuid())).isPresent().get()
assertThat(debitor.getDebitorRel().getHolder().getTradeName()) .matches(debitor -> {
.isEqualTo(givenDebitor.getDebitorRel().getHolder().getTradeName()); assertThat(debitor.getDebitorRel().getHolder().getTradeName())
assertThat(debitor.getDebitorRel().getContact().getCaption()).isEqualTo("fourth contact"); .isEqualTo(givenDebitor.getDebitorRel().getHolder().getTradeName());
assertThat(debitor.getVatId()).isEqualTo("VAT222222"); assertThat(debitor.getDebitorRel().getContact().getCaption()).isEqualTo("fourth contact");
assertThat(debitor.getVatCountryCode()).isEqualTo("AA"); assertThat(debitor.getVatId()).isEqualTo("VAT222222");
assertThat(debitor.isVatBusiness()).isEqualTo(true); assertThat(debitor.getVatCountryCode()).isEqualTo("AA");
return true; assertThat(debitor.isVatBusiness()).isEqualTo(true);
}); return true;
});
}).assertSuccessful();
} }
@Test @Test
@ -718,7 +720,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
private HsOfficeDebitorEntity givenSomeTemporaryDebitor() { private HsOfficeDebitorEntity givenSomeTemporaryDebitor() {
return jpaAttempt.transacted(() -> { return jpaAttempt.transacted(() -> {
context.define("superuser-alex@hostsharing.net"); context.define("superuser-alex@hostsharing.net");
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("Fourth").get(0); final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("Fourth").get(0).load();
final var givenContact = contactRepo.findContactByOptionalCaptionLike("fourth contact").get(0); final var givenContact = contactRepo.findContactByOptionalCaptionLike("fourth contact").get(0);
final var newDebitor = HsOfficeDebitorEntity.builder() final var newDebitor = HsOfficeDebitorEntity.builder()
.debitorNumberSuffix(nextDebitorSuffix()) .debitorNumberSuffix(nextDebitorSuffix())
@ -735,7 +737,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
.vatReverseCharge(false) .vatReverseCharge(false)
.build(); .build();
return debitorRepo.save(newDebitor); return debitorRepo.save(newDebitor).load();
}).assertSuccessful().returnedValue(); }).assertSuccessful().returnedValue();
} }

View File

@ -23,7 +23,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.orm.jpa.JpaObjectRetrievalFailureException;
import org.springframework.orm.jpa.JpaSystemException; import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -83,12 +82,14 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
// given // given
context("superuser-alex@hostsharing.net"); context("superuser-alex@hostsharing.net");
final var count = debitorRepo.count(); final var count = debitorRepo.count();
final var givenPartner = partnerRepo.findPartnerByPartnerNumber(10001);
final var givenPartnerPerson = one(personRepo.findPersonByOptionalNameLike("First GmbH")); final var givenPartnerPerson = one(personRepo.findPersonByOptionalNameLike("First GmbH"));
final var givenContact = one(contactRepo.findContactByOptionalCaptionLike("first contact")); final var givenContact = one(contactRepo.findContactByOptionalCaptionLike("first contact"));
// when // when
final var result = attempt(em, () -> { final var result = attempt(em, () -> {
final var newDebitor = HsOfficeDebitorEntity.builder() final var newDebitor = HsOfficeDebitorEntity.builder()
.partner(givenPartner)
.debitorNumberSuffix("21") .debitorNumberSuffix("21")
.debitorRel(HsOfficeRelationEntity.builder() .debitorRel(HsOfficeRelationEntity.builder()
.type(HsOfficeRelationType.DEBITOR) .type(HsOfficeRelationType.DEBITOR)
@ -99,7 +100,8 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
.defaultPrefix("abc") .defaultPrefix("abc")
.billable(false) .billable(false)
.build(); .build();
return toCleanup(debitorRepo.save(newDebitor)); final HsOfficeDebitorEntity entity = debitorRepo.save(newDebitor);
return toCleanup(entity.load());
}); });
// then // then
@ -339,14 +341,13 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
givenDebitor.setVatId(givenNewVatId); givenDebitor.setVatId(givenNewVatId);
givenDebitor.setVatCountryCode(givenNewVatCountryCode); givenDebitor.setVatCountryCode(givenNewVatCountryCode);
givenDebitor.setVatBusiness(givenNewVatBusiness); givenDebitor.setVatBusiness(givenNewVatBusiness);
return toCleanup(debitorRepo.save(givenDebitor)); final HsOfficeDebitorEntity entity = debitorRepo.save(givenDebitor);
return toCleanup(entity.load());
}); });
// then // then
result.assertSuccessful(); result.assertSuccessful();
assertThatDebitorIsVisibleForUserWithRole( assertThatDebitorIsVisibleForUserWithRole(result.returnedValue(), "global#global:ADMIN", true);
result.returnedValue(),
"global#global:ADMIN", true);
// ... partner role was reassigned: // ... partner role was reassigned:
assertThatDebitorIsNotVisibleForUserWithRole( assertThatDebitorIsNotVisibleForUserWithRole(
@ -388,7 +389,7 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
final var result = jpaAttempt.transacted(() -> { final var result = jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net"); context("superuser-alex@hostsharing.net");
givenDebitor.setRefundBankAccount(givenNewBankAccount); givenDebitor.setRefundBankAccount(givenNewBankAccount);
return toCleanup(debitorRepo.save(givenDebitor)); return toCleanup(debitorRepo.save(givenDebitor).load());
}); });
// then // then
@ -417,7 +418,7 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
final var result = jpaAttempt.transacted(() -> { final var result = jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net"); context("superuser-alex@hostsharing.net");
givenDebitor.setRefundBankAccount(null); givenDebitor.setRefundBankAccount(null);
return toCleanup(debitorRepo.save(givenDebitor)); return toCleanup(debitorRepo.save(givenDebitor).load());
}); });
// then // then
@ -460,22 +461,21 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
context("superuser-alex@hostsharing.net"); context("superuser-alex@hostsharing.net");
final var givenDebitor = givenSomeTemporaryDebitor("Fourth", "ninth", "Fourth", "nin"); final var givenDebitor = givenSomeTemporaryDebitor("Fourth", "ninth", "Fourth", "nin");
assertThatDebitorActuallyInDatabase(givenDebitor, true); assertThatDebitorActuallyInDatabase(givenDebitor, true);
assertThatDebitorIsVisibleForUserWithRole( assertThatDebitorIsVisibleForUserWithRole(givenDebitor, "hs_office_contact#ninthcontact:ADMIN", false);
givenDebitor,
"hs_office_contact#ninthcontact:ADMIN", false);
// when // when
final var result = jpaAttempt.transacted(() -> { final var result = jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net", "hs_office_contact#ninthcontact:ADMIN"); context("superuser-alex@hostsharing.net", "hs_office_contact#ninthcontact:ADMIN");
givenDebitor.setVatId("NEW-VAT-ID"); givenDebitor.setVatId("NEW-VAT-ID");
return toCleanup(debitorRepo.save(givenDebitor)); final HsOfficeDebitorEntity entity = debitorRepo.save(givenDebitor);
return toCleanup(entity.load());
}); });
// then // then
result.assertExceptionWithRootCauseMessage( result.assertExceptionWithRootCauseMessage(
JpaObjectRetrievalFailureException.class, JpaSystemException.class,
// this technical error message gets translated to a [403] error at the controller level "ERROR: [403]",
"Unable to find net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity with id "); "is not allowed to update hs_office_debitor uuid");
} }
private void assertThatDebitorActuallyInDatabase(final HsOfficeDebitorEntity saved, final boolean withPartner) { private void assertThatDebitorActuallyInDatabase(final HsOfficeDebitorEntity saved, final boolean withPartner) {
@ -608,11 +608,13 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
final String defaultPrefix) { final String defaultPrefix) {
return jpaAttempt.transacted(() -> { return jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net"); context("superuser-alex@hostsharing.net");
final var givenPartnerPerson = one(personRepo.findPersonByOptionalNameLike(partnerName)); final var givenPartner = one(partnerRepo.findPartnerByOptionalNameLike(partnerName));
final var givenPartnerPerson = givenPartner.getPartnerRel().getHolder();
final var givenContact = one(contactRepo.findContactByOptionalCaptionLike(contactCaption)); final var givenContact = one(contactRepo.findContactByOptionalCaptionLike(contactCaption));
final var givenBankAccount = final var givenBankAccount =
bankAccountHolder != null ? one(bankAccountRepo.findByOptionalHolderLike(bankAccountHolder)) : null; bankAccountHolder != null ? one(bankAccountRepo.findByOptionalHolderLike(bankAccountHolder)) : null;
final var newDebitor = HsOfficeDebitorEntity.builder() final var newDebitor = HsOfficeDebitorEntity.builder()
.partner(givenPartner)
.debitorNumberSuffix("20") .debitorNumberSuffix("20")
.debitorRel(HsOfficeRelationEntity.builder() .debitorRel(HsOfficeRelationEntity.builder()
.type(HsOfficeRelationType.DEBITOR) .type(HsOfficeRelationType.DEBITOR)
@ -625,7 +627,8 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
.billable(true) .billable(true)
.build(); .build();
return toCleanup(debitorRepo.save(newDebitor)); final HsOfficeDebitorEntity entity = debitorRepo.save(newDebitor);
return toCleanup(entity.load());
}).assertSuccessful().returnedValue(); }).assertSuccessful().returnedValue();
} }

View File

@ -75,7 +75,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
.validity(Range.closedInfinite(LocalDate.parse("2020-01-01"))) .validity(Range.closedInfinite(LocalDate.parse("2020-01-01")))
.membershipFeeBillable(true) .membershipFeeBillable(true)
.build(); .build();
return toCleanup(membershipRepo.save(newMembership)); return toCleanup(membershipRepo.save(newMembership).load());
}); });
// then // then
@ -143,7 +143,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
private void assertThatMembershipIsPersisted(final HsOfficeMembershipEntity saved) { private void assertThatMembershipIsPersisted(final HsOfficeMembershipEntity saved) {
final var found = membershipRepo.findByUuid(saved.getUuid()); final var found = membershipRepo.findByUuid(saved.getUuid());
assertThat(found).isNotEmpty().get().usingRecursiveComparison().isEqualTo(saved); assertThat(found).isNotEmpty().get().extracting(Object::toString).isEqualTo(saved.toString()) ;
} }
} }
@ -203,7 +203,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
public void globalAdmin_canUpdateValidityOfArbitraryMembership() { public void globalAdmin_canUpdateValidityOfArbitraryMembership() {
// given // given
context("superuser-alex@hostsharing.net"); context("superuser-alex@hostsharing.net");
final var givenMembership = givenSomeTemporaryMembership("First", "11"); final var givenMembership = givenSomeTemporaryMembership("First", "11");
assertThatMembershipExistsAndIsAccessibleToCurrentContext(givenMembership); assertThatMembershipExistsAndIsAccessibleToCurrentContext(givenMembership);
final var newValidityEnd = LocalDate.now(); final var newValidityEnd = LocalDate.now();
@ -214,13 +214,12 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
givenMembership.setValidity(Range.closedOpen( givenMembership.setValidity(Range.closedOpen(
givenMembership.getValidity().lower(), newValidityEnd)); givenMembership.getValidity().lower(), newValidityEnd));
givenMembership.setStatus(HsOfficeMembershipStatus.CANCELLED); givenMembership.setStatus(HsOfficeMembershipStatus.CANCELLED);
return toCleanup(membershipRepo.save(givenMembership)); final HsOfficeMembershipEntity entity = membershipRepo.save(givenMembership);
return toCleanup(entity.load());
}); });
// then // then
result.assertSuccessful(); result.assertSuccessful();
membershipRepo.deleteByUuid(givenMembership.getUuid());
} }
@Test @Test
@ -363,7 +362,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
.membershipFeeBillable(true) .membershipFeeBillable(true)
.build(); .build();
return toCleanup(membershipRepo.save(newMembership)); return toCleanup(membershipRepo.save(newMembership).load());
}).assertSuccessful().returnedValue(); }).assertSuccessful().returnedValue();
} }

View File

@ -548,7 +548,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu
.build()) .build())
.build(); .build();
return partnerRepo.save(newPartner); return partnerRepo.save(newPartner).load();
}).assertSuccessful().returnedValue(); }).assertSuccessful().returnedValue();
} }

View File

@ -180,7 +180,7 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean
private void assertThatPartnerIsPersisted(final HsOfficePartnerEntity saved) { private void assertThatPartnerIsPersisted(final HsOfficePartnerEntity saved) {
final var found = partnerRepo.findByUuid(saved.getUuid()); final var found = partnerRepo.findByUuid(saved.getUuid());
assertThat(found).isNotEmpty().get().usingRecursiveComparison().isEqualTo(saved); assertThat(found).isNotEmpty().get().extracting(Object::toString).isEqualTo(saved.toString());
} }
} }
@ -473,7 +473,7 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean
.anchor(givenMandantorPerson) .anchor(givenMandantorPerson)
.contact(givenContact) .contact(givenContact)
.build(); .build();
relationRepo.save(partnerRel); relationRepo.save(partnerRel).load();
return partnerRel; return partnerRel;
} }

View File

@ -298,7 +298,10 @@ class HsOfficePersonControllerAcceptanceTest extends ContextBasedTestWithCleanup
.statusCode(204); // @formatter:on .statusCode(204); // @formatter:on
// then the given person is still there // then the given person is still there
assertThat(personRepo.findByUuid(givenPerson.getUuid())).isEmpty(); jpaAttempt.transacted(() -> {
context.define("superuser-alex@hostsharing.net");
assertThat(personRepo.findByUuid(givenPerson.getUuid())).isEmpty();
}).assertSuccessful();
} }
@Test @Test
@ -332,7 +335,7 @@ class HsOfficePersonControllerAcceptanceTest extends ContextBasedTestWithCleanup
.givenName("Temp Given Name " + RandomStringUtils.randomAlphabetic(10)) .givenName("Temp Given Name " + RandomStringUtils.randomAlphabetic(10))
.build(); .build();
return personRepo.save(newPerson); return personRepo.save(newPerson).load();
}).assertSuccessful().returnedValue(); }).assertSuccessful().returnedValue();
} }

View File

@ -124,7 +124,7 @@ class HsOfficePersonRepositoryIntegrationTest extends ContextBasedTestWithCleanu
private void assertThatPersonIsPersisted(final HsOfficePersonEntity saved) { private void assertThatPersonIsPersisted(final HsOfficePersonEntity saved) {
final var found = personRepo.findByUuid(saved.getUuid()); final var found = personRepo.findByUuid(saved.getUuid());
assertThat(found).isNotEmpty().get().usingRecursiveComparison().isEqualTo(saved); assertThat(found).isNotEmpty().get().extracting(Object::toString).isEqualTo(saved.toString());
} }
} }

View File

@ -158,7 +158,7 @@ class HsOfficeRelationRepositoryIntegrationTest extends ContextBasedTestWithClea
private void assertThatRelationIsPersisted(final HsOfficeRelationEntity saved) { private void assertThatRelationIsPersisted(final HsOfficeRelationEntity saved) {
final var found = relationRepo.findByUuid(saved.getUuid()); final var found = relationRepo.findByUuid(saved.getUuid());
assertThat(found).isNotEmpty().get().usingRecursiveComparison().isEqualTo(saved); assertThat(found).isNotEmpty().get().extracting(Object::toString).isEqualTo(saved.toString());
} }
} }
@ -225,7 +225,7 @@ class HsOfficeRelationRepositoryIntegrationTest extends ContextBasedTestWithClea
final var result = jpaAttempt.transacted(() -> { final var result = jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net"); context("superuser-alex@hostsharing.net");
givenRelation.setContact(givenContact); givenRelation.setContact(givenContact);
return toCleanup(relationRepo.save(givenRelation)); return toCleanup(relationRepo.save(givenRelation).load());
}); });
// then // then
@ -295,7 +295,8 @@ class HsOfficeRelationRepositoryIntegrationTest extends ContextBasedTestWithClea
final var found = relationRepo.findByUuid(saved.getUuid()); final var found = relationRepo.findByUuid(saved.getUuid());
assertThat(found).isNotEmpty().get() assertThat(found).isNotEmpty().get()
.isNotSameAs(saved) .isNotSameAs(saved)
.usingRecursiveComparison().ignoringFields("version").isEqualTo(saved); .extracting(HsOfficeRelationEntity::toString)
.isEqualTo(saved.toString());
} }
private void assertThatRelationIsVisibleForUserWithRole( private void assertThatRelationIsVisibleForUserWithRole(

View File

@ -152,7 +152,7 @@ class HsOfficeSepaMandateRepositoryIntegrationTest extends ContextBasedTestWithC
private void assertThatSepaMandateIsPersisted(final HsOfficeSepaMandateEntity saved) { private void assertThatSepaMandateIsPersisted(final HsOfficeSepaMandateEntity saved) {
final var found = sepaMandateRepo.findByUuid(saved.getUuid()); final var found = sepaMandateRepo.findByUuid(saved.getUuid());
assertThat(found).isNotEmpty().get().usingRecursiveComparison().isEqualTo(saved); assertThat(found).isNotEmpty().get().extracting(Object::toString).isEqualTo(saved.toString());
} }
} }
@ -250,7 +250,7 @@ class HsOfficeSepaMandateRepositoryIntegrationTest extends ContextBasedTestWithC
jpaAttempt.transacted(() -> { jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net"); context("superuser-alex@hostsharing.net");
assertThat(sepaMandateRepo.findByUuid(givenSepaMandate.getUuid())).isNotEmpty().get() assertThat(sepaMandateRepo.findByUuid(givenSepaMandate.getUuid())).isNotEmpty().get()
.usingRecursiveComparison().isEqualTo(givenSepaMandate); .extracting(Object::toString).isEqualTo(givenSepaMandate.toString());
}).assertSuccessful(); }).assertSuccessful();
} }

View File

@ -90,7 +90,7 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest {
private void assertThatCustomerIsPersisted(final TestCustomerEntity saved) { private void assertThatCustomerIsPersisted(final TestCustomerEntity saved) {
final var found = testCustomerRepository.findByUuid(saved.getUuid()); final var found = testCustomerRepository.findByUuid(saved.getUuid());
assertThat(found).isNotEmpty().get().usingRecursiveComparison().isEqualTo(saved); assertThat(found).isNotEmpty().get().extracting(Object::toString).isEqualTo(saved.toString());
} }
} }

View File

@ -26,7 +26,7 @@ spring:
liquibase: liquibase:
change-log: classpath:/db/changelog/db.changelog-master.yaml change-log: classpath:/db/changelog/db.changelog-master.yaml
contexts: tc,test,dev contexts: tc,test,dev,pg_stat_statements
logging: logging:
level: level: