From 3a24e1c7267ff15d4fa346817bdf67b369030694 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 19 Mar 2026 10:17:08 +0100 Subject: [PATCH] avoid-recursive-rbac-query-for-global-admins in the _rv generator (#216) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/216 Reviewed-by: Marc Sandlus --- ...formance-optimization-for-global-admins.md | 325 ++++++++++ .../db/changelog/1-rbac/1054-rbac-context.sql | 11 +- .../changelog/1-rbac/1058-rbac-generators.sql | 91 +-- .../db/changelog/1-rbac/1080-rbac-global.sql | 17 +- .../2013-rbactest-customer-rbac.sql | 1 + .../2023-rbactest-package-rbac.sql | 1 + .../2033-rbactest-domain-rbac.sql | 1 + .../5013-hs-office-contact-rbac.sql | 1 + .../502-person/5023-hs-office-person-rbac.sql | 1 + .../5033-hs-office-relation-rbac.sql | 1 + .../5043-hs-office-partner-rbac.sql | 1 + .../5044-hs-office-partner-details-rbac.sql | 1 + .../5053-hs-office-bankaccount-rbac.sql | 1 + .../5063-hs-office-debitor-rbac.sql | 1 + .../5073-hs-office-sepamandate-rbac.sql | 1 + .../5103-hs-office-membership-rbac.sql | 1 + .../5113-hs-office-coopshares-rbac.sql | 1 + .../5123-hs-office-coopassets-rbac.sql | 1 + .../6203-hs-booking-project-rbac.sql | 1 + .../6303-hs-booking-item-rbac.sql | 1 + .../7013-hs-hosting-asset-rbac.sql | 1 + .../9520-hs-mass-test-data-generators.sql | 586 ++++++++++++++++++ .../9521-has-mass-test-data-performance.sql | 74 +++ .../db/changelog/db.changelog-master.yaml | 6 + .../rbac/context/ContextIntegrationTests.java | 16 +- 25 files changed, 1101 insertions(+), 42 deletions(-) create mode 100644 doc/PR/2026-03-08-PR#-216-rbac-performance-optimization-for-global-admins.md create mode 100644 src/main/resources/db/changelog/9-hs-global/9520-hs-mass-test-data-generators.sql create mode 100644 src/main/resources/db/changelog/9-hs-global/9521-has-mass-test-data-performance.sql diff --git a/doc/PR/2026-03-08-PR#-216-rbac-performance-optimization-for-global-admins.md b/doc/PR/2026-03-08-PR#-216-rbac-performance-optimization-for-global-admins.md new file mode 100644 index 00000000..7c8bc12b --- /dev/null +++ b/doc/PR/2026-03-08-PR#-216-rbac-performance-optimization-for-global-admins.md @@ -0,0 +1,325 @@ +# PR#216: RBAC Performance Optimization for Global Admins + +## The Problem + +We have a severe performance problem in SELECT Queries when executed as global-admin. + +The cause of the performance problem is, that if a global-admin runs SELECT queries, +they can see all rows but yet the ReBAC filter is still active. +In other words, in the case of a SELECT without a WHERE-condition, +the ReBAC access rights are checked for each row in the target table. +This is horribly expensive because that's a recursive CTE query. + +There was some shortcut in the code (see `procedure rbac.generateRbacRestrictedView` before this merge commit), +but which was not really used by the query-optimizer and still the whole recursive CTE query got exectuted. +This can be seen below in [Query-Plan before](#query-plan-before). + +## The Solution + +To find a solution, we need mass test-data, a query-plan analysis and a refactored rekursive CTE query. + +### Test-Data Generation + +To be able to do performance-tests, mass test-data was needed. +I estimated the production database contains about 400 partner records and 500 SEPA mandate records, both including old ones. +For performance-tests I needed similar test data, or better even a bit more to be future safe, e.g. about twice the quantity. + +The test-data script is a stored procedure `procedure hs_office.contact_create_mass_test_data` which is also part of the test-data Liquibase profile, but just the script, no mass test-data is generated automatically. + +You can actually generate mass test-data by running the following SQL commands: + +```PostgresSQL +rollback; -- for the case of any previously failed transaction + +-- generate test data for partner numbers 20xxx with 80% membership +call hs_office.partner_create_mass_bundle_test_data(20000, 20999, 80); + +-- show some statistics about what was generated +select * from hs_statistics_v; + +-- we mostly care about SEPA mandates: +select count(*) from hs_office.sepamandate; +``` + +The last statement will most likely show 1003, 3 from the normal test-data plus 1000 from the mass test data. + +Find the statistics for the database after mass test-data generation in attachment [Mass-Data-Statistics](#attachment-mass-data-statistics). + + +### The Performance-Test Script + +Find the performance-script in `procedure hs_office.bench_debitor_sepamandates`, +which is now part of the Liquibase-changesets for the test-data profile. + +The test can be run this way: + +```PostgreSQL +\o /dev/null +rollback; +call hs_office.bench_debitor_sepamandates(100); -- 100 is the number of loops +``` + +### Query Plan Analysis + +To get hints about what's going wrong, I did a query-plan analysis: + +```PostgreSQ +rollback; + +begin; + call base.defineContext( 'query debitor', null, 'superuser-alex@hostsharing.net' ); + \timing on + explain analyze + select count(*) + from hs_office.debitor d + join hs_office.sepamandate_rv s on (s.debitoruuid = d.uuid) + join hs_office.bankaccount b on (b.uuid = s.bankAccountUuid); +``` + +#### Query-Plan before + +The following was the query plan with the shortcut-optimization which was not picked up by the query-optimizer, +thus on the commit before the merge-commit for this branch. + +No need to read the resulting query-plan in details, simply put, it does way too much. +If curious, you can find it [in the attachment](#attachment-query-plan-before) + + + +#### Query-Plan after optimization with isGlobalAdmin-cache + +``` +Aggregate (cost=90.27..90.28 rows=1 width=8) (actual time=14.961..14.982 rows=1 loops=1) + -> Hash Join (cost=72.50..87.77 rows=1000 width=0) (actual time=7.692..13.800 rows=1003 loops=1) + Hash Cond: (select_hs_office_sepamandate_rv.bankaccountuuid = b.uuid) + -> Hash Join (cost=35.82..48.45 rows=1000 width=16) (actual time=3.945..7.648 rows=1003 loops=1) + Hash Cond: (select_hs_office_sepamandate_rv.debitoruuid = d.uuid) + -> Function Scan on select_hs_office_sepamandate_rv (cost=0.25..10.25 rows=1000 width=32) (actual time=1.018..2.277 rows=1003 loops=1) + -> Hash (cost=23.03..23.03 rows=1003 width=16) (actual time=2.894..2.899 rows=1003 loops=1) + Buckets: 1024 Batches: 1 Memory Usage: 56kB + -> Seq Scan on debitor d (cost=0.00..23.03 rows=1003 width=16) (actual time=0.020..1.464 rows=1003 loops=1) + -> Hash (cost=24.08..24.08 rows=1008 width=16) (actual time=3.734..3.738 rows=1008 loops=1) + Buckets: 1024 Batches: 1 Memory Usage: 56kB + -> Seq Scan on bankaccount b (cost=0.00..24.08 rows=1008 width=16) (actual time=0.036..1.937 rows=1008 loops=1) + +Planning Time: 0.349 ms +Execution Time: 15.063 ms +``` + +This looks like a sensible tidy query plan for the job. + +### Performance-Comparison + +#### case A) with old optimization, freshly generated schema + +``` +limit10 min/avg/max: 2545.297 ms / 2796.658 ms / 4433.851 ms +count all min/avg/max: 2571.942 ms / 2821.460 ms / 3658.856 ms +completed in 9 m 22 s 8 ms +``` + +This clearly shows the problem; up to almost 3 seconds for a single query is too long. + +#### case B) with new optimization, freshly generated schema + +``` +limit10 min/avg/max: 2418.032 ms / 2656.825 ms / 4212.158 ms +count all min/avg/max: 2443.345 ms / 2680.387 ms / 3475.913 ms +completed in 8 m 53 s 908 ms +``` + +My new shortcut implementation in the recursive CTE query did not show much improvement, +probably so even no improvement as the difference is statistically just too little. + +#### case C) without optimization, but cached isGlobalAdmin, freshly generated schema + +Now, I went back to the previous shortcut but store the information +if the current subject is a global admin as a session-variable. +The caching is done right when `global.defineContext()` is called. + +``` +limit10 min/avg/max: 383.585 ms / 425.593 ms / 1135.419 ms +count all min/avg/max: 374.289 ms / 408.141 ms / 474.026 ms +completed in 1 m 23 s 437 ms +``` + +As we can see, this showed some progress, but not enough. + +#### case D) with new optimization + cache, freshly generated schema + +Now I combined both approaches, the new shortcut and cached the isGlobalAdmin information + +``` +limit10 min/avg/max: 0.806 ms / 1.212 ms / 2.159 ms +count all min/avg/max: 1.305 ms / 1.805 ms / 3.469 ms +completed in 620 ms +``` + +This brought the breakthrough; we are now down from almost 4 seconds to below 4 milliseconds, +**faster by a factor of 1000**. + +#### case E) with new optimization + cache, upgraded schema + +So far, I always freshly generated the schema. +But for our production database, we need to upgrade the existing schema. +Unfortunately, parts of the improved implementation are in code that is generated +(by `procedure rbac.generateRbacRestrictedView`), thus, +not just the generator had to be updated, but also be called for each table with RBAC support. + +To be on the safe side that it really worked, I ran the performance-tests again: + +``` +limit10 min/avg/max: 0.515 ms / 1.012 ms / 3.464 ms +count all min/avg/max: 1.116 ms / 1.801 ms / 2.614 ms +completed in 510 ms +``` + +Which is quite similar to case D, as expected. + + +### Epilogue + +This performance optimization only works if the current subject is a global admin, +the global-admin role may or may not be assumed, but no lower role. + +If any lower role gets assumed or the subject is not granted the global-admin role, +the rekursive CTE query still has to be executed. + +This might still be a performance problem, but not as bad as in the case of a global-admin, +because normal users cannot see that many objects, nor do they have that many (indirect) grants. +Therefore, both the width and the depth of the recursion are much smaller than for global-admins. + +But for users who can see very many objects, e.g. the admin of a large client, +there could still be a severe performance problem. + +There are ideas for optimizing the ReBAC-system, which are described in [RBAC Performance Analysis](rbac-performance-analysis.md#the-problematically-huge-join). +But these need major changes in the RBAC system, for which we currently have no financial capacity. + + +## Attachments + +### Attachment: Mass-Data-Statistics + + +| count | rbac-table | hs-table | type | +| :--- | :--- | :--- | :--- | +| 218 019 | grants | | | +| 168 865 | references | | | +| 94 370 | permissions | | | +| 69 242 | roles | | | +| 29 576 | objects | | | +| 7 021 | objects | hs\_booking.item | | +| 5 253 | login users | | | +| 4 818 | objects | hs\_office.coopassettx | | +| 3 212 | objects | hs\_office.coopsharetx | | +| 3 015 | objects | hs\_office.relation | | +| 2 017 | objects | hs\_office.person | | +| 2 006 | objects | hs\_booking.item | MANAGED\_WEBSPACE | +| 2 006 | objects | hs\_booking.item | MANAGED\_SERVER | +| 2 006 | objects | hs\_booking.item | CLOUD\_SERVER | +| 1 620 | objects | hs\_hosting.asset | | +| 1 012 | objects | hs\_office.contact | | +| 1 008 | objects | hs\_office.bankaccount | | +| 1 005 | objects | hs\_office.partner | | +| 1 005 | objects | hs\_office.partner\_details | | +| 1 003 | objects | hs\_office.debitor | | +| 1 003 | objects | hs\_booking.item | PRIVATE\_CLOUD | +| 1 003 | objects | hs\_booking.project | | +| 1 003 | objects | hs\_office.sepamandate | | +| 803 | objects | hs\_office.membership | | +| 180 | objects | hs\_hosting.asset | UNIX\_USER | +| 90 | objects | hs\_hosting.asset | DOMAIN\_SMTP\_SETUP | +| 90 | objects | hs\_hosting.asset | EMAIL\_ADDRESS | +| 90 | objects | hs\_hosting.asset | CLOUD\_SERVER | +| 90 | objects | hs\_hosting.asset | PGSQL\_DATABASE | +| 90 | objects | hs\_hosting.asset | MANAGED\_WEBSPACE | +| 90 | objects | hs\_hosting.asset | DOMAIN\_SETUP | +| 90 | objects | hs\_hosting.asset | MARIADB\_USER | +| 90 | objects | hs\_hosting.asset | PGSQL\_USER | +| 90 | objects | hs\_hosting.asset | DOMAIN\_MBOX\_SETUP | +| 90 | objects | hs\_hosting.asset | DOMAIN\_HTTP\_SETUP | +| 90 | objects | hs\_hosting.asset | DOMAIN\_DNS\_SETUP | +| 90 | objects | hs\_hosting.asset | MANAGED\_SERVER | +| 90 | objects | hs\_hosting.asset | PGSQL\_INSTANCE | +| 90 | objects | hs\_hosting.asset | EMAIL\_ALIAS | +| 90 | objects | hs\_hosting.asset | MARIADB\_DATABASE | +| 90 | objects | hs\_hosting.asset | MARIADB\_INSTANCE | +| 18 | objects | rbactest.domain | | +| 9 | objects | rbactest.package | | +| 3 | objects | rbactest.customer | | +| 1 | objects | rbac.global | | + + +### Attachment: Query-Plan before + +``` +Aggregate (cost=1800394.72..1800394.73 rows=1 width=8) (actual time=801.557..801.594 rows=1 loops=1) + -> Hash Join (cost=1800381.15..1800393.04 rows=669 width=0) (actual time=794.549..800.418 rows=1003 loops=1) + Hash Cond: (target.bankaccountuuid = b.uuid) + -> Hash Join (cost=1800344.47..1800354.60 rows=669 width=16) (actual time=60.423..63.940 rows=1003 loops=1) + Hash Cond: (target.debitoruuid = d.uuid) + -> Sort (cost=1800308.90..1800310.57 rows=669 width=280) (actual time=56.670..57.796 rows=1003 loops=1) + Sort Key: target.validity + Sort Method: quicksort Memory: 87kB + CTE accessible_uuids + -> HashAggregate (cost=1799078.92..1799361.78 rows=28286 width=16) (never executed) + Group Key: perm.objectuuid + CTE recursive_grants + -> Recursive Union (cost=4655.41..1575457.36 rows=3199924 width=37) (never executed) + -> Subquery Scan on "*SELECT* 1" (cost=4655.41..4720.95 rows=6554 width=37) (never executed) + -> HashAggregate (cost=4655.41..4720.95 rows=6554 width=37) (never executed) + Group Key: "grant".descendantuuid, "grant".ascendantuuid + -> Bitmap Heap Scan on "grant" (cost=136.43..4622.27 rows=6629 width=37) (never executed) + Recheck Cond: (ascendantuuid = ANY (rbac.currentsubjectorassumedrolesuuids())) + Filter: assumed + -> Bitmap Index Scan on grant_ascendantuuid_idx (cost=0.00..134.77 rows=6715 width=0) (never executed) + Index Cond: (ascendantuuid = ANY (rbac.currentsubjectorassumedrolesuuids())) + -> Unique (cost=149882.00..153873.72 rows=319337 width=37) (never executed) + -> Sort (cost=149882.00..150680.35 rows=319337 width=37) (never executed) + Sort Key: g.descendantuuid, g.ascendantuuid, ((grants.level + 1)), (base.asserttrue((grants.level < 22), ('too many grant-levels: '::text (grants.level)::text))) + -> Merge Join (cost=6554.45..111954.57 rows=319337 width=37) (never executed) + Merge Cond: (g.ascendantuuid = grants.descendantuuid) + -> Index Scan using grant_ascendantuuid_idx on "grant" g (cost=0.42..16246.48 rows=215214 width=32) (never executed) + Filter: assumed + -> Sort (cost=6554.03..6717.88 rows=65540 width=20) (never executed) + Sort Key: grants.descendantuuid + -> WorkTable Scan on recursive_grants grants (cost=0.00..1310.80 rows=65540 width=20) (never executed) + CTE count_check + -> Result (cost=143996.60..143996.87 rows=1 width=1) (never executed) + InitPlan 2 + -> Aggregate (cost=71998.29..71998.30 rows=1 width=8) (never executed) + -> CTE Scan on recursive_grants (cost=0.00..63998.48 rows=3199924 width=0) (never executed) + InitPlan 3 + -> Aggregate (cost=71998.29..71998.30 rows=1 width=8) (never executed) + -> CTE Scan on recursive_grants recursive_grants_1 (cost=0.00..63998.48 rows=3199924 width=0) (never executed) + -> Hash Join (cost=2270.14..79353.40 rows=108518 width=16) (never executed) + Hash Cond: (recursive_grants_2.descendantuuid = perm.uuid) + -> CTE Scan on recursive_grants recursive_grants_2 (cost=0.00..63998.48 rows=3199924 width=16) (never executed) + -> Hash (cost=2230.14..2230.14 rows=3200 width=32) (never executed) + -> Hash Join (cost=80.55..2230.14 rows=3200 width=32) (never executed) + Hash Cond: (perm.objectuuid = obj.uuid) + -> Seq Scan on permission perm (cost=0.00..1763.70 rows=94370 width=32) (never executed) + -> Hash (cost=68.02..68.02 rows=1003 width=16) (never executed) + -> Nested Loop (cost=0.41..68.02 rows=1003 width=16) (never executed) + -> CTE Scan on count_check cc (cost=0.00..0.02 rows=1 width=0) (never executed) + Filter: valid + -> Index Only Scan using object_objecttable_uuid_key on object obj (cost=0.41..57.97 rows=1003 width=16) (never executed) + Index Cond: (objecttable = 'hs_office.sepamandate'::text) + Heap Fetches: 0 + -> Seq Scan on sepamandate target (cost=636.44..915.72 rows=669 width=280) (actual time=1.189..54.696 rows=1003 loops=1) + Filter: (rbac.hasglobaladminrole() OR (ANY (uuid = (hashed SubPlan 6).col1))) + SubPlan 6 + -> CTE Scan on accessible_uuids (cost=0.00..565.72 rows=28286 width=16) (never executed) + -> Hash (cost=23.03..23.03 rows=1003 width=16) (actual time=3.680..3.683 rows=1003 loops=1) + Buckets: 1024 Batches: 1 Memory Usage: 56kB + -> Seq Scan on debitor d (cost=0.00..23.03 rows=1003 width=16) (actual time=0.168..1.824 rows=1003 loops=1) + -> Hash (cost=24.08..24.08 rows=1008 width=16) (actual time=734.056..734.059 rows=1008 loops=1) + Buckets: 1024 Batches: 1 Memory Usage: 56kB + -> Seq Scan on bankaccount b (cost=0.00..24.08 rows=1008 width=16) (actual time=731.222..732.672 rows=1008 loops=1) +Planning Time: 3.765 ms +JIT: + Functions: 81 + Options: Inlining true, Optimization true, Expressions true, Deforming true + Timing: Generation 9.230 ms (Deform 3.886 ms), Inlining 201.353 ms, Optimization 312.388 ms, Emission 217.583 ms, Total 740.554 ms +Execution Time: 888.157 ms +``` diff --git a/src/main/resources/db/changelog/1-rbac/1054-rbac-context.sql b/src/main/resources/db/changelog/1-rbac/1054-rbac-context.sql index d43c644f..db0138ab 100644 --- a/src/main/resources/db/changelog/1-rbac/1054-rbac-context.sql +++ b/src/main/resources/db/changelog/1-rbac/1054-rbac-context.sql @@ -83,7 +83,7 @@ begin end; $$; -- ============================================================================ ---changeset michael.hoennig:rbac-context-CONTEXT-DEFINED endDelimiter:--// +--changeset michael.hoennig:rbac-context-CONTEXT-DEFINED runOnChange:true validCheckSum:ANY endDelimiter:--// -- ---------------------------------------------------------------------------- /* Callback which is called after the context has been (re-) defined. @@ -98,6 +98,7 @@ create or replace procedure base.contextDefined( language plpgsql as $$ declare currentSubjectUuid uuid; + currentSubjectHasGlobalAdminRole boolean; begin execute format('set local hsadminng.currentTask to %L', currentTask); @@ -111,6 +112,13 @@ begin execute format('set local hsadminng.currentSubjectOrAssumedRolesUuids to %L', (select array_to_string(rbac.determineCurrentSubjectOrAssumedRolesUuids(currentSubjectUuid, assumedRoles), ';'))); + if currentSubjectUuid is null then + currentSubjectHasGlobalAdminRole := false; + else + currentSubjectHasGlobalAdminRole := rbac.isGranted(array[currentSubjectUuid], rbac.findRoleId(rbac.global_ADMIN())); + end if; + execute format('set local hsadminng.isGlobalAdmin to %L', currentSubjectHasGlobalAdminRole::text); + raise notice 'Context defined as: %, %, %, [%]', currentTask, currentRequest, currentSubject, assumedRoles; end; $$; @@ -181,4 +189,3 @@ begin return string_to_array(currentSubjectOrAssumedRolesUuids, ';'); end; $$; --// - diff --git a/src/main/resources/db/changelog/1-rbac/1058-rbac-generators.sql b/src/main/resources/db/changelog/1-rbac/1058-rbac-generators.sql index b0df146a..b8d7d351 100644 --- a/src/main/resources/db/changelog/1-rbac/1058-rbac-generators.sql +++ b/src/main/resources/db/changelog/1-rbac/1058-rbac-generators.sql @@ -179,8 +179,10 @@ create or replace procedure rbac.generateRbacRestrictedView(targetTable text, or declare sql text; newColumns text; + functionName text; begin targetTable := lower(targetTable); + functionName := replace(targetTable, '.', '_'); if columnNames = '*' then columnNames := base.tableColumnNames(targetTable); end if; @@ -189,45 +191,62 @@ begin Creates a restricted view based on the 'SELECT' permission of the current subject. */ sql := format($sql$ + create or replace function rbac.select_%3$s_rv() + returns setof %1$s + language plpgsql + as $f$ + begin + if rbac.hasGlobalAdminRole() then + return query + select target.* + from %1$s as target + order by %2$s; + else + return query + with accessible_uuids as ( + with recursive + recursive_grants as + (select distinct rbac.grant.descendantuuid, + rbac.grant.ascendantuuid, + 1 as level, + true + from rbac.grant + where rbac.grant.assumed + and (rbac.grant.ascendantuuid = any (rbac.currentSubjectOrAssumedRolesUuids())) + union all + select distinct g.descendantuuid, + g.ascendantuuid, + grants.level + 1 as level, + base.assertTrue(grants.level < 22, 'too many grant-levels: ' || grants.level) + from rbac.grant g + join recursive_grants grants on grants.descendantuuid = g.ascendantuuid + where g.assumed), + grant_count AS ( + SELECT COUNT(*) AS grant_count FROM recursive_grants + ), + count_check as (select base.assertTrue((select count(*) as grant_count from recursive_grants) < 400000, + 'too many grants for current subjects: ' || (select count(*) as grant_count from recursive_grants)) + as valid) + select distinct perm.objectuuid + from recursive_grants + join rbac.permission perm on recursive_grants.descendantuuid = perm.uuid + join rbac.object obj on obj.uuid = perm.objectuuid + join count_check cc on cc.valid + where obj.objectTable = '%1$s' -- 'SELECT' permission is included in all other permissions + ) + select target.* + from %1$s as target + where target.uuid in (select * from accessible_uuids) + order by %2$s; + end if; + end; + $f$; + create or replace view %1$s_rv as - with accessible_uuids as ( - with recursive - recursive_grants as - (select distinct rbac.grant.descendantuuid, - rbac.grant.ascendantuuid, - 1 as level, - true - from rbac.grant - where rbac.grant.assumed - and (rbac.grant.ascendantuuid = any (rbac.currentSubjectOrAssumedRolesUuids())) - union all - select distinct g.descendantuuid, - g.ascendantuuid, - grants.level + 1 as level, - base.assertTrue(grants.level < 22, 'too many grant-levels: ' || grants.level) - from rbac.grant g - join recursive_grants grants on grants.descendantuuid = g.ascendantuuid - where g.assumed), - grant_count AS ( - SELECT COUNT(*) AS grant_count FROM recursive_grants - ), - count_check as (select base.assertTrue((select count(*) as grant_count from recursive_grants) < 400000, - 'too many grants for current subjects: ' || (select count(*) as grant_count from recursive_grants)) - as valid) - select distinct perm.objectuuid - from recursive_grants - join rbac.permission perm on recursive_grants.descendantuuid = perm.uuid - join rbac.object obj on obj.uuid = perm.objectuuid - join count_check cc on cc.valid - where obj.objectTable = '%1$s' -- 'SELECT' permission is included in all other permissions - ) - select target.* - from %1$s as target - where rbac.hasGlobalAdminRole() or target.uuid in (select * from accessible_uuids) - order by %2$s; + select * from rbac.select_%3$s_rv(); grant all privileges on %1$s_rv to ${HSADMINNG_POSTGRES_RESTRICTED_USERNAME}; - $sql$, targetTable, orderBy); + $sql$, targetTable, orderBy, functionName); execute sql; /** diff --git a/src/main/resources/db/changelog/1-rbac/1080-rbac-global.sql b/src/main/resources/db/changelog/1-rbac/1080-rbac-global.sql index f42b9d25..e1284470 100644 --- a/src/main/resources/db/changelog/1-rbac/1080-rbac-global.sql +++ b/src/main/resources/db/changelog/1-rbac/1080-rbac-global.sql @@ -23,23 +23,34 @@ grant select on rbac.global to ${HSADMINNG_POSTGRES_RESTRICTED_USERNAME}; -- ============================================================================ ---changeset michael.hoennig:rbac-global-IS-GLOBAL-ADMIN endDelimiter:--// +--changeset michael.hoennig:rbac-global-IS-GLOBAL-ADMIN runOnChange:true validCheckSum:ANY endDelimiter:--// -- ------------------------------------------------------------------ create or replace function rbac.isGlobalAdmin() returns boolean language plpgsql as $$ +declare + isGlobalAdmin text; begin - return rbac.isGranted(rbac.currentSubjectOrAssumedRolesUuids(), rbac.findRoleId(rbac.global_ADMIN())); + isGlobalAdmin := current_setting('hsadminng.isGlobalAdmin', true); + if isGlobalAdmin is not null then + return isGlobalAdmin::boolean; + end if; + + raise exception '`hsadminng.isGlobalAdmin` should have been set by `rbac.defineContext()`'; end; $$; --// -- ============================================================================ ---changeset michael.hoennig:rbac-global-HAS-GLOBAL-ADMIN-ROLE endDelimiter:--// +--changeset michael.hoennig:rbac-global-HAS-GLOBAL-ADMIN-ROLE runOnChange:true validCheckSum:ANY endDelimiter:--// -- ---------------------------------------------------------------------------- /* Returns true if the current user is a global admin and has no assumed role. + + ATTENTION: It's false if the global-admin role is assumed, + because the global admin role does not have the global admin role, but it is the global admin role. + The differentiation is important for the cases where this function is used. */ create or replace function rbac.hasGlobalAdminRole() returns boolean diff --git a/src/main/resources/db/changelog/2-rbactest/201-rbactest-customer/2013-rbactest-customer-rbac.sql b/src/main/resources/db/changelog/2-rbactest/201-rbactest-customer/2013-rbactest-customer-rbac.sql index 4d06857a..2fae25b3 100644 --- a/src/main/resources/db/changelog/2-rbactest/201-rbactest-customer/2013-rbactest-customer-rbac.sql +++ b/src/main/resources/db/changelog/2-rbactest/201-rbactest-customer/2013-rbactest-customer-rbac.sql @@ -167,6 +167,7 @@ call rbac.generateRbacIdentityViewFromProjection('rbactest.customer', -- ============================================================================ --changeset RbacRestrictedViewGenerator:rbactest-customer-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--// -- ---------------------------------------------------------------------------- +-- trigger change of change in generateRbacRestrictedView regarding #453 optimization for global:ADMIN call rbac.generateRbacRestrictedView('rbactest.customer', $orderBy$ reference diff --git a/src/main/resources/db/changelog/2-rbactest/202-rbactest-package/2023-rbactest-package-rbac.sql b/src/main/resources/db/changelog/2-rbactest/202-rbactest-package/2023-rbactest-package-rbac.sql index b5bf393b..8748f611 100644 --- a/src/main/resources/db/changelog/2-rbactest/202-rbactest-package/2023-rbactest-package-rbac.sql +++ b/src/main/resources/db/changelog/2-rbactest/202-rbactest-package/2023-rbactest-package-rbac.sql @@ -232,6 +232,7 @@ call rbac.generateRbacIdentityViewFromProjection('rbactest.package', -- ============================================================================ --changeset RbacRestrictedViewGenerator:rbactest-package-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--// -- ---------------------------------------------------------------------------- +-- trigger change of change in generateRbacRestrictedView regarding #453 optimization for global:ADMIN call rbac.generateRbacRestrictedView('rbactest.package', $orderBy$ name diff --git a/src/main/resources/db/changelog/2-rbactest/203-rbactest-domain/2033-rbactest-domain-rbac.sql b/src/main/resources/db/changelog/2-rbactest/203-rbactest-domain/2033-rbactest-domain-rbac.sql index 7ad279bb..28336cb6 100644 --- a/src/main/resources/db/changelog/2-rbactest/203-rbactest-domain/2033-rbactest-domain-rbac.sql +++ b/src/main/resources/db/changelog/2-rbactest/203-rbactest-domain/2033-rbactest-domain-rbac.sql @@ -231,6 +231,7 @@ call rbac.generateRbacIdentityViewFromProjection('rbactest.domain', -- ============================================================================ --changeset RbacRestrictedViewGenerator:rbactest-domain-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--// -- ---------------------------------------------------------------------------- +-- trigger change of change in generateRbacRestrictedView regarding #453 optimization for global:ADMIN call rbac.generateRbacRestrictedView('rbactest.domain', $orderBy$ name diff --git a/src/main/resources/db/changelog/5-hs-office/501-contact/5013-hs-office-contact-rbac.sql b/src/main/resources/db/changelog/5-hs-office/501-contact/5013-hs-office-contact-rbac.sql index a1fe10d5..d9e9f0a3 100644 --- a/src/main/resources/db/changelog/5-hs-office/501-contact/5013-hs-office-contact-rbac.sql +++ b/src/main/resources/db/changelog/5-hs-office/501-contact/5013-hs-office-contact-rbac.sql @@ -90,6 +90,7 @@ call rbac.generateRbacIdentityViewFromProjection('hs_office.contact', -- ============================================================================ --changeset RbacRestrictedViewGenerator:hs-office-contact-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--// -- ---------------------------------------------------------------------------- +-- trigger change of change in generateRbacRestrictedView regarding #453 optimization for global:ADMIN call rbac.generateRbacRestrictedView('hs_office.contact', $orderBy$ caption diff --git a/src/main/resources/db/changelog/5-hs-office/502-person/5023-hs-office-person-rbac.sql b/src/main/resources/db/changelog/5-hs-office/502-person/5023-hs-office-person-rbac.sql index 33e974f2..6be50e0d 100644 --- a/src/main/resources/db/changelog/5-hs-office/502-person/5023-hs-office-person-rbac.sql +++ b/src/main/resources/db/changelog/5-hs-office/502-person/5023-hs-office-person-rbac.sql @@ -90,6 +90,7 @@ call rbac.generateRbacIdentityViewFromProjection('hs_office.person', -- ============================================================================ --changeset RbacRestrictedViewGenerator:hs-office-person-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--// -- ---------------------------------------------------------------------------- +-- trigger change of change in generateRbacRestrictedView regarding #453 optimization for global:ADMIN call rbac.generateRbacRestrictedView('hs_office.person', $orderBy$ concat(tradeName, familyName, givenName) diff --git a/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.sql b/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.sql index 8e1507d4..100a9fba 100644 --- a/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.sql +++ b/src/main/resources/db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.sql @@ -245,6 +245,7 @@ call rbac.generateRbacIdentityViewFromProjection('hs_office.relation', -- ============================================================================ --changeset RbacRestrictedViewGenerator:hs-office-relation-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--// -- ---------------------------------------------------------------------------- +-- trigger change of change in generateRbacRestrictedView regarding #453 optimization for global:ADMIN call rbac.generateRbacRestrictedView('hs_office.relation', $orderBy$ (select idName from hs_office.person_iv p where p.uuid = target.holderUuid) diff --git a/src/main/resources/db/changelog/5-hs-office/504-partner/5043-hs-office-partner-rbac.sql b/src/main/resources/db/changelog/5-hs-office/504-partner/5043-hs-office-partner-rbac.sql index 4903d6ef..c0186a1e 100644 --- a/src/main/resources/db/changelog/5-hs-office/504-partner/5043-hs-office-partner-rbac.sql +++ b/src/main/resources/db/changelog/5-hs-office/504-partner/5043-hs-office-partner-rbac.sql @@ -244,6 +244,7 @@ call rbac.generateRbacIdentityViewFromProjection('hs_office.partner', -- ============================================================================ --changeset RbacRestrictedViewGenerator:hs-office-partner-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--// -- ---------------------------------------------------------------------------- +-- trigger change of change in generateRbacRestrictedView regarding #453 optimization for global:ADMIN call rbac.generateRbacRestrictedView('hs_office.partner', $orderBy$ 'P-' || partnerNumber diff --git a/src/main/resources/db/changelog/5-hs-office/504-partner/5044-hs-office-partner-details-rbac.sql b/src/main/resources/db/changelog/5-hs-office/504-partner/5044-hs-office-partner-details-rbac.sql index 743a45f9..f1494bae 100644 --- a/src/main/resources/db/changelog/5-hs-office/504-partner/5044-hs-office-partner-details-rbac.sql +++ b/src/main/resources/db/changelog/5-hs-office/504-partner/5044-hs-office-partner-details-rbac.sql @@ -151,6 +151,7 @@ call rbac.generateRbacIdentityViewFromQuery('hs_office.partner_details', -- ============================================================================ --changeset RbacRestrictedViewGenerator:hs-office-partner-details-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--// -- ---------------------------------------------------------------------------- +-- trigger change of change in generateRbacRestrictedView regarding #453 optimization for global:ADMIN call rbac.generateRbacRestrictedView('hs_office.partner_details', $orderBy$ uuid diff --git a/src/main/resources/db/changelog/5-hs-office/505-bankaccount/5053-hs-office-bankaccount-rbac.sql b/src/main/resources/db/changelog/5-hs-office/505-bankaccount/5053-hs-office-bankaccount-rbac.sql index 6a98ca04..73ec2363 100644 --- a/src/main/resources/db/changelog/5-hs-office/505-bankaccount/5053-hs-office-bankaccount-rbac.sql +++ b/src/main/resources/db/changelog/5-hs-office/505-bankaccount/5053-hs-office-bankaccount-rbac.sql @@ -90,6 +90,7 @@ call rbac.generateRbacIdentityViewFromProjection('hs_office.bankaccount', -- ============================================================================ --changeset RbacRestrictedViewGenerator:hs-office-bankaccount-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--// -- ---------------------------------------------------------------------------- +-- trigger change of change in generateRbacRestrictedView regarding #453 optimization for global:ADMIN call rbac.generateRbacRestrictedView('hs_office.bankaccount', $orderBy$ iban diff --git a/src/main/resources/db/changelog/5-hs-office/506-debitor/5063-hs-office-debitor-rbac.sql b/src/main/resources/db/changelog/5-hs-office/506-debitor/5063-hs-office-debitor-rbac.sql index 674581d7..dc222a6e 100644 --- a/src/main/resources/db/changelog/5-hs-office/506-debitor/5063-hs-office-debitor-rbac.sql +++ b/src/main/resources/db/changelog/5-hs-office/506-debitor/5063-hs-office-debitor-rbac.sql @@ -226,6 +226,7 @@ call rbac.generateRbacIdentityViewFromQuery('hs_office.debitor', -- ============================================================================ --changeset RbacRestrictedViewGenerator:hs-office-debitor-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--// -- ---------------------------------------------------------------------------- +-- trigger change of change in generateRbacRestrictedView regarding #453 optimization for global:ADMIN call rbac.generateRbacRestrictedView('hs_office.debitor', $orderBy$ defaultPrefix diff --git a/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5073-hs-office-sepamandate-rbac.sql b/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5073-hs-office-sepamandate-rbac.sql index 7448ce98..025bc251 100644 --- a/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5073-hs-office-sepamandate-rbac.sql +++ b/src/main/resources/db/changelog/5-hs-office/507-sepamandate/5073-hs-office-sepamandate-rbac.sql @@ -200,6 +200,7 @@ call rbac.generateRbacIdentityViewFromQuery('hs_office.sepamandate', -- ============================================================================ --changeset RbacRestrictedViewGenerator:hs-office-sepamandate-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--// -- ---------------------------------------------------------------------------- +-- trigger change of change in generateRbacRestrictedView regarding #453 optimization for global:ADMIN call rbac.generateRbacRestrictedView('hs_office.sepamandate', $orderBy$ validity diff --git a/src/main/resources/db/changelog/5-hs-office/510-membership/5103-hs-office-membership-rbac.sql b/src/main/resources/db/changelog/5-hs-office/510-membership/5103-hs-office-membership-rbac.sql index e50ddf83..80d7e655 100644 --- a/src/main/resources/db/changelog/5-hs-office/510-membership/5103-hs-office-membership-rbac.sql +++ b/src/main/resources/db/changelog/5-hs-office/510-membership/5103-hs-office-membership-rbac.sql @@ -182,6 +182,7 @@ call rbac.generateRbacIdentityViewFromQuery('hs_office.membership', -- ============================================================================ --changeset RbacRestrictedViewGenerator:hs-office-membership-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--// -- ---------------------------------------------------------------------------- +-- trigger change of change in generateRbacRestrictedView regarding #453 optimization for global:ADMIN call rbac.generateRbacRestrictedView('hs_office.membership', $orderBy$ validity diff --git a/src/main/resources/db/changelog/5-hs-office/511-coopshares/5113-hs-office-coopshares-rbac.sql b/src/main/resources/db/changelog/5-hs-office/511-coopshares/5113-hs-office-coopshares-rbac.sql index 9f0a0812..e0c21410 100644 --- a/src/main/resources/db/changelog/5-hs-office/511-coopshares/5113-hs-office-coopshares-rbac.sql +++ b/src/main/resources/db/changelog/5-hs-office/511-coopshares/5113-hs-office-coopshares-rbac.sql @@ -155,6 +155,7 @@ call rbac.generateRbacIdentityViewFromProjection('hs_office.coopsharetx', -- ============================================================================ --changeset RbacRestrictedViewGenerator:hs-office-coopsharetx-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--// -- ---------------------------------------------------------------------------- +-- trigger change of change in generateRbacRestrictedView regarding #453 optimization for global:ADMIN call rbac.generateRbacRestrictedView('hs_office.coopsharetx', $orderBy$ reference diff --git a/src/main/resources/db/changelog/5-hs-office/512-coopassets/5123-hs-office-coopassets-rbac.sql b/src/main/resources/db/changelog/5-hs-office/512-coopassets/5123-hs-office-coopassets-rbac.sql index 69736f09..4b4e0870 100644 --- a/src/main/resources/db/changelog/5-hs-office/512-coopassets/5123-hs-office-coopassets-rbac.sql +++ b/src/main/resources/db/changelog/5-hs-office/512-coopassets/5123-hs-office-coopassets-rbac.sql @@ -155,6 +155,7 @@ call rbac.generateRbacIdentityViewFromProjection('hs_office.coopassettx', -- ============================================================================ --changeset RbacRestrictedViewGenerator:hs-office-coopassettx-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--// -- ---------------------------------------------------------------------------- +-- trigger change of change in generateRbacRestrictedView regarding #453 optimization for global:ADMIN call rbac.generateRbacRestrictedView('hs_office.coopassettx', $orderBy$ reference diff --git a/src/main/resources/db/changelog/6-hs-booking/620-booking-project/6203-hs-booking-project-rbac.sql b/src/main/resources/db/changelog/6-hs-booking/620-booking-project/6203-hs-booking-project-rbac.sql index 96784255..a1de8e1c 100644 --- a/src/main/resources/db/changelog/6-hs-booking/620-booking-project/6203-hs-booking-project-rbac.sql +++ b/src/main/resources/db/changelog/6-hs-booking/620-booking-project/6203-hs-booking-project-rbac.sql @@ -194,6 +194,7 @@ call rbac.generateRbacIdentityViewFromQuery('hs_booking.project', -- ============================================================================ --changeset RbacRestrictedViewGenerator:hs-booking-project-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--// -- ---------------------------------------------------------------------------- +-- trigger change of change in generateRbacRestrictedView regarding #453 optimization for global:ADMIN call rbac.generateRbacRestrictedView('hs_booking.project', $orderBy$ caption diff --git a/src/main/resources/db/changelog/6-hs-booking/630-booking-item/6303-hs-booking-item-rbac.sql b/src/main/resources/db/changelog/6-hs-booking/630-booking-item/6303-hs-booking-item-rbac.sql index 2ca3104c..b115d98f 100644 --- a/src/main/resources/db/changelog/6-hs-booking/630-booking-item/6303-hs-booking-item-rbac.sql +++ b/src/main/resources/db/changelog/6-hs-booking/630-booking-item/6303-hs-booking-item-rbac.sql @@ -263,6 +263,7 @@ call rbac.generateRbacIdentityViewFromProjection('hs_booking.item', -- ============================================================================ --changeset RbacRestrictedViewGenerator:hs-booking-item-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--// -- ---------------------------------------------------------------------------- +-- trigger change of change in generateRbacRestrictedView regarding #453 optimization for global:ADMIN call rbac.generateRbacRestrictedView('hs_booking.item', $orderBy$ validity diff --git a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.sql b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.sql index b0e6efa1..e6253be7 100644 --- a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.sql +++ b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.sql @@ -168,6 +168,7 @@ call rbac.generateRbacIdentityViewFromProjection('hs_hosting.asset', -- ============================================================================ --changeset RbacRestrictedViewGenerator:hs-hosting-asset-rbac-RESTRICTED-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--// -- ---------------------------------------------------------------------------- +-- trigger change of change in generateRbacRestrictedView regarding #453 optimization for global:ADMIN call rbac.generateRbacRestrictedView('hs_hosting.asset', $orderBy$ identifier diff --git a/src/main/resources/db/changelog/9-hs-global/9520-hs-mass-test-data-generators.sql b/src/main/resources/db/changelog/9-hs-global/9520-hs-mass-test-data-generators.sql new file mode 100644 index 00000000..d6349fb1 --- /dev/null +++ b/src/main/resources/db/changelog/9-hs-global/9520-hs-mass-test-data-generators.sql @@ -0,0 +1,586 @@ +--liquibase formatted sql + + +-- ============================================================================ +--changeset michael.hoennig:hs-mass-test-data-GENERATORS context:!without-test-data endDelimiter:--// +-- ---------------------------------------------------------------------------- + +/* + Loop-based mass test-data generators for office/booking/hosting/accounts. + */ +create or replace procedure hs_office.contact_create_mass_test_data( + startCount integer, + endCount integer, + captionPrefix varchar default 'mass contact ' +) + language plpgsql as $$ +begin + for t in startCount..endCount + loop + call base.defineContext('mass contact test-data #' || t, null, 'superuser-alex@hostsharing.net', 'rbac.global#global:ADMIN'); + call hs_office.contact_create_test_data(captionPrefix || base.intToVarChar(t, 4)); + commit; + end loop; +end; $$; +--// + +create or replace procedure hs_office.person_create_mass_test_data( + startCount integer, + endCount integer +) + language plpgsql as $$ +declare + idx varchar; +begin + for t in startCount..endCount + loop + call base.defineContext('mass person test-data #' || t, null, 'superuser-alex@hostsharing.net', 'rbac.global#global:ADMIN'); + idx := base.intToVarChar(t, 4); + if t % 5 = 0 then + call hs_office.person_create_test_data('NP', null, 'MassFamily' || idx, 'MassGiven' || idx, true); + else + call hs_office.person_create_test_data('LP', 'Mass Partner ' || idx || ' GmbH', null, null, true); + end if; + call hs_office.person_create_test_data('NP', null, 'MassRep' || idx, 'User' || idx, true); + commit; + end loop; +end; $$; +--// + +create or replace procedure hs_office.relation_create_mass_test_data( + startCount integer, + endCount integer, + mandantTradeName varchar default 'Hostsharing eG', + contactCaptionPrefix varchar default 'mass contact ' +) + language plpgsql as $$ +declare + idx varchar; + partnerPersonName varchar; + representativeFamilyName varchar; + contactCaption varchar; + mandantPerson hs_office.person; + partnerPerson hs_office.person; + representativePerson hs_office.person; + contact hs_office.contact; +begin + select p.* into mandantPerson from hs_office.person p where p.tradeName = mandantTradeName; + if mandantPerson is null then + raise exception 'mandant "%" not found', mandantTradeName; + end if; + + for t in startCount..endCount + loop + call base.defineContext('mass relation test-data #' || t, null, 'superuser-alex@hostsharing.net', 'rbac.global#global:ADMIN'); + idx := base.intToVarChar(t, 4); + partnerPersonName := case when t % 5 = 0 then 'MassFamily' || idx else 'Mass Partner ' || idx || ' GmbH' end; + representativeFamilyName := 'MassRep' || idx; + contactCaption := contactCaptionPrefix || idx; + + select p.* into partnerPerson + from hs_office.person p + where p.tradeName = partnerPersonName or p.familyName = partnerPersonName; + select p.* into representativePerson from hs_office.person p where p.familyName = representativeFamilyName; + select c.* into contact from hs_office.contact c where c.caption = contactCaption; + + if partnerPerson is null or representativePerson is null or contact is null then + raise exception 'missing mass test base data for index %', idx; + end if; + + if not exists ( + select 1 from hs_office.relation r + where r.type = 'PARTNER' and r.anchorUuid = mandantPerson.uuid and r.holderUuid = partnerPerson.uuid + ) then + call hs_office.relation_create_test_data(partnerPersonName, 'PARTNER', mandantTradeName, contactCaption); + end if; + + if not exists ( + select 1 from hs_office.relation r + where r.type = 'REPRESENTATIVE' and r.anchorUuid = partnerPerson.uuid and r.holderUuid = representativePerson.uuid and r.contactUuid = contact.uuid + ) then + call hs_office.relation_create_test_data(representativeFamilyName, 'REPRESENTATIVE', partnerPersonName, contactCaption); + end if; + + if not exists ( + select 1 from hs_office.relation r + where r.type = 'DEBITOR' and r.anchorUuid = partnerPerson.uuid and r.holderUuid = partnerPerson.uuid and r.contactUuid = contact.uuid + ) then + call hs_office.relation_create_test_data(partnerPersonName, 'DEBITOR', partnerPersonName, contactCaption); + end if; + + commit; + end loop; +end; $$; +--// + +create or replace procedure hs_office.partner_create_mass_test_data( + startPartnerNumber numeric(5), + endPartnerNumber numeric(5), + mandantTradeName varchar default 'Hostsharing eG', + contactCaptionPrefix varchar default 'mass contact ' +) + language plpgsql as $$ +declare + t integer; + idx varchar; + partnerPersonName varchar; + contactCaption varchar; +begin + for t in startPartnerNumber::integer..endPartnerNumber::integer + loop + call base.defineContext('mass partner test-data #' || t, null, 'superuser-alex@hostsharing.net', 'rbac.global#global:ADMIN'); + idx := base.intToVarChar(t, 4); + partnerPersonName := case when t % 5 = 0 then 'MassFamily' || idx else 'Mass Partner ' || idx || ' GmbH' end; + contactCaption := contactCaptionPrefix || idx; + + if not exists (select 1 from hs_office.partner p where p.partnerNumber = t) then + call hs_office.partner_create_test_data(mandantTradeName, t, partnerPersonName, contactCaption); + end if; + commit; + end loop; +end; $$; +--// + +create or replace procedure hs_office.bankaccount_create_mass_test_data( + startPartnerNumber numeric(5), + endPartnerNumber numeric(5) +) + language plpgsql as $$ +declare + t integer; + idx varchar; + v_holder varchar; + v_iban varchar; + v_bic varchar; +begin + for t in startPartnerNumber::integer..endPartnerNumber::integer + loop + call base.defineContext('mass bankaccount test-data #' || t, null, 'superuser-alex@hostsharing.net', 'rbac.global#global:ADMIN'); + idx := base.intToVarChar(t, 4); + v_holder := case when t % 5 = 0 then 'MassFamily' || idx else 'Mass Partner ' || idx || ' GmbH' end; + v_iban := 'DE' || lpad(t::text, 20, '0'); + v_bic := 'MASSDEFF' || lpad((t % 1000)::text, 3, '0'); + + if not exists ( + select 1 from hs_office.bankaccount b where b.holder = v_holder and b.iban = v_iban + ) then + call hs_office.bankaccount_create_test_data(v_holder, v_iban, v_bic); + end if; + commit; + end loop; +end; $$; +--// + +create or replace procedure hs_office.debitor_create_mass_test_data( + startPartnerNumber numeric(5), + endPartnerNumber numeric(5), + contactCaptionPrefix varchar default 'mass contact ' +) + language plpgsql as $$ +declare + t integer; + idx varchar; + partnerPersonName varchar; + suffixNum integer; + suffixText char(2); + defaultPrefix char(3); +begin + for t in startPartnerNumber::integer..endPartnerNumber::integer + loop + call base.defineContext('mass debitor test-data #' || t, null, 'superuser-alex@hostsharing.net', 'rbac.global#global:ADMIN'); + idx := base.intToVarChar(t, 4); + partnerPersonName := case when t % 5 = 0 then 'MassFamily' || idx else 'Mass Partner ' || idx || ' GmbH' end; + suffixNum := 10 + (t % 90); + suffixText := lpad(suffixNum::text, 2, '0'); + defaultPrefix := lower( + chr(97 + ((t / 676) % 26)) || + chr(97 + ((t / 26) % 26)) || + chr(97 + (t % 26)) + ); + + if not exists ( + select 1 + from hs_office.debitor d + join hs_office.relation debitorRel on debitorRel.uuid = d.debitorRelUuid and debitorRel.type = 'DEBITOR' + join hs_office.relation partnerRel on partnerRel.holderUuid = debitorRel.anchorUuid and partnerRel.type = 'PARTNER' + join hs_office.partner p on p.partnerRelUuid = partnerRel.uuid + where p.partnerNumber = t and d.debitorNumberSuffix = suffixText + ) then + call hs_office.debitor_create_test_data(suffixNum, partnerPersonName, contactCaptionPrefix || idx, defaultPrefix); + end if; + commit; + end loop; +end; $$; +--// + +create or replace procedure hs_office.sepamandate_create_mass_test_data( + startPartnerNumber numeric(5), + endPartnerNumber numeric(5) +) + language plpgsql as $$ +declare + t integer; + suffixText char(2); + iban varchar; +begin + for t in startPartnerNumber::integer..endPartnerNumber::integer + loop + call base.defineContext('mass sepa-mandate test-data #' || t, null, 'superuser-alex@hostsharing.net', 'rbac.global#global:ADMIN'); + suffixText := lpad((10 + (t % 90))::text, 2, '0'); + iban := 'DE' || lpad(t::text, 20, '0'); + + if not exists ( + select 1 + from hs_office.sepamandate sm + join hs_office.debitor d on d.uuid = sm.debitorUuid + join hs_office.relation debitorRel on debitorRel.uuid = d.debitorRelUuid + join hs_office.relation partnerRel on partnerRel.holderUuid = debitorRel.anchorUuid + join hs_office.partner p on p.partnerRelUuid = partnerRel.uuid + where p.partnerNumber = t and d.debitorNumberSuffix = suffixText + ) then + call hs_office.sepamandate_create_test_data(t, suffixText, iban, 'mass-ref-' || t::text || '-' || suffixText); + end if; + commit; + end loop; +end; $$; +--// + +create or replace procedure hs_office.membership_create_mass_test_data( + startPartnerNumber numeric(5), + endPartnerNumber numeric(5), + withMembershipPercentage integer default 80 +) + language plpgsql as $$ +declare + t integer; + memberSuffix char(2); +begin + if withMembershipPercentage < 0 or withMembershipPercentage > 100 then + raise exception 'withMembershipPercentage must be between 0 and 100'; + end if; + + for t in startPartnerNumber::integer..endPartnerNumber::integer + loop + call base.defineContext('mass membership test-data #' || t, null, 'superuser-alex@hostsharing.net', 'rbac.global#global:ADMIN'); + if (t % 100) < withMembershipPercentage then + memberSuffix := lpad((10 + (t % 90))::text, 2, '0'); + call hs_office.membership_create_test_data(t, memberSuffix, daterange('20221001', null, '[]'), 'ACTIVE'); + end if; + commit; + end loop; +end; $$; +--// + +create or replace procedure hs_office.coopsharetx_create_mass_test_data( + startPartnerNumber numeric(5), + endPartnerNumber numeric(5), + withMembershipPercentage integer default 80 +) + language plpgsql as $$ +declare + t integer; + memberSuffix char(2); + v_membershipUuid uuid; +begin + for t in startPartnerNumber::integer..endPartnerNumber::integer + loop + call base.defineContext('mass coop-sharetx test-data #' || t, null, 'superuser-alex@hostsharing.net', 'rbac.global#global:ADMIN'); + if (t % 100) < withMembershipPercentage then + memberSuffix := lpad((10 + (t % 90))::text, 2, '0'); + select m.uuid into v_membershipUuid + from hs_office.membership m + join hs_office.partner p on p.uuid = m.partnerUuid + where p.partnerNumber = t and m.memberNumberSuffix = memberSuffix; + + if v_membershipUuid is not null and not exists ( + select 1 from hs_office.coopsharetx tx where tx.membershipUuid = v_membershipUuid + ) then + call hs_office.coopsharetx_create_test_data(t, memberSuffix); + end if; + end if; + commit; + end loop; +end; $$; +--// + +create or replace procedure hs_office.coopassettx_create_mass_test_data( + startPartnerNumber numeric(5), + endPartnerNumber numeric(5), + withMembershipPercentage integer default 80 +) + language plpgsql as $$ +declare + t integer; + memberSuffix char(2); + v_membershipUuid uuid; +begin + for t in startPartnerNumber::integer..endPartnerNumber::integer + loop + call base.defineContext('mass coop-assettx test-data #' || t, null, 'superuser-alex@hostsharing.net', 'rbac.global#global:ADMIN'); + if (t % 100) < withMembershipPercentage then + memberSuffix := lpad((10 + (t % 90))::text, 2, '0'); + select m.uuid into v_membershipUuid + from hs_office.membership m + join hs_office.partner p on p.uuid = m.partnerUuid + where p.partnerNumber = t and m.memberNumberSuffix = memberSuffix; + + if v_membershipUuid is not null and not exists ( + select 1 from hs_office.coopassettx tx where tx.membershipUuid = v_membershipUuid + ) then + call hs_office.coopassettx_create_test_data(t, memberSuffix); + end if; + end if; + commit; + end loop; +end; $$; +--// + +create or replace procedure hs_booking.project_create_mass_test_data( + startPartnerNumber numeric(5), + endPartnerNumber numeric(5) +) + language plpgsql as $$ +declare + t integer; + suffixText char(2); +begin + for t in startPartnerNumber::integer..endPartnerNumber::integer + loop + call base.defineContext('mass booking-project test-data #' || t, null, 'superuser-alex@hostsharing.net', 'rbac.global#global:ADMIN'); + suffixText := lpad((10 + (t % 90))::text, 2, '0'); + if not exists ( + select 1 from hs_booking.project p where p.caption = 'D-' || t::text || suffixText || ' default project' + ) then + call hs_booking.project_create_test_data(t, suffixText); + end if; + commit; + end loop; +end; $$; +--// + +create or replace procedure hs_booking.item_create_mass_test_data( + startPartnerNumber numeric(5), + endPartnerNumber numeric(5) +) + language plpgsql as $$ +declare + t integer; + suffixText char(2); + v_projectUuid uuid; +begin + for t in startPartnerNumber::integer..endPartnerNumber::integer + loop + call base.defineContext('mass booking-item test-data #' || t, null, 'superuser-alex@hostsharing.net', 'rbac.global#global:ADMIN'); + suffixText := lpad((10 + (t % 90))::text, 2, '0'); + select p.uuid into v_projectUuid from hs_booking.project p where p.caption = 'D-' || t::text || suffixText || ' default project'; + if v_projectUuid is not null and not exists ( + select 1 from hs_booking.item i where i.projectUuid = v_projectUuid + ) then + call hs_booking.item_create_test_data(t, suffixText); + end if; + commit; + end loop; +end; $$; +--// + +create or replace procedure hs_hosting.asset_create_mass_test_data( + startPartnerNumber numeric(5), + endPartnerNumber numeric(5) +) + language plpgsql as $$ +declare + t integer; + suffixText char(2); + projectCaption varchar; + v_debitorNumberSuffix char(2); + v_defaultPrefix char(3); +begin + for t in startPartnerNumber::integer..endPartnerNumber::integer + loop + call base.defineContext('mass hosting-asset test-data #' || t, null, 'superuser-alex@hostsharing.net', 'rbac.global#global:ADMIN'); + suffixText := lpad((10 + (t % 90))::text, 2, '0'); + projectCaption := 'D-' || t::text || suffixText || ' default project'; + select d.debitorNumberSuffix, d.defaultPrefix + into v_debitorNumberSuffix, v_defaultPrefix + from hs_booking.project p + join hs_office.debitor d on d.uuid = p.debitorUuid + where p.caption = projectCaption; + if v_debitorNumberSuffix is not null + and not exists ( + select 1 + from hs_hosting.asset a + join hs_booking.item i on i.uuid = a.bookingItemUuid + join hs_booking.project p on p.uuid = i.projectUuid + where p.caption = projectCaption + ) + and not exists ( + select 1 from hs_hosting.asset a + where (a.type = 'MANAGED_SERVER' and a.identifier = 'vm10' || v_debitorNumberSuffix) + or (a.type = 'CLOUD_SERVER' and a.identifier = 'vm20' || v_debitorNumberSuffix) + or (a.type = 'MANAGED_WEBSPACE' and a.identifier = v_defaultPrefix || '01') + or (a.type = 'MARIADB_INSTANCE' and a.identifier = 'vm10' || v_debitorNumberSuffix || '.MariaDB.default') + or (a.type = 'MARIADB_USER' and a.identifier = v_defaultPrefix || '01_web') + or (a.type = 'MARIADB_DATABASE' and a.identifier = v_defaultPrefix || '01_web') + or (a.type = 'PGSQL_INSTANCE' and a.identifier = 'vm10' || v_debitorNumberSuffix || '.Postgresql.default') + or (a.type = 'PGSQL_USER' and a.identifier = v_defaultPrefix || '01_web') + or (a.type = 'PGSQL_DATABASE' and a.identifier = v_defaultPrefix || '01_web') + or (a.type = 'EMAIL_ALIAS' and a.identifier = v_defaultPrefix || '01-web') + or (a.type = 'UNIX_USER' and a.identifier = v_defaultPrefix || '01-web') + or (a.type = 'UNIX_USER' and a.identifier = v_defaultPrefix || '01-mbox') + or (a.type = 'DOMAIN_SETUP' and a.identifier = v_defaultPrefix || '.example.org') + or (a.type = 'DOMAIN_DNS_SETUP' and a.identifier = v_defaultPrefix || '.example.org|DNS') + or (a.type = 'DOMAIN_HTTP_SETUP' and a.identifier = v_defaultPrefix || '.example.org|HTTP') + or (a.type = 'DOMAIN_SMTP_SETUP' and a.identifier = v_defaultPrefix || '.example.org|SMTP') + or (a.type = 'DOMAIN_MBOX_SETUP' and a.identifier = v_defaultPrefix || '.example.org|MBOX') + or (a.type = 'EMAIL_ADDRESS' and a.identifier = 'test@' || v_defaultPrefix || '.example.org') + ) then + call hs_hosting.asset_create_test_data(projectCaption); + end if; + commit; + end loop; +end; $$; +--// + +create or replace procedure hs_office.person_create_mass_test_data_for_accounts( + startCount integer, + endCount integer +) + language plpgsql as $$ +declare + t integer; + idx varchar; +begin + for t in startCount..endCount + loop + call base.defineContext('mass account-person test-data #' || t, null, 'superuser-alex@hostsharing.net', 'rbac.global#global:ADMIN'); + idx := base.intToVarChar(t, 4); + call hs_office.person_create_test_data('NP', null, 'MassAccountFamily' || idx, 'MassAccountGiven' || idx, true); + commit; + end loop; +end; $$; +--// + +create or replace procedure hs_accounts.account_create_mass_test_data( + startCount integer, + endCount integer, + emailPrefix varchar default 'mass-account-', + uidOffset integer default 200000 +) + language plpgsql as $$ +declare + t integer; + idx varchar; + accountEmail varchar; + subjectUuid uuid; + personUuid uuid; +begin + for t in startCount..endCount + loop + call base.defineContext('mass profile test-data #' || t, null, 'superuser-alex@hostsharing.net', 'rbac.global#global:ADMIN'); + idx := base.intToVarChar(t, 4); + accountEmail := emailPrefix || idx || '@example.com'; + select p.uuid into personUuid + from hs_office.person p + where p.familyName = 'MassAccountFamily' || idx and p.givenName = 'MassAccountGiven' || idx; + + if personUuid is not null and not exists ( + select 1 from hs_accounts.account pr where pr.person_uuid = personUuid + ) then + perform rbac.create_subject(accountEmail); + select s.uuid into subjectUuid from rbac.subject s where s.name = accountEmail; + + insert into hs_accounts.account ( + uuid, version, person_uuid, + global_uid, global_gid + ) values ( + subjectUuid, 0, personUuid, + uidOffset + t, uidOffset + t + ); + end if; + commit; + end loop; +end; $$; +--// + +create or replace procedure hs_office.partner_create_mass_bundle_test_data( + startPartnerNumber numeric(5), + endPartnerNumber numeric(5), + withMembershipPercentage integer default 80 +) + language plpgsql as $$ +declare + t integer; + idx varchar; + personUuid uuid; + accountEmail varchar; + subjectUuid uuid; +begin + call base.defineContext('creating mass partner bundle test-data', null, 'superuser-alex@hostsharing.net', 'rbac.global#global:ADMIN'); + set constraints all deferred; + + call hs_office.contact_create_mass_test_data(startPartnerNumber::integer, endPartnerNumber::integer); + call hs_office.person_create_mass_test_data(startPartnerNumber::integer, endPartnerNumber::integer); + call hs_office.relation_create_mass_test_data(startPartnerNumber::integer, endPartnerNumber::integer); + call hs_office.partner_create_mass_test_data(startPartnerNumber, endPartnerNumber); + call hs_office.bankaccount_create_mass_test_data(startPartnerNumber, endPartnerNumber); + call hs_office.debitor_create_mass_test_data(startPartnerNumber, endPartnerNumber); + call hs_office.sepamandate_create_mass_test_data(startPartnerNumber, endPartnerNumber); + + call hs_office.membership_create_mass_test_data(startPartnerNumber, endPartnerNumber, withMembershipPercentage); + call hs_office.coopsharetx_create_mass_test_data(startPartnerNumber, endPartnerNumber, withMembershipPercentage); + call hs_office.coopassettx_create_mass_test_data(startPartnerNumber, endPartnerNumber, withMembershipPercentage); + + call hs_booking.project_create_mass_test_data(startPartnerNumber, endPartnerNumber); + call hs_booking.item_create_mass_test_data(startPartnerNumber, endPartnerNumber); + call hs_hosting.asset_create_mass_test_data(startPartnerNumber, endPartnerNumber); + + for t in startPartnerNumber::integer..endPartnerNumber::integer + loop + call base.defineContext('mass partner bundle account test-data #' || t, null, 'superuser-alex@hostsharing.net', 'rbac.global#global:ADMIN'); + if t % 5 = 0 then + idx := base.intToVarChar(t, 4); + select p.uuid into personUuid + from hs_office.person p + where p.familyName = 'MassFamily' || idx and p.givenName = 'MassGiven' || idx; + + if personUuid is not null and not exists ( + select 1 from hs_accounts.account pr where pr.person_uuid = personUuid + ) then + accountEmail := 'mass-person-' || idx || '@example.com'; + perform rbac.create_subject(accountEmail); + select s.uuid into subjectUuid from rbac.subject s where s.name = accountEmail; + + insert into hs_accounts.account ( + uuid, version, person_uuid, + global_uid, global_gid + ) values ( + subjectUuid, 0, personUuid, + 300000 + t, 300000 + t + ); + end if; + end if; + + idx := base.intToVarChar(t, 4); + select p.uuid into personUuid + from hs_office.person p + where p.familyName = 'MassRep' || idx and p.givenName = 'User' || idx; + + if personUuid is not null and not exists ( + select 1 from hs_accounts.account pr where pr.person_uuid = personUuid + ) then + accountEmail := 'mass-rep-' || idx || '@example.com'; + perform rbac.create_subject(accountEmail); + select s.uuid into subjectUuid from rbac.subject s where s.name = accountEmail; + + insert into hs_accounts.account ( + uuid, version, person_uuid, + global_uid, global_gid + ) values ( + subjectUuid, 0, personUuid, + 400000 + t, 400000 + t + ); + end if; + commit; + end loop; +end; $$; +--// diff --git a/src/main/resources/db/changelog/9-hs-global/9521-has-mass-test-data-performance.sql b/src/main/resources/db/changelog/9-hs-global/9521-has-mass-test-data-performance.sql new file mode 100644 index 00000000..23c3d442 --- /dev/null +++ b/src/main/resources/db/changelog/9-hs-global/9521-has-mass-test-data-performance.sql @@ -0,0 +1,74 @@ +--liquibase formatted sql + + +-- ============================================================================ +--changeset michael.hoennig:hs-mass-test-data-PERFORMANCE context:!without-test-data endDelimiter:--// +-- ---------------------------------------------------------------------------- + +create or replace procedure hs_office.bench_debitor_sepamandates(iterations int default 10) + language plpgsql +as $$ +declare + i int; + t0 timestamptz; + ms numeric; + total_limit numeric := 0; + total_count numeric := 0; + min_limit numeric := null; + max_limit numeric := null; + min_count numeric := null; + max_count numeric := null; + rows_read bigint; +begin + for i in 1..iterations loop + call base.defineContext( + 'query debitor', + null, + 'superuser-alex@hostsharing.net'); + + t0 := clock_timestamp(); + select count(*) into rows_read + from ( + select d.defaultprefix, b.iban, s.validity + from hs_office.debitor d + join hs_office.sepamandate_rv s on s.debitoruuid = d.uuid + join hs_office.bankaccount b on b.uuid = s.bankaccountuuid + where d.defaultprefix like 'dq%' + limit 10 + ) x; + + ms := extract(epoch from (clock_timestamp() - t0)) * 1000; + total_limit := total_limit + ms; + if min_limit is null or ms < min_limit then min_limit := ms; end if; + if max_limit is null or ms > max_limit then max_limit := ms; end if; + + commit; + end loop; + + for i in 1..iterations loop + call base.defineContext('query debitor', + null, + 'superuser-alex@hostsharing.net'); + + t0 := clock_timestamp(); + select count(*) into rows_read + from hs_office.debitor d + join hs_office.sepamandate_rv s on s.debitoruuid = d.uuid + join hs_office.bankaccount b on b.uuid = s.bankaccountuuid; + + ms := extract(epoch from (clock_timestamp() - t0)) * 1000; + total_count := total_count + ms; + if min_count is null or ms < min_count then min_count := ms; end if; + if max_count is null or ms > max_count then max_count := ms; end if; + + commit; + end loop; + + raise notice 'limit10 min/avg/max: % ms / % ms / % ms', + round(min_limit, 3), round(total_limit / iterations, 3), round(max_limit, 3); + + raise notice 'count all min/avg/max: % ms / % ms / % ms', + round(min_count, 3), round(total_count / iterations, 3), round(max_count, 3); +end $$; +--// + diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index 1e2ea71e..b137342d 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -229,6 +229,12 @@ databaseChangeLog: - include: file: db/changelog/9-hs-global/950-accounts/9519-hs-accounts-test-data.sql context: "!only-prod-schema and !without-test-data" + - include: + file: db/changelog/9-hs-global/9520-hs-mass-test-data-generators.sql + context: "!only-prod-schema and !without-test-data" + - include: + file: db/changelog/9-hs-global/9521-has-mass-test-data-performance.sql + context: "!only-prod-schema and !without-test-data" - include: file: db/changelog/9-hs-global/960-integrations/9600-hs-integration-schema.sql diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/context/ContextIntegrationTests.java b/src/test/java/net/hostsharing/hsadminng/rbac/context/ContextIntegrationTests.java index 846c750e..2d8ae4f8 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/context/ContextIntegrationTests.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/context/ContextIntegrationTests.java @@ -189,7 +189,7 @@ class ContextIntegrationTests { @Test public void hasGlobalAdminRoleIsTrueForGlobalAdminWithAssumedRole() { - final var hsGlobalAdminRole = jpaAttempt.transacted(() -> { + final var hasGlobalAdminRole = jpaAttempt.transacted(() -> { // given context.define("superuser-alex@hostsharing.net", "rbactest.package#yyy00:ADMIN"); @@ -199,6 +199,20 @@ class ContextIntegrationTests { // when + // then + assertThat(hasGlobalAdminRole.returnedValue()).isFalse(); + } + + @Test + public void hasGlobalAdminRoleIsFalseForGlobalAdminWithAssumedGlobalAdminRole() { + final var hsGlobalAdminRole = jpaAttempt.transacted(() -> { + // given + context.define("superuser-alex@hostsharing.net", "rbac.global#global:ADMIN"); + + // when + return (boolean) em.createNativeQuery("select rbac.hasGlobalAdminRole()").getSingleResult(); + }); + // then assertThat(hsGlobalAdminRole.returnedValue()).isFalse(); }