From 14592b91ae27ac189646a13c388a8c6c6e77272a Mon Sep 17 00:00:00 2001 From: Marc Villain Date: Wed, 22 Jul 2026 09:16:59 +0200 Subject: [PATCH] test(hierarchies): cover cache invalidation on hierarchy removal --- .DS_Store | Bin 6148 -> 0 bytes tests/02_hierarchies.sql | 116 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index ff87a51e88a20881358b453d8e1162ddce266dd8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKK~EDw7<~gpwxY37Nw}D7;>{oyBu3(;lvX5BNNCX*HKp5DtSmd&7JDEhy_x7= zG4bjz@xOS|_stHt+tv#QW5|5X%$uG4-ZwLEW@kD8RPm^}2;=~GOoExq%*L3!mo#D% zwnoUx#8^iePqBq{ER1-&3a5Zm;D1v{O61gtkD1H!CTn(Bs&Wq)jj>AJLL$aB%c)`;8z_=lB}z}mu+=THlRe0w z35^E2qdir360My11^w=!9ntsTFl={w`TP$xF`1dVH0@1$Gv1rZJKeAJ>b;|GvEF$l zPgSkM;Iv#1p0`{5TJGkWj(YWW)an>QyV;`4>lf{)sryBJ6g9hsTZvzISub15%^e>v zJzSah@8|O;^ZxP5QhwfFez1IUlJ#b9-+Q!O+Yb*S{aK{thRPk)*j0*<*d??$pvNsfET4#LUxI-0U zbh%T&DUeoRGJed-`CtC~{Xb1|Pfh`+z=cvkWlEJ&kwda)>&W2bto508nZ(3z5Tl_m kv&XS1$Wc7UWQ^~8DG&=52QjjS=Kcs68C>QR_@@f|23WNOSpWb4 diff --git a/tests/02_hierarchies.sql b/tests/02_hierarchies.sql index d2165f7..da57b82 100644 --- a/tests/02_hierarchies.sql +++ b/tests/02_hierarchies.sql @@ -156,6 +156,122 @@ SELECT morbac.t('Tech lead approves Engineering documents (own perm)', 'approve', 'documents' ), TRUE); +-- --------------------------------------------------------------------------- +-- Section 2b: Role hierarchy cache refresh and closure maintenance +-- --------------------------------------------------------------------------- +\echo '' +\echo '--- 2b. Role hierarchy cache refresh and closure maintenance ---' + +-- Existing transitive closure is materialized: CEO reaches intern at depth 4. +SELECT morbac.t('mv_role_closure includes ceo -> intern at depth 4', + EXISTS( + SELECT 1 + FROM morbac.mv_role_closure + WHERE senior_role_id = '20000000-0001-0000-0000-000000000001'::uuid + AND junior_role_id = '20000000-0001-0000-0000-000000000005'::uuid + AND depth = 4 + ), TRUE); + +-- Add a temporary specialist role with a unique permission, then attach it +-- under employee so the hierarchy trigger must refresh mv_role_closure. +INSERT INTO morbac.roles (id, org_id, name, description) VALUES + ('20000000-0001-0000-0000-000000000099', '10000000-0000-0000-0000-000000000001', 'temporary_specialist', 'Temporary role for role_hierarchy tests'); + +INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality) +VALUES ( + '10000000-0000-0000-0000-000000000001', + '20000000-0001-0000-0000-000000000099', + 'read', 'contracts', + (SELECT id FROM morbac.contexts WHERE name = 'always'), + 'permission' +); + +SELECT morbac.t('Dave (employee) initially cannot read contracts', + morbac.is_allowed_nocache( + '30000000-0000-0000-0000-000000000004'::uuid, + '10000000-0000-0000-0000-000000000001'::uuid, + 'read', 'contracts' + ), FALSE); + +INSERT INTO morbac.role_hierarchy (senior_role_id, junior_role_id) VALUES + ('20000000-0001-0000-0000-000000000004', '20000000-0001-0000-0000-000000000099'); + +SELECT morbac.t('mv_role_closure refreshes after role_hierarchy insert (employee -> temporary_specialist)', + EXISTS( + SELECT 1 + FROM morbac.mv_role_closure + WHERE senior_role_id = '20000000-0001-0000-0000-000000000004'::uuid + AND junior_role_id = '20000000-0001-0000-0000-000000000099'::uuid + AND depth = 1 + ), TRUE); + +SELECT morbac.t('Dave (employee) gains temporary_specialist permission via refreshed hierarchy', + morbac.is_allowed_nocache( + '30000000-0000-0000-0000-000000000004'::uuid, + '10000000-0000-0000-0000-000000000001'::uuid, + 'read', 'contracts' + ), TRUE); + +-- Re-enable cache briefly to verify hierarchy changes invalidate stale auth decisions. +SELECT morbac.set_config('cache_ttl_seconds', '3600'); +DELETE FROM morbac.auth_cache; + +SELECT morbac.t('Dave cached authorization sees inherited contracts access', + morbac.is_allowed( + '30000000-0000-0000-0000-000000000004'::uuid, + '10000000-0000-0000-0000-000000000001'::uuid, + 'read', 'contracts' + ), TRUE); + +SELECT morbac.t('Dave auth result is cached before hierarchy removal', + EXISTS( + SELECT 1 + FROM morbac.auth_cache + WHERE user_id = '30000000-0000-0000-0000-000000000004'::uuid + AND org_id = '10000000-0000-0000-0000-000000000001'::uuid + AND activity = 'read' + AND view = 'contracts' + ), TRUE); + +DELETE FROM morbac.role_hierarchy +WHERE senior_role_id = '20000000-0001-0000-0000-000000000004' + AND junior_role_id = '20000000-0001-0000-0000-000000000099'; + +SELECT morbac.t('Hierarchy change invalidates cached auth decision for Dave/contracts', + NOT EXISTS( + SELECT 1 + FROM morbac.auth_cache + WHERE user_id = '30000000-0000-0000-0000-000000000004'::uuid + AND org_id = '10000000-0000-0000-0000-000000000001'::uuid + AND activity = 'read' + AND view = 'contracts' + ), TRUE); + +SELECT morbac.t('Dave cached authorization recomputes to denied after hierarchy removal', + morbac.is_allowed( + '30000000-0000-0000-0000-000000000004'::uuid, + '10000000-0000-0000-0000-000000000001'::uuid, + 'read', 'contracts' + ), FALSE); + +DELETE FROM morbac.rules +WHERE org_id = '10000000-0000-0000-0000-000000000001' + AND role_id = '20000000-0001-0000-0000-000000000099' + AND activity = 'read' + AND view = 'contracts' + AND modality = 'permission'; + +DELETE FROM morbac.roles +WHERE id = '20000000-0001-0000-0000-000000000099'; + +DELETE FROM morbac.auth_cache +WHERE user_id = '30000000-0000-0000-0000-000000000004' + AND org_id = '10000000-0000-0000-0000-000000000001' + AND activity = 'read' + AND view = 'contracts'; + +SELECT morbac.set_config('cache_ttl_seconds', '0'); + -- --------------------------------------------------------------------------- -- Section 3: Activity hierarchy -- ---------------------------------------------------------------------------