rbac-optimization #80
10
Dockerfile
Normal file
10
Dockerfile
Normal 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
|
301
doc/rbac-performance-analysis.md
Normal file
301
doc/rbac-performance-analysis.md
Normal 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
19
docker-compose.yml
Normal 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
|
10
etc/postgresql-log-slow-queries.conf
Normal file
10
etc/postgresql-log-slow-queries.conf
Normal 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 = '*'
|
@ -70,7 +70,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class HsBookingItemEntity implements Stringifyable, RbacObject, PropertiesProvider {
|
||||
public class HsBookingItemEntity implements Stringifyable, RbacObject<HsBookingItemEntity>, PropertiesProvider {
|
||||
|
||||
private static Stringify<HsBookingItemEntity> stringify = stringify(HsBookingItemEntity.class)
|
||||
.withProp(HsBookingItemEntity::getProject)
|
||||
|
@ -34,7 +34,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class HsBookingProjectEntity implements Stringifyable, RbacObject {
|
||||
public class HsBookingProjectEntity implements Stringifyable, RbacObject<HsBookingProjectEntity> {
|
||||
|
||||
private static Stringify<HsBookingProjectEntity> stringify = stringify(HsBookingProjectEntity.class)
|
||||
.withProp(HsBookingProjectEntity::getDebitor)
|
||||
|
@ -70,7 +70,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class HsHostingAssetEntity implements Stringifyable, RbacObject, PropertiesProvider {
|
||||
public class HsHostingAssetEntity implements Stringifyable, RbacObject<HsHostingAssetEntity>, PropertiesProvider {
|
||||
|
||||
private static Stringify<HsHostingAssetEntity> stringify = stringify(HsHostingAssetEntity.class)
|
||||
.withProp(HsHostingAssetEntity::getType)
|
||||
|
@ -27,7 +27,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
@AllArgsConstructor
|
||||
@FieldNameConstants
|
||||
@DisplayName("BankAccount")
|
||||
public class HsOfficeBankAccountEntity implements RbacObject, Stringifyable {
|
||||
public class HsOfficeBankAccountEntity implements RbacObject<HsOfficeBankAccountEntity>, Stringifyable {
|
||||
|
||||
private static Stringify<HsOfficeBankAccountEntity> toString = stringify(HsOfficeBankAccountEntity.class, "bankAccount")
|
||||
.withIdProp(HsOfficeBankAccountEntity::getIban)
|
||||
|
@ -35,7 +35,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
@AllArgsConstructor
|
||||
@FieldNameConstants
|
||||
@DisplayName("Contact")
|
||||
public class HsOfficeContactEntity implements Stringifyable, RbacObject {
|
||||
public class HsOfficeContactEntity implements Stringifyable, RbacObject<HsOfficeContactEntity> {
|
||||
|
||||
private static Stringify<HsOfficeContactEntity> toString = stringify(HsOfficeContactEntity.class, "contact")
|
||||
.withProp(Fields.caption, HsOfficeContactEntity::getCaption)
|
||||
|
@ -41,7 +41,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@DisplayName("CoopAssetsTransaction")
|
||||
public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, RbacObject {
|
||||
public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, RbacObject<HsOfficeCoopAssetsTransactionEntity> {
|
||||
|
||||
private static Stringify<HsOfficeCoopAssetsTransactionEntity> stringify = stringify(HsOfficeCoopAssetsTransactionEntity.class)
|
||||
.withIdProp(HsOfficeCoopAssetsTransactionEntity::getTaggedMemberNumber)
|
||||
@ -105,6 +105,13 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, RbacO
|
||||
@OneToOne(mappedBy = "adjustedAssetTx")
|
||||
private HsOfficeCoopAssetsTransactionEntity adjustmentAssetTx;
|
||||
|
||||
@Override
|
||||
public HsOfficeCoopAssetsTransactionEntity load() {
|
||||
RbacObject.super.load();
|
||||
membership.load();
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getTaggedMemberNumber() {
|
||||
return ofNullable(membership).map(HsOfficeMembershipEntity::toShortString).orElse("M-???????");
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@DisplayName("CoopShareTransaction")
|
||||
public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, RbacObject {
|
||||
public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, RbacObject<HsOfficeCoopSharesTransactionEntity> {
|
||||
|
||||
private static Stringify<HsOfficeCoopSharesTransactionEntity> stringify = stringify(HsOfficeCoopSharesTransactionEntity.class)
|
||||
.withIdProp(HsOfficeCoopSharesTransactionEntity::getMemberNumberTagged)
|
||||
@ -102,6 +102,13 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, RbacO
|
||||
@OneToOne(mappedBy = "adjustedShareTx")
|
||||
private HsOfficeCoopSharesTransactionEntity adjustmentShareTx;
|
||||
|
||||
@Override
|
||||
public HsOfficeCoopSharesTransactionEntity load() {
|
||||
RbacObject.super.load();
|
||||
membership.load();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return stringify.apply(this);
|
||||
|
@ -21,6 +21,7 @@ import org.hibernate.annotations.NotFoundAction;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
@ -57,7 +58,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@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 TWO_DECIMAL_DIGITS = "^([0-9]{2})$";
|
||||
@ -77,7 +78,7 @@ public class HsOfficeDebitorEntity implements RbacObject, Stringifyable {
|
||||
@Version
|
||||
private int version;
|
||||
|
||||
@ManyToOne
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinFormula(
|
||||
referencedColumnName = "uuid",
|
||||
value = """
|
||||
@ -91,14 +92,14 @@ public class HsOfficeDebitorEntity implements RbacObject, Stringifyable {
|
||||
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;
|
||||
|
||||
@Column(name = "debitornumbersuffix", length = 2)
|
||||
@Pattern(regexp = TWO_DECIMAL_DIGITS)
|
||||
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)
|
||||
private HsOfficeRelationEntity debitorRel;
|
||||
|
||||
@ -117,13 +118,27 @@ public class HsOfficeDebitorEntity implements RbacObject, Stringifyable {
|
||||
@Column(name = "vatreversecharge")
|
||||
private boolean vatReverseCharge;
|
||||
|
||||
@ManyToOne
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "refundbankaccountuuid")
|
||||
@NotFound(action = NotFoundAction.IGNORE)
|
||||
private HsOfficeBankAccountEntity refundBankAccount;
|
||||
|
||||
@Column(name = "defaultprefix", columnDefinition = "char(3) not null")
|
||||
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() {
|
||||
return ofNullable(partner)
|
||||
.filter(partner -> debitorNumberSuffix != null)
|
||||
|
@ -21,6 +21,7 @@ import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
@ -61,7 +62,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@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 TWO_DECIMAL_DIGITS = "^([0-9]{2})$";
|
||||
@ -80,7 +81,7 @@ public class HsOfficeMembershipEntity implements RbacObject, Stringifyable {
|
||||
@Version
|
||||
private int version;
|
||||
|
||||
@ManyToOne
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "partneruuid")
|
||||
private HsOfficePartnerEntity partner;
|
||||
|
||||
@ -99,6 +100,13 @@ public class HsOfficeMembershipEntity implements RbacObject, Stringifyable {
|
||||
@Enumerated(EnumType.STRING)
|
||||
private HsOfficeMembershipStatus status;
|
||||
|
||||
@Override
|
||||
public HsOfficeMembershipEntity load() {
|
||||
RbacObject.super.load();
|
||||
partner.load();
|
||||
return this;
|
||||
}
|
||||
|
||||
public void setValidFrom(final LocalDate validFrom) {
|
||||
setValidity(toPostgresDateRange(validFrom, getValidTo()));
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@DisplayName("PartnerDetails")
|
||||
public class HsOfficePartnerDetailsEntity implements RbacObject, Stringifyable {
|
||||
public class HsOfficePartnerDetailsEntity implements RbacObject<HsOfficePartnerDetailsEntity>, Stringifyable {
|
||||
|
||||
private static Stringify<HsOfficePartnerDetailsEntity> stringify = stringify(
|
||||
HsOfficePartnerDetailsEntity.class,
|
||||
|
@ -40,7 +40,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@DisplayName("Partner")
|
||||
public class HsOfficePartnerEntity implements Stringifyable, RbacObject {
|
||||
public class HsOfficePartnerEntity implements Stringifyable, RbacObject<HsOfficePartnerEntity> {
|
||||
|
||||
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")
|
||||
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)
|
||||
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")
|
||||
@NotFound(action = NotFoundAction.IGNORE)
|
||||
private HsOfficePartnerDetailsEntity details;
|
||||
|
||||
@Override
|
||||
public HsOfficePartnerEntity load() {
|
||||
RbacObject.super.load();
|
||||
partnerRel.load();
|
||||
details.load();
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getTaggedPartnerNumber() {
|
||||
return PARTNER_NUMBER_TAG + partnerNumber;
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
@AllArgsConstructor
|
||||
@FieldNameConstants
|
||||
@DisplayName("Person")
|
||||
public class HsOfficePersonEntity implements RbacObject, Stringifyable {
|
||||
public class HsOfficePersonEntity implements RbacObject<HsOfficePersonEntity>, Stringifyable {
|
||||
|
||||
private static Stringify<HsOfficePersonEntity> toString = stringify(HsOfficePersonEntity.class, "person")
|
||||
.withProp(Fields.personType, HsOfficePersonEntity::getPersonType)
|
||||
|
@ -56,15 +56,15 @@ public class HsOfficeRelationEntity implements RbacObject, Stringifyable {
|
||||
@Version
|
||||
private int version;
|
||||
|
||||
@ManyToOne
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "anchoruuid")
|
||||
private HsOfficePersonEntity anchor;
|
||||
|
||||
@ManyToOne
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "holderuuid")
|
||||
private HsOfficePersonEntity holder;
|
||||
|
||||
@ManyToOne
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "contactuuid")
|
||||
private HsOfficeContactEntity contact;
|
||||
|
||||
@ -75,6 +75,15 @@ public class HsOfficeRelationEntity implements RbacObject, Stringifyable {
|
||||
@Column(name = "mark")
|
||||
private String mark;
|
||||
|
||||
@Override
|
||||
public HsOfficeRelationEntity load() {
|
||||
RbacObject.super.load();
|
||||
anchor.load();
|
||||
holder.load();
|
||||
contact.load();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toString.apply(this);
|
||||
|
@ -40,7 +40,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@DisplayName("SEPA-Mandate")
|
||||
public class HsOfficeSepaMandateEntity implements Stringifyable, RbacObject {
|
||||
public class HsOfficeSepaMandateEntity implements Stringifyable, RbacObject<HsOfficeSepaMandateEntity> {
|
||||
|
||||
private static Stringify<HsOfficeSepaMandateEntity> stringify = stringify(HsOfficeSepaMandateEntity.class)
|
||||
.withProp(e -> e.getBankAccount().getIban())
|
||||
|
@ -1,10 +1,18 @@
|
||||
package net.hostsharing.hsadminng.rbac.rbacobject;
|
||||
|
||||
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public interface RbacObject {
|
||||
public interface RbacObject<T extends RbacObject<?>> {
|
||||
UUID getUuid();
|
||||
|
||||
int getVersion();
|
||||
|
||||
default T load() {
|
||||
Hibernate.initialize(this);
|
||||
//noinspection unchecked
|
||||
return (T) this;
|
||||
};
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TestCustomerEntity implements RbacObject {
|
||||
public class TestCustomerEntity implements RbacObject<TestCustomerEntity> {
|
||||
|
||||
@Id
|
||||
@GeneratedValue
|
||||
|
@ -27,7 +27,7 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TestDomainEntity implements RbacObject {
|
||||
public class TestDomainEntity implements RbacObject<TestDomainEntity> {
|
||||
|
||||
@Id
|
||||
@GeneratedValue
|
||||
|
@ -27,7 +27,7 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TestPackageEntity implements RbacObject {
|
||||
public class TestPackageEntity implements RbacObject<TestPackageEntity> {
|
||||
|
||||
@Id
|
||||
@GeneratedValue
|
||||
|
@ -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";
|
||||
hsh-michaelhoennig marked this conversation as resolved
|
||||
--//
|
||||
|
@ -372,6 +372,9 @@ create table RbacPermission
|
||||
op RbacOp not null,
|
||||
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
|
||||
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);
|
||||
|
||||
call create_journal('RbacGrants');
|
||||
|
||||
create or replace function findGrantees(grantedId uuid)
|
||||
returns setof RbacReference
|
||||
returns null on null input
|
||||
language sql as $$
|
||||
select reference.*
|
||||
from (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 ascendantUuid
|
||||
from grants) as grantee
|
||||
join RbacReference reference on reference.uuid = grantee.ascendantUuid;
|
||||
with recursive grants as (
|
||||
select descendantUuid, ascendantUuid
|
||||
from RbacGrants
|
||||
where descendantUuid = grantedId
|
||||
union all
|
||||
select g.descendantUuid, g.ascendantUuid
|
||||
from RbacGrants g
|
||||
inner join grants on grants.ascendantUuid = g.descendantUuid
|
||||
)
|
||||
select ref.*
|
||||
from grants
|
||||
join RbacReference ref on ref.uuid = grants.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)
|
||||
returns bool
|
||||
returns null on null input
|
||||
language sql as $$
|
||||
select granteeId = grantedId or granteeId in (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 ascendantUuid
|
||||
from grants);
|
||||
select * from isGranted(array[granteeId], grantedId);
|
||||
$$;
|
||||
|
||||
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)
|
||||
returns BOOL
|
||||
stable -- leakproof
|
||||
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 *
|
||||
from RbacUser
|
||||
where uuid in (with recursive grants as (select descendantUuid,
|
||||
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)
|
||||
);
|
||||
select true
|
||||
from grants
|
||||
where ascendantUuid = subjectId
|
||||
);
|
||||
$$;
|
||||
|
||||
create or replace function hasInsertPermission(objectUuid uuid, tableName text )
|
||||
@ -708,14 +701,14 @@ begin
|
||||
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(
|
||||
requiredOp RbacOp,
|
||||
forObjectTable varchar, -- reduces the result set, but is not really faster when used in restricted view
|
||||
forObjectTable varchar,
|
||||
subjectIds uuid[],
|
||||
maxObjects integer = 8000)
|
||||
returns setof uuid
|
||||
@ -724,23 +717,29 @@ create or replace function queryAccessibleObjectUuidsOfSubjectIds(
|
||||
declare
|
||||
foundRows bigint;
|
||||
begin
|
||||
return query select distinct perm.objectUuid
|
||||
from (with recursive grants as (select descendantUuid, ascendantUuid, 1 as level
|
||||
from RbacGrants
|
||||
where assumed
|
||||
and ascendantUuid = any (subjectIds)
|
||||
union
|
||||
distinct
|
||||
select "grant".descendantUuid, "grant".ascendantUuid, level + 1 as level
|
||||
from RbacGrants "grant"
|
||||
inner join grants recur on recur.descendantUuid = "grant".ascendantUuid
|
||||
where assumed)
|
||||
select descendantUuid
|
||||
from grants) as granted
|
||||
join RbacPermission perm
|
||||
on granted.descendantUuid = perm.uuid and (requiredOp = 'SELECT' or perm.op = requiredOp)
|
||||
join RbacObject obj on obj.uuid = perm.objectUuid and obj.objectTable = forObjectTable
|
||||
limit maxObjects + 1;
|
||||
return query
|
||||
WITH RECURSIVE grants AS (
|
||||
SELECT descendantUuid, ascendantUuid, 1 AS level
|
||||
FROM RbacGrants
|
||||
WHERE assumed
|
||||
AND ascendantUuid = any(subjectIds)
|
||||
UNION ALL
|
||||
SELECT g.descendantUuid, g.ascendantUuid, grants.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
|
||||
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();
|
||||
if foundRows > maxObjects then
|
||||
@ -751,7 +750,6 @@ begin
|
||||
end if;
|
||||
end;
|
||||
$$;
|
||||
|
||||
--//
|
||||
|
||||
-- ============================================================================
|
||||
@ -764,24 +762,23 @@ create or replace function queryPermissionsGrantedToSubjectId(subjectId uuid)
|
||||
returns setof RbacPermission
|
||||
strict
|
||||
language sql as $$
|
||||
-- @formatter:off
|
||||
select *
|
||||
from RbacPermission
|
||||
where uuid in (
|
||||
with recursive grants as (
|
||||
select distinct descendantUuid, ascendantUuid
|
||||
from RbacGrants
|
||||
where ascendantUuid = subjectId
|
||||
union all
|
||||
select "grant".descendantUuid, "grant".ascendantUuid
|
||||
from RbacGrants "grant"
|
||||
inner join grants recur on recur.descendantUuid = "grant".ascendantUuid
|
||||
)
|
||||
select descendantUuid
|
||||
from grants
|
||||
);
|
||||
-- @formatter:on
|
||||
with recursive grants as (
|
||||
select descendantUuid, ascendantUuid
|
||||
from RbacGrants
|
||||
where ascendantUuid = subjectId
|
||||
union all
|
||||
select g.descendantUuid, g.ascendantUuid
|
||||
from RbacGrants g
|
||||
inner join grants on grants.descendantUuid = g.ascendantUuid
|
||||
)
|
||||
select perm.*
|
||||
from RbacPermission perm
|
||||
where perm.uuid in (
|
||||
select descendantUuid
|
||||
from grants
|
||||
);
|
||||
$$;
|
||||
|
||||
--//
|
||||
|
||||
-- ============================================================================
|
||||
|
@ -175,16 +175,38 @@ begin
|
||||
Creates a restricted view based on the 'SELECT' permission of the current subject.
|
||||
*/
|
||||
sql := format($sql$
|
||||
set session session authorization default;
|
||||
create view %1$s_rv as
|
||||
with accessibleObjects as (
|
||||
select queryAccessibleObjectUuidsOfSubjectIds('SELECT', '%1$s', currentSubjectsUuids())
|
||||
create or replace view %1$s_rv as
|
||||
with accessible_%1$s_uuids as (
|
||||
|
||||
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.*
|
||||
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;
|
||||
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);
|
||||
execute sql;
|
||||
|
||||
|
@ -21,6 +21,8 @@ databaseChangeLog:
|
||||
file: db/changelog/0-basis/010-context.sql
|
||||
- include:
|
||||
file: db/changelog/0-basis/020-audit-log.sql
|
||||
- include:
|
||||
file: db/changelog/0-basis/090-log-slow-queries-extensions.sql
|
||||
- include:
|
||||
file: db/changelog/1-rbac/1050-rbac-base.sql
|
||||
- include:
|
||||
|
@ -287,7 +287,10 @@ class HsOfficeBankAccountControllerAcceptanceTest extends ContextBasedTestWithCl
|
||||
.statusCode(204); // @formatter:on
|
||||
|
||||
// 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
|
||||
|
@ -123,7 +123,7 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTestWithC
|
||||
|
||||
private void assertThatBankAccountIsPersisted(final HsOfficeBankAccountEntity saved) {
|
||||
final var found = bankAccountRepo.findByUuid(saved.getUuid());
|
||||
assertThat(found).isNotEmpty().get().usingRecursiveComparison().isEqualTo(saved);
|
||||
assertThat(found).isNotEmpty().get().extracting(Object::toString).isEqualTo(saved.toString());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -309,7 +309,10 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
||||
.statusCode(204); // @formatter:on
|
||||
|
||||
// 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
|
||||
@ -326,7 +329,10 @@ class HsOfficeContactControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
||||
.statusCode(204); // @formatter:on
|
||||
|
||||
// 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
|
||||
|
@ -122,7 +122,7 @@ class HsOfficeContactRepositoryIntegrationTest extends ContextBasedTestWithClean
|
||||
|
||||
private void assertThatContactIsPersisted(final HsOfficeContactEntity saved) {
|
||||
final var found = contactRepo.findByUuid(saved.getUuid());
|
||||
assertThat(found).isNotEmpty().get().usingRecursiveComparison().isEqualTo(saved);
|
||||
assertThat(found).isNotEmpty().get().extracting(Object::toString).isEqualTo(saved.toString());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,7 +62,7 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase
|
||||
// given
|
||||
context("superuser-alex@hostsharing.net");
|
||||
final var count = coopAssetsTransactionRepo.count();
|
||||
final var givenMembership = membershipRepo.findMembershipByMemberNumber(1000101);
|
||||
final var givenMembership = membershipRepo.findMembershipByMemberNumber(1000101).load();
|
||||
|
||||
// when
|
||||
final var result = attempt(em, () -> {
|
||||
@ -119,7 +119,7 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase
|
||||
|
||||
private void assertThatCoopAssetsTransactionIsPersisted(final HsOfficeCoopAssetsTransactionEntity saved) {
|
||||
final var found = coopAssetsTransactionRepo.findByUuid(saved.getUuid());
|
||||
assertThat(found).isNotEmpty().get().usingRecursiveComparison().isEqualTo(saved);
|
||||
assertThat(found).isNotEmpty().get().extracting(HsOfficeCoopAssetsTransactionEntity::toString).isEqualTo(saved.toString());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -61,7 +61,7 @@ class HsOfficeCoopSharesTransactionRepositoryIntegrationTest extends ContextBase
|
||||
// given
|
||||
context("superuser-alex@hostsharing.net");
|
||||
final var count = coopSharesTransactionRepo.count();
|
||||
final var givenMembership = membershipRepo.findMembershipByMemberNumber(1000101);
|
||||
final var givenMembership = membershipRepo.findMembershipByMemberNumber(1000101).load();
|
||||
|
||||
// when
|
||||
final var result = attempt(em, () -> {
|
||||
@ -118,7 +118,7 @@ class HsOfficeCoopSharesTransactionRepositoryIntegrationTest extends ContextBase
|
||||
|
||||
private void assertThatCoopSharesTransactionIsPersisted(final HsOfficeCoopSharesTransactionEntity saved) {
|
||||
final var found = coopSharesTransactionRepo.findByUuid(saved.getUuid());
|
||||
assertThat(found).isNotEmpty().get().usingRecursiveComparison().isEqualTo(saved);
|
||||
assertThat(found).isNotEmpty().get().extracting(HsOfficeCoopSharesTransactionEntity::toString).isEqualTo(saved.toString());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -609,22 +609,24 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
||||
"defaultPrefix": "for"
|
||||
}
|
||||
"""
|
||||
.replace("${debitorNumberSuffix}", givenDebitor.getDebitorNumberSuffix().toString()))
|
||||
.replace("${debitorNumberSuffix}", givenDebitor.getDebitorNumberSuffix()))
|
||||
);
|
||||
// @formatter:on
|
||||
|
||||
// finally, the debitor is actually updated
|
||||
context.define("superuser-alex@hostsharing.net");
|
||||
assertThat(debitorRepo.findByUuid(givenDebitor.getUuid())).isPresent().get()
|
||||
.matches(debitor -> {
|
||||
assertThat(debitor.getDebitorRel().getHolder().getTradeName())
|
||||
.isEqualTo(givenDebitor.getDebitorRel().getHolder().getTradeName());
|
||||
assertThat(debitor.getDebitorRel().getContact().getCaption()).isEqualTo("fourth contact");
|
||||
assertThat(debitor.getVatId()).isEqualTo("VAT222222");
|
||||
assertThat(debitor.getVatCountryCode()).isEqualTo("AA");
|
||||
assertThat(debitor.isVatBusiness()).isEqualTo(true);
|
||||
return true;
|
||||
});
|
||||
jpaAttempt.transacted(() -> {
|
||||
context.define("superuser-alex@hostsharing.net");
|
||||
assertThat(debitorRepo.findByUuid(givenDebitor.getUuid())).isPresent().get()
|
||||
.matches(debitor -> {
|
||||
assertThat(debitor.getDebitorRel().getHolder().getTradeName())
|
||||
.isEqualTo(givenDebitor.getDebitorRel().getHolder().getTradeName());
|
||||
assertThat(debitor.getDebitorRel().getContact().getCaption()).isEqualTo("fourth contact");
|
||||
assertThat(debitor.getVatId()).isEqualTo("VAT222222");
|
||||
assertThat(debitor.getVatCountryCode()).isEqualTo("AA");
|
||||
assertThat(debitor.isVatBusiness()).isEqualTo(true);
|
||||
return true;
|
||||
});
|
||||
}).assertSuccessful();
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -718,7 +720,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
||||
private HsOfficeDebitorEntity givenSomeTemporaryDebitor() {
|
||||
return jpaAttempt.transacted(() -> {
|
||||
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 newDebitor = HsOfficeDebitorEntity.builder()
|
||||
.debitorNumberSuffix(nextDebitorSuffix())
|
||||
@ -735,7 +737,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
||||
.vatReverseCharge(false)
|
||||
.build();
|
||||
|
||||
return debitorRepo.save(newDebitor);
|
||||
return debitorRepo.save(newDebitor).load();
|
||||
}).assertSuccessful().returnedValue();
|
||||
}
|
||||
|
||||
|
@ -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.mock.mockito.MockBean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.orm.jpa.JpaObjectRetrievalFailureException;
|
||||
import org.springframework.orm.jpa.JpaSystemException;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@ -83,12 +82,14 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
|
||||
// given
|
||||
context("superuser-alex@hostsharing.net");
|
||||
final var count = debitorRepo.count();
|
||||
final var givenPartner = partnerRepo.findPartnerByPartnerNumber(10001);
|
||||
final var givenPartnerPerson = one(personRepo.findPersonByOptionalNameLike("First GmbH"));
|
||||
final var givenContact = one(contactRepo.findContactByOptionalCaptionLike("first contact"));
|
||||
|
||||
// when
|
||||
final var result = attempt(em, () -> {
|
||||
final var newDebitor = HsOfficeDebitorEntity.builder()
|
||||
.partner(givenPartner)
|
||||
.debitorNumberSuffix("21")
|
||||
.debitorRel(HsOfficeRelationEntity.builder()
|
||||
.type(HsOfficeRelationType.DEBITOR)
|
||||
@ -99,7 +100,8 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
|
||||
.defaultPrefix("abc")
|
||||
.billable(false)
|
||||
.build();
|
||||
return toCleanup(debitorRepo.save(newDebitor));
|
||||
final HsOfficeDebitorEntity entity = debitorRepo.save(newDebitor);
|
||||
return toCleanup(entity.load());
|
||||
});
|
||||
|
||||
// then
|
||||
@ -339,14 +341,13 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
|
||||
givenDebitor.setVatId(givenNewVatId);
|
||||
givenDebitor.setVatCountryCode(givenNewVatCountryCode);
|
||||
givenDebitor.setVatBusiness(givenNewVatBusiness);
|
||||
return toCleanup(debitorRepo.save(givenDebitor));
|
||||
final HsOfficeDebitorEntity entity = debitorRepo.save(givenDebitor);
|
||||
return toCleanup(entity.load());
|
||||
});
|
||||
|
||||
// then
|
||||
result.assertSuccessful();
|
||||
assertThatDebitorIsVisibleForUserWithRole(
|
||||
result.returnedValue(),
|
||||
"global#global:ADMIN", true);
|
||||
assertThatDebitorIsVisibleForUserWithRole(result.returnedValue(), "global#global:ADMIN", true);
|
||||
|
||||
// ... partner role was reassigned:
|
||||
assertThatDebitorIsNotVisibleForUserWithRole(
|
||||
@ -388,7 +389,7 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
|
||||
final var result = jpaAttempt.transacted(() -> {
|
||||
context("superuser-alex@hostsharing.net");
|
||||
givenDebitor.setRefundBankAccount(givenNewBankAccount);
|
||||
return toCleanup(debitorRepo.save(givenDebitor));
|
||||
return toCleanup(debitorRepo.save(givenDebitor).load());
|
||||
});
|
||||
|
||||
// then
|
||||
@ -417,7 +418,7 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
|
||||
final var result = jpaAttempt.transacted(() -> {
|
||||
context("superuser-alex@hostsharing.net");
|
||||
givenDebitor.setRefundBankAccount(null);
|
||||
return toCleanup(debitorRepo.save(givenDebitor));
|
||||
return toCleanup(debitorRepo.save(givenDebitor).load());
|
||||
});
|
||||
|
||||
// then
|
||||
@ -460,22 +461,21 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
|
||||
context("superuser-alex@hostsharing.net");
|
||||
final var givenDebitor = givenSomeTemporaryDebitor("Fourth", "ninth", "Fourth", "nin");
|
||||
assertThatDebitorActuallyInDatabase(givenDebitor, true);
|
||||
assertThatDebitorIsVisibleForUserWithRole(
|
||||
givenDebitor,
|
||||
"hs_office_contact#ninthcontact:ADMIN", false);
|
||||
assertThatDebitorIsVisibleForUserWithRole(givenDebitor, "hs_office_contact#ninthcontact:ADMIN", false);
|
||||
|
||||
// when
|
||||
final var result = jpaAttempt.transacted(() -> {
|
||||
context("superuser-alex@hostsharing.net", "hs_office_contact#ninthcontact:ADMIN");
|
||||
givenDebitor.setVatId("NEW-VAT-ID");
|
||||
return toCleanup(debitorRepo.save(givenDebitor));
|
||||
final HsOfficeDebitorEntity entity = debitorRepo.save(givenDebitor);
|
||||
return toCleanup(entity.load());
|
||||
});
|
||||
|
||||
// then
|
||||
result.assertExceptionWithRootCauseMessage(
|
||||
JpaObjectRetrievalFailureException.class,
|
||||
// this technical error message gets translated to a [403] error at the controller level
|
||||
"Unable to find net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity with id ");
|
||||
JpaSystemException.class,
|
||||
"ERROR: [403]",
|
||||
"is not allowed to update hs_office_debitor uuid");
|
||||
}
|
||||
|
||||
private void assertThatDebitorActuallyInDatabase(final HsOfficeDebitorEntity saved, final boolean withPartner) {
|
||||
@ -608,11 +608,13 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
|
||||
final String defaultPrefix) {
|
||||
return jpaAttempt.transacted(() -> {
|
||||
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 givenBankAccount =
|
||||
bankAccountHolder != null ? one(bankAccountRepo.findByOptionalHolderLike(bankAccountHolder)) : null;
|
||||
final var newDebitor = HsOfficeDebitorEntity.builder()
|
||||
.partner(givenPartner)
|
||||
.debitorNumberSuffix("20")
|
||||
.debitorRel(HsOfficeRelationEntity.builder()
|
||||
.type(HsOfficeRelationType.DEBITOR)
|
||||
@ -625,7 +627,8 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
|
||||
.billable(true)
|
||||
.build();
|
||||
|
||||
return toCleanup(debitorRepo.save(newDebitor));
|
||||
final HsOfficeDebitorEntity entity = debitorRepo.save(newDebitor);
|
||||
return toCleanup(entity.load());
|
||||
}).assertSuccessful().returnedValue();
|
||||
}
|
||||
|
||||
|
@ -75,7 +75,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
|
||||
.validity(Range.closedInfinite(LocalDate.parse("2020-01-01")))
|
||||
.membershipFeeBillable(true)
|
||||
.build();
|
||||
return toCleanup(membershipRepo.save(newMembership));
|
||||
return toCleanup(membershipRepo.save(newMembership).load());
|
||||
});
|
||||
|
||||
// then
|
||||
@ -143,7 +143,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
|
||||
|
||||
private void assertThatMembershipIsPersisted(final HsOfficeMembershipEntity saved) {
|
||||
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() {
|
||||
// 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();
|
||||
|
||||
@ -214,13 +214,12 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
|
||||
givenMembership.setValidity(Range.closedOpen(
|
||||
givenMembership.getValidity().lower(), newValidityEnd));
|
||||
givenMembership.setStatus(HsOfficeMembershipStatus.CANCELLED);
|
||||
return toCleanup(membershipRepo.save(givenMembership));
|
||||
final HsOfficeMembershipEntity entity = membershipRepo.save(givenMembership);
|
||||
return toCleanup(entity.load());
|
||||
});
|
||||
|
||||
// then
|
||||
result.assertSuccessful();
|
||||
|
||||
membershipRepo.deleteByUuid(givenMembership.getUuid());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -363,7 +362,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl
|
||||
.membershipFeeBillable(true)
|
||||
.build();
|
||||
|
||||
return toCleanup(membershipRepo.save(newMembership));
|
||||
return toCleanup(membershipRepo.save(newMembership).load());
|
||||
}).assertSuccessful().returnedValue();
|
||||
}
|
||||
|
||||
|
@ -548,7 +548,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
||||
.build())
|
||||
.build();
|
||||
|
||||
return partnerRepo.save(newPartner);
|
||||
return partnerRepo.save(newPartner).load();
|
||||
}).assertSuccessful().returnedValue();
|
||||
}
|
||||
|
||||
|
@ -180,7 +180,7 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean
|
||||
|
||||
private void assertThatPartnerIsPersisted(final HsOfficePartnerEntity saved) {
|
||||
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)
|
||||
.contact(givenContact)
|
||||
.build();
|
||||
relationRepo.save(partnerRel);
|
||||
relationRepo.save(partnerRel).load();
|
||||
return partnerRel;
|
||||
}
|
||||
|
||||
|
@ -298,7 +298,10 @@ class HsOfficePersonControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
||||
.statusCode(204); // @formatter:on
|
||||
|
||||
// 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
|
||||
@ -332,7 +335,7 @@ class HsOfficePersonControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
||||
.givenName("Temp Given Name " + RandomStringUtils.randomAlphabetic(10))
|
||||
.build();
|
||||
|
||||
return personRepo.save(newPerson);
|
||||
return personRepo.save(newPerson).load();
|
||||
}).assertSuccessful().returnedValue();
|
||||
}
|
||||
|
||||
|
@ -124,7 +124,7 @@ class HsOfficePersonRepositoryIntegrationTest extends ContextBasedTestWithCleanu
|
||||
|
||||
private void assertThatPersonIsPersisted(final HsOfficePersonEntity saved) {
|
||||
final var found = personRepo.findByUuid(saved.getUuid());
|
||||
assertThat(found).isNotEmpty().get().usingRecursiveComparison().isEqualTo(saved);
|
||||
assertThat(found).isNotEmpty().get().extracting(Object::toString).isEqualTo(saved.toString());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -158,7 +158,7 @@ class HsOfficeRelationRepositoryIntegrationTest extends ContextBasedTestWithClea
|
||||
|
||||
private void assertThatRelationIsPersisted(final HsOfficeRelationEntity saved) {
|
||||
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(() -> {
|
||||
context("superuser-alex@hostsharing.net");
|
||||
givenRelation.setContact(givenContact);
|
||||
return toCleanup(relationRepo.save(givenRelation));
|
||||
return toCleanup(relationRepo.save(givenRelation).load());
|
||||
});
|
||||
|
||||
// then
|
||||
@ -295,7 +295,8 @@ class HsOfficeRelationRepositoryIntegrationTest extends ContextBasedTestWithClea
|
||||
final var found = relationRepo.findByUuid(saved.getUuid());
|
||||
assertThat(found).isNotEmpty().get()
|
||||
.isNotSameAs(saved)
|
||||
.usingRecursiveComparison().ignoringFields("version").isEqualTo(saved);
|
||||
.extracting(HsOfficeRelationEntity::toString)
|
||||
.isEqualTo(saved.toString());
|
||||
}
|
||||
|
||||
private void assertThatRelationIsVisibleForUserWithRole(
|
||||
|
@ -152,7 +152,7 @@ class HsOfficeSepaMandateRepositoryIntegrationTest extends ContextBasedTestWithC
|
||||
|
||||
private void assertThatSepaMandateIsPersisted(final HsOfficeSepaMandateEntity saved) {
|
||||
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(() -> {
|
||||
context("superuser-alex@hostsharing.net");
|
||||
assertThat(sepaMandateRepo.findByUuid(givenSepaMandate.getUuid())).isNotEmpty().get()
|
||||
.usingRecursiveComparison().isEqualTo(givenSepaMandate);
|
||||
.extracting(Object::toString).isEqualTo(givenSepaMandate.toString());
|
||||
}).assertSuccessful();
|
||||
}
|
||||
|
||||
|
@ -90,7 +90,7 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest {
|
||||
|
||||
private void assertThatCustomerIsPersisted(final TestCustomerEntity saved) {
|
||||
final var found = testCustomerRepository.findByUuid(saved.getUuid());
|
||||
assertThat(found).isNotEmpty().get().usingRecursiveComparison().isEqualTo(saved);
|
||||
assertThat(found).isNotEmpty().get().extracting(Object::toString).isEqualTo(saved.toString());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@ spring:
|
||||
|
||||
liquibase:
|
||||
change-log: classpath:/db/changelog/db.changelog-master.yaml
|
||||
contexts: tc,test,dev
|
||||
contexts: tc,test,dev,pg_stat_statements
|
||||
|
||||
logging:
|
||||
level:
|
||||
|
Loading…
Reference in New Issue
Block a user
nicht für prod