docs: complete function reference, drop unicode punctuation

Documents refresh_hierarchy_cache, is_rule_valid and org_in_scope, and
corrects the get_org_scope scope list which still omitted unattributed
and all.

Replaces em dashes and other typographic unicode with ASCII throughout
the schema comments, the test suite and the documentation. Comments and
prose are ASCII only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 07:26:35 +02:00
parent fa7567f5f0
commit cc3d65a013
25 changed files with 290 additions and 284 deletions
+35 -29
View File
@@ -123,7 +123,7 @@ erDiagram
**morbac.contexts**: Contextual conditions as callable predicates. Column `evaluator` (REGPROC) references a function returning BOOLEAN (preferably STABLE). Built-in context `always` returns true.
**morbac.rules**: Core rules linking org, role, activity, view, context, modality, and scope. The `scope` column (default `'self'`) controls which orgs the rule covers relative to `org_id` evaluated at query time so new child orgs are picked up automatically without re-inserting rules.
**morbac.rules**: Core rules linking org, role, activity, view, context, modality, and scope. The `scope` column (default `'self'`) controls which orgs the rule covers relative to `org_id` - evaluated at query time so new child orgs are picked up automatically without re-inserting rules.
### Advanced Feature Tables
@@ -198,7 +198,7 @@ Inter-organizational access rules.
**morbac.system_principals**
Registry of backend service accounts. Registered user UUIDs are protected at the trigger level no role assignment, rule, delegation, or prohibition can target them. Their permission rules in `global_rules` are equally immutable.
Registry of backend service accounts. Registered user UUIDs are protected at the trigger level - no role assignment, rule, delegation, or prohibition can target them. Their permission rules in `global_rules` are equally immutable.
**Key columns:**
- `user_id`: UUID of the service account
@@ -218,7 +218,7 @@ System-wide rules with no org or role binding.
- `modality`: Permission or prohibition
- `priority`: Optional; same resolution semantics as `morbac.rules`
**Behavior:** Evaluated at steps 3.5 (prohibitions) and 6.5 (permissions) in `is_allowed_nocache()`. NULL on `activity` or `view` matches any value no hierarchy setup required for broad rules.
**Behavior:** Evaluated at steps 3.5 (prohibitions) and 6.5 (permissions) in `is_allowed_nocache()`. NULL on `activity` or `view` matches any value - no hierarchy setup required for broad rules.
**morbac.activity_view_bindings**
@@ -373,7 +373,7 @@ JOIN morbac.contexts c ON c.name = v.context_name;
### Organization Rule Scope
Every rule has a `scope` column (default `'self'`) that controls which organizations the rule covers relative to its `org_id`. Scope is **evaluated at query time** adding a new child org to the hierarchy is enough for it to be covered by existing scoped rules. No rule re-creation needed.
Every rule has a `scope` column (default `'self'`) that controls which organizations the rule covers relative to its `org_id`. Scope is **evaluated at query time** - adding a new child org to the hierarchy is enough for it to be covered by existing scoped rules. No rule re-creation needed.
| Scope | Covers |
|---|---|
@@ -414,11 +414,11 @@ WHERE o.name = 'EMEA Region';
**Cache behavior:** The auth cache is fully invalidated whenever the org tree changes (`INSERT`/`UPDATE`/`DELETE` on `morbac.orgs`), so scoped rules are always consistent.
**`get_org_scope(org_id, scope, max_depth?)`** is the underlying helper it returns `(org_id, depth)` rows and can be used directly when you need to iterate over an org set. An optional `p_max_depth` limits traversal depth.
**`get_org_scope(org_id, scope, max_depth?)`** is the underlying helper - it returns `(org_id, depth)` rows and can be used directly when you need to iterate over an org set. An optional `p_max_depth` limits traversal depth.
### Unattributed (no-org) Objects
An object whose `org_id` is `NULL` is **unattributed**: it belongs to no organization, typically because it is awaiting attribution. This is the only meaning `NULL` carries in the org dimension it never means "any org" and never means "all orgs".
An object whose `org_id` is `NULL` is **unattributed**: it belongs to no organization, typically because it is awaiting attribution. This is the only meaning `NULL` carries in the org dimension - it never means "any org" and never means "all orgs".
#### The org target vocabulary
@@ -426,15 +426,15 @@ Every rule kind selects its target the same way. There are exactly three targets
| Target | Role-based (`morbac.rules`) | User-level (roleless) |
|---|---|---|
| A specific organization | `scope` = `'self'`, `'subtree'`, | `user_rules` with an `org_id` |
| A specific organization | `scope` = `'self'`, `'subtree'`, ... | `user_rules` with an `org_id` |
| Unattributed objects | `scope` = `'unattributed'` | `user_rules` with `org_id = NULL` |
| All orgs (unattributed included) | `scope` = `'all'` | `global_rules` |
The two object classes are **partitioned**: an `'unattributed'` rule can never reach an object that has an org, and the tree scopes (`'self'`, `'subtree'`, ) can never reach an unattributed object. Only `'all'` and `global_rules` deliberately span both.
The two object classes are **partitioned**: an `'unattributed'` rule can never reach an object that has an org, and the tree scopes (`'self'`, `'subtree'`, ...) can never reach an unattributed object. Only `'all'` and `global_rules` deliberately span both.
#### Granting a role access to unattributed objects
The declaring org is the policy authority; the role must be held **in that org**. Grant, revoke, and delegate the role exactly as usual access to the unattributed pool follows.
The declaring org is the policy authority; the role must be held **in that org**. Grant, revoke, and delegate the role exactly as usual - access to the unattributed pool follows.
```sql
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, scope)
@@ -445,7 +445,7 @@ JOIN morbac.contexts c ON c.name = 'always'
WHERE o.name = 'Acme Corp';
```
Several organizations may each declare their own policy over the same unattributed pool that is ordinary Multi-OrBAC: independent authorities over a shared object space.
Several organizations may each declare their own policy over the same unattributed pool - that is ordinary Multi-OrBAC: independent authorities over a shared object space.
Prohibitions, priorities, contexts, temporal validity, role hierarchy, delegation, derived roles, negative assignments and SoD all apply unchanged:
@@ -463,7 +463,7 @@ Granting a single user access without a role uses `user_rules` with no org:
```sql
INSERT INTO morbac.user_rules (user_id, org_id, activity, view, context_id, modality)
SELECT '…user…'::uuid, NULL, 'read', 'documents', c.id, 'permission'
SELECT '...user...'::uuid, NULL, 'read', 'documents', c.id, 'permission'
FROM morbac.contexts c WHERE c.name = 'always';
```
@@ -492,9 +492,9 @@ Which records come back is chosen with the session variables. A JSON `null` elem
| Session | Returns |
|---|---|
| *(nothing set)* | all authorized records every org **and** unattributed |
| `morbac.org_id = '<uuid>'` | that org only unattributed excluded |
| `morbac.org_ids = '["<uuid>"]'` | those orgs only unattributed excluded |
| *(nothing set)* | all authorized records - every org **and** unattributed |
| `morbac.org_id = '<uuid>'` | that org only - unattributed excluded |
| `morbac.org_ids = '["<uuid>"]'` | those orgs only - unattributed excluded |
| `morbac.org_ids = '[null]'` | **unattributed only** (the attribution queue) |
| `morbac.org_ids = '["<uuid>", null]'` | that org **plus** unattributed |
@@ -508,7 +508,7 @@ SELECT * FROM app.documents;
#### Capability probe for UI gating
To decide whether to show a feature at all rather than authorize a specific object use:
To decide whether to show a feature at all - rather than authorize a specific object - use:
```sql
SELECT morbac.has_permission(user_id, 'read', 'documents');
@@ -683,7 +683,7 @@ INSERT INTO morbac.cross_org_rules (
SELECT morbac.is_allowed(auditor_id, subsidiary_id, 'read', 'financials'); -- TRUE
```
**Scope vs. cross-org rules when to use which:**
**Scope vs. cross-org rules - when to use which:**
| Need | Use |
|---|---|
@@ -692,7 +692,7 @@ SELECT morbac.is_allowed(auditor_id, subsidiary_id, 'read', 'financials'); -- TR
### Global Rules
Global rules apply system-wide no org or role required. Use them to define blanket access policies that cut across the entire org hierarchy.
Global rules apply system-wide - no org or role required. Use them to define blanket access policies that cut across the entire org hierarchy.
**Table:** `morbac.global_rules`
@@ -736,7 +736,7 @@ Priority 100 ensures this prohibition overrides any role-based permission. To ex
### System Principals
Backend service accounts that must be fully immutable at the database level no policy, no admin, no superadmin can touch them once registered.
Backend service accounts that must be fully immutable at the database level - no policy, no admin, no superadmin can touch them once registered.
**Table:** `morbac.system_principals`
@@ -745,7 +745,7 @@ Backend service accounts that must be fully immutable at the database level —
| `user_id` | UUID of the service account (external, from your auth system) |
| `description` | Human-readable label |
**What is protected (trigger level fires for all users including superusers):**
**What is protected (trigger level - fires for all users including superusers):**
| Table | Blocked operations |
|---|---|
@@ -755,9 +755,9 @@ Backend service accounts that must be fully immutable at the database level —
| `delegations` | INSERT, UPDATE involving the principal |
| `global_rules` | All operations where `user_id` matches a system principal |
**Authorization behavior:** Prohibition evaluation (steps 13.5) is skipped entirely for system principals. Even a blanket `user_id=NULL` global prohibition does not affect them. Only their permission rules matter.
**Authorization behavior:** Prohibition evaluation (steps 1-3.5) is skipped entirely for system principals. Even a blanket `user_id=NULL` global prohibition does not affect them. Only their permission rules matter.
**Ruleset:** Define permissions for system principals via `global_rules` at deploy time. Those rows are immutable once inserted no one can modify or delete them. Use `activity=NULL, view=NULL` to grant full access, or restrict to specific activities/views:
**Ruleset:** Define permissions for system principals via `global_rules` at deploy time. Those rows are immutable once inserted - no one can modify or delete them. Use `activity=NULL, view=NULL` to grant full access, or restrict to specific activities/views:
```sql
-- Register the service account (DB owner only)
@@ -775,23 +775,23 @@ VALUES (:service_uuid, 'read', NULL,
(SELECT id FROM morbac.contexts WHERE name = 'always'), 'permission');
```
**Access control on the registry itself:** `morbac.system_principals` has a SELECT-only RLS policy a user needs `is_allowed(..., 'read', 'system_principals')` to list them. INSERT/UPDATE/DELETE have no RLS policy, so they are blocked for all non-superusers automatically. Only the database owner can register or remove system principals.
**Access control on the registry itself:** `morbac.system_principals` has a SELECT-only RLS policy - a user needs `is_allowed(..., 'read', 'system_principals')` to list them. INSERT/UPDATE/DELETE have no RLS policy, so they are blocked for all non-superusers automatically. Only the database owner can register or remove system principals.
### Administration
Admin operations use the same `is_allowed()` engine as everything else no separate code path.
Admin operations use the same `is_allowed()` engine as everything else - no separate code path.
**System table RLS**
`morbac.*` tables have RLS policies. `is_allowed()` and all its internal callees are `SECURITY DEFINER`, running as the extension owner and bypassing RLS. This breaks the recursion: RLS policies call `is_allowed()`, which queries morbac tables without re-triggering the policies.
The database owner (superuser) bypasses RLS by default use that privilege only during bootstrap.
The database owner (superuser) bypasses RLS by default - use that privilege only during bootstrap.
**System view names**
The extension seeds built-in activities (`create`, `read`, `update`, `delete`) and system view names (`orgs`, `roles`, `rules`, `user_roles`, `contexts`, `activities`, `views`, `delegations`, `cross_org_rules`, `user_rules`, `global_rules`, `system_principals`) at install time.
These names are config-driven. Override with `morbac.set_config()` to use your own naming conventions the new name must then exist in `morbac.views` and your rules must reference it:
These names are config-driven. Override with `morbac.set_config()` to use your own naming conventions - the new name must then exist in `morbac.views` and your rules must reference it:
```sql
-- Rename 'rules' to 'policies' in your system
@@ -953,7 +953,7 @@ WHERE table_name = 'rules'
### Authorization Functions
**`is_allowed(user_id, org_id, activity, view)`**: Main authorization decision. Returns BOOLEAN. Evaluates local rules, cross-org rules, user rules, and global rules; defaults to deny. `org_id` is a specific organization, or `NULL` when the object is unattributed (no org) `NULL` never means "any org". Cache writes are silently skipped in read-only transactions so this function is safe to call from both read-write and read-only contexts (e.g. PostgREST GET requests). Unattributed decisions are not cached.
**`is_allowed(user_id, org_id, activity, view)`**: Main authorization decision. Returns BOOLEAN. Evaluates local rules, cross-org rules, user rules, and global rules; defaults to deny. `org_id` is a specific organization, or `NULL` when the object is unattributed (no org) - `NULL` never means "any org". Cache writes are silently skipped in read-only transactions so this function is safe to call from both read-write and read-only contexts (e.g. PostgREST GET requests). Unattributed decisions are not cached.
```sql
SELECT morbac.is_allowed(user_uuid, org_uuid, 'read', 'documents');
@@ -976,7 +976,11 @@ SELECT morbac.has_permission(user_uuid, 'read', 'documents');
**`get_org_descendants(org_id)`**: Returns all child organizations with depth (including self at depth 0).
**`get_org_scope(org_id, scope, max_depth?)`**: Returns a named set of organizations relative to `org_id`. Scope values: `self`, `children`, `descendants`, `subtree`, `parent`, `ancestors`, `lineage`, `root`. Optional `max_depth` limits traversal depth.
**`get_org_scope(org_id, scope, max_depth?)`**: Returns a named set of organizations relative to `org_id`. Scope values: `self`, `children`, `descendants`, `subtree`, `parent`, `ancestors`, `lineage`, `root`, plus `all` (every organization) and `unattributed` (no rows - an unattributed object has no org to return). Optional `max_depth` limits traversal depth.
**`org_in_scope(target_org_id, rule_org_id, scope)`**: Returns TRUE when a rule declared at `rule_org_id` with `scope` covers `target_org_id`. A NULL target is covered only by `unattributed` and `all`; the tree scopes never match one.
**`refresh_hierarchy_cache()`**: Rebuilds the materialized org, role, activity and view closures. Triggers call it whenever a hierarchy changes; call it manually after a bulk load.
```sql
-- All orgs in the subtree, up to 2 levels deep
@@ -1007,6 +1011,8 @@ SELECT * FROM morbac.get_org_scope(org_uuid, 'subtree', 2);
**`eval_derived_role(evaluator, user_id, org_id)`**: Evaluate a derived role condition function (REGPROC). Returns BOOLEAN.
**`is_rule_valid(valid_from, valid_until)`**: Returns TRUE when a validity window covers the current timestamp. Every store applies it before a rule can match.
### Context Functions
**`eval_context(context_id)`**: Evaluate a context predicate.
@@ -1083,10 +1089,10 @@ The row-scoped `rls_check` resolves which records to return from session variabl
| Session variable | Behaviour |
|---|---|
| `morbac.org_id` set | that single org unattributed excluded |
| `morbac.org_id` set | that single org - unattributed excluded |
| `morbac.org_ids` set | the listed orgs; a `null` element adds unattributed records |
| `morbac.org_ids = '[null]'` | unattributed records only |
| neither set | all authorized records every org **and** unattributed |
| neither set | all authorized records - every org **and** unattributed |
#### Setting context from HTTP headers
+1 -1
View File
@@ -19,7 +19,7 @@ BEGIN
IF EXISTS (SELECT 1 FROM morbac.activity_view_bindings WHERE activity = NEW.activity)
AND NOT EXISTS (SELECT 1 FROM morbac.activity_view_bindings WHERE activity = NEW.activity AND view = NEW.view)
THEN
RAISE EXCEPTION 'Activity "%" is not allowed on view "%" add a binding to morbac.activity_view_bindings to permit it',
RAISE EXCEPTION 'Activity "%" is not allowed on view "%" - add a binding to morbac.activity_view_bindings to permit it',
NEW.activity, NEW.view;
END IF;
RETURN NEW;
+2 -2
View File
@@ -125,7 +125,7 @@ CREATE TRIGGER trg_invalidate_cache_user_rules
AFTER INSERT OR UPDATE OR DELETE ON morbac.user_rules
FOR EACH ROW EXECUTE FUNCTION morbac.invalidate_cache_on_user_rule_change();
-- Global rules have no org scope any change invalidates the entire cache
-- Global rules have no org scope - any change invalidates the entire cache
CREATE OR REPLACE FUNCTION morbac.invalidate_cache_on_global_rule_change()
RETURNS TRIGGER
LANGUAGE plpgsql
@@ -141,7 +141,7 @@ CREATE TRIGGER trg_invalidate_cache_global_rules
AFTER INSERT OR UPDATE OR DELETE ON morbac.global_rules
FOR EACH ROW EXECUTE FUNCTION morbac.invalidate_cache_on_global_rule_change();
-- system_principals changes affect prohibition bypass invalidate per user
-- system_principals changes affect prohibition bypass - invalidate per user
CREATE OR REPLACE FUNCTION morbac.invalidate_cache_on_system_principal_change()
RETURNS TRIGGER
LANGUAGE plpgsql
+8 -8
View File
@@ -17,7 +17,7 @@
-- Scope:
-- - rules.scope selects which objects a rule covers: a specific org
-- (self/subtree/descendants/...), 'unattributed', or 'all'.
-- Evaluated at query time via org_in_scope() new orgs are covered automatically.
-- Evaluated at query time via org_in_scope() - new orgs are covered automatically.
-- - cross_org_rules.source_org_id is always required: user must hold the role there.
-- - user_rules target a specific user directly (no role required); their org_id
-- is a specific org, or NULL for unattributed objects.
@@ -44,10 +44,10 @@ BEGIN
SELECT 1 FROM morbac.system_principals WHERE user_id = p_user_id
);
-- STEPS 1-3.5: Prohibitions skipped entirely for system principals
-- STEPS 1-3.5: Prohibitions - skipped entirely for system principals
IF NOT v_is_system_principal THEN
-- STEP 1: Local prohibitions find the highest-priority applicable one
-- STEP 1: Local prohibitions - find the highest-priority applicable one
FOR v_rule IN
SELECT r.context_id, COALESCE(r.priority, 0) AS prio
FROM morbac.rules r
@@ -65,7 +65,7 @@ BEGIN
END IF;
END LOOP;
-- STEP 2: Cross-org prohibitions update max if a higher priority is found
-- STEP 2: Cross-org prohibitions - update max if a higher priority is found
FOR v_rule IN
SELECT cr.context_id, COALESCE(cr.priority, 0) AS prio
FROM morbac.cross_org_rules cr
@@ -85,7 +85,7 @@ BEGIN
END IF;
END LOOP;
-- STEP 3: User-level prohibitions direct user rules, update max if higher
-- STEP 3: User-level prohibitions - direct user rules, update max if higher
FOR v_rule IN
SELECT ur.context_id, COALESCE(ur.priority, 0) AS prio
FROM morbac.user_rules ur
@@ -126,7 +126,7 @@ BEGIN
END IF; -- v_is_system_principal
-- STEP 4: Local permissions find the highest-priority applicable one
-- STEP 4: Local permissions - find the highest-priority applicable one
FOR v_rule IN
SELECT r.context_id, COALESCE(r.priority, 0) AS prio
FROM morbac.rules r
@@ -144,7 +144,7 @@ BEGIN
END IF;
END LOOP;
-- STEP 5: Cross-org permissions update max if higher found
-- STEP 5: Cross-org permissions - update max if higher found
FOR v_rule IN
SELECT cr.context_id, COALESCE(cr.priority, 0) AS prio
FROM morbac.cross_org_rules cr
@@ -164,7 +164,7 @@ BEGIN
END IF;
END LOOP;
-- STEP 6: User-level permissions direct user rules, update max if higher
-- STEP 6: User-level permissions - direct user rules, update max if higher
FOR v_rule IN
SELECT ur.context_id, COALESCE(ur.priority, 0) AS prio
FROM morbac.user_rules ur
+1 -1
View File
@@ -1,6 +1,6 @@
-- Global rules: Rule(user_id, activity, view, context, modality)
--
-- No org_id or role_id applies system-wide regardless of org membership or roles.
-- No org_id or role_id - applies system-wide regardless of org membership or roles.
-- user_id NULL = every user; non-NULL = specific user only.
-- activity NULL = any activity; view NULL = any view.
--
+10 -10
View File
@@ -53,16 +53,16 @@ COMMENT ON FUNCTION morbac.get_org_descendants(UUID) IS
-- Get a named scope of organizations relative to a given org.
--
-- Supported scopes:
-- 'self' the org itself only (depth = 0)
-- 'children' direct children only (descendants at depth = 1)
-- 'descendants' all descendants, excluding self (depth > 0)
-- 'subtree' self + all descendants (equivalent to get_org_descendants)
-- 'parent' direct parent only (ancestor at depth = 1)
-- 'ancestors' all ancestors, excluding self (depth > 0)
-- 'lineage' self + all ancestors (equivalent to get_org_ancestors)
-- 'root' topmost ancestor only (max depth ancestor)
-- 'unattributed' the no-org bucket; resolves to no real orgs (empty set)
-- 'all' every organization (unattributed is not an org, so it is
-- 'self' - the org itself only (depth = 0)
-- 'children' - direct children only (descendants at depth = 1)
-- 'descendants' - all descendants, excluding self (depth > 0)
-- 'subtree' - self + all descendants (equivalent to get_org_descendants)
-- 'parent' - direct parent only (ancestor at depth = 1)
-- 'ancestors' - all ancestors, excluding self (depth > 0)
-- 'lineage' - self + all ancestors (equivalent to get_org_ancestors)
-- 'root' - topmost ancestor only (max depth ancestor)
-- 'unattributed' - the no-org bucket; resolves to no real orgs (empty set)
-- 'all' - every organization (unattributed is not an org, so it is
-- not listed here; org_in_scope('all') does cover it)
--
-- Optional p_max_depth limits how many levels are traversed (NULL = unlimited).
+12 -12
View File
@@ -1,17 +1,17 @@
-- Core OrBAC rule relation: Rule(org, role, activity, view, context, modality)
--
-- scope controls which orgs this rule covers relative to org_id:
-- 'self' exact org only (default, current behavior)
-- 'subtree' org + all descendants
-- 'descendants' all descendants, excluding self
-- 'children' direct children only
-- 'parent' direct parent only
-- 'ancestors' all ancestors, excluding self
-- 'lineage' self + all ancestors
-- 'root' topmost ancestor only
-- 'unattributed' no-org objects only (org_id column above is the declaring authority)
-- 'all' every org, unattributed included
-- Evaluated at query time via org_in_scope() new orgs are picked up automatically.
-- 'self' - exact org only (default, current behavior)
-- 'subtree' - org + all descendants
-- 'descendants' - all descendants, excluding self
-- 'children' - direct children only
-- 'parent' - direct parent only
-- 'ancestors' - all ancestors, excluding self
-- 'lineage' - self + all ancestors
-- 'root' - topmost ancestor only
-- 'unattributed' - no-org objects only (org_id column above is the declaring authority)
-- 'all' - every org, unattributed included
-- Evaluated at query time via org_in_scope() - new orgs are picked up automatically.
--
-- Org target vocabulary: a specific org (the tree scopes), 'unattributed', or 'all'.
@@ -44,7 +44,7 @@ INCLUDE (role_id, context_id)
WHERE is_active = true;
COMMENT ON TABLE morbac.rules IS 'Core OrBAC rules - Permission, Prohibition, Obligation, Recommendation';
COMMENT ON COLUMN morbac.rules.scope IS 'Object scope: self (default), subtree, descendants, children, parent, ancestors, lineage, root, unattributed, all. Selects which objects (by org) the rule reaches: a specific org via the tree scopes, unattributed for no-org objects only, or all for every org including unattributed. Evaluated at query time new orgs are covered automatically.';
COMMENT ON COLUMN morbac.rules.scope IS 'Object scope: self (default), subtree, descendants, children, parent, ancestors, lineage, root, unattributed, all. Selects which objects (by org) the rule reaches: a specific org via the tree scopes, unattributed for no-org objects only, or all for every org including unattributed. Evaluated at query time - new orgs are covered automatically.';
COMMENT ON COLUMN morbac.rules.modality IS 'Deontic modality: permission, prohibition, obligation, recommendation';
COMMENT ON COLUMN morbac.rules.priority IS 'Optional rule priority (higher wins). NULL = 0. A permission with higher priority than a prohibition overrides it.';
+2 -2
View File
@@ -7,7 +7,7 @@
-- - No targeted global_rules prohibitions
-- - All prohibitions are skipped in is_allowed_nocache() (see authorization.sql)
--
-- This table has no INSERT/UPDATE/DELETE RLS policies only the database owner
-- This table has no INSERT/UPDATE/DELETE RLS policies - only the database owner
-- can register or remove system principals (done in SQL at deploy time).
-- SELECT is gated by is_allowed() like all other system tables.
@@ -17,7 +17,7 @@ CREATE TABLE morbac.system_principals (
);
COMMENT ON TABLE morbac.system_principals IS
'Registry of backend service accounts. Immutable at the trigger level no policy can touch them.';
'Registry of backend service accounts. Immutable at the trigger level - no policy can touch them.';
COMMENT ON COLUMN morbac.system_principals.user_id IS
'External user UUID of the service account';
+5 -5
View File
@@ -85,7 +85,7 @@ CREATE POLICY user_roles_delete ON morbac.user_roles FOR DELETE
USING (morbac.is_allowed(morbac.current_user_id(), org_id, 'delete',
morbac.get_config('system_view.user_roles')));
-- morbac.contexts (global use current session org for writes)
-- morbac.contexts (global - use current session org for writes)
ALTER TABLE morbac.contexts ENABLE ROW LEVEL SECURITY;
CREATE POLICY contexts_select ON morbac.contexts FOR SELECT
@@ -104,7 +104,7 @@ CREATE POLICY contexts_delete ON morbac.contexts FOR DELETE
USING (morbac.is_allowed(morbac.current_user_id(), morbac.current_org_id(), 'delete',
morbac.get_config('system_view.contexts')));
-- morbac.activities (global use current session org for writes)
-- morbac.activities (global - use current session org for writes)
ALTER TABLE morbac.activities ENABLE ROW LEVEL SECURITY;
CREATE POLICY activities_select ON morbac.activities FOR SELECT
@@ -123,7 +123,7 @@ CREATE POLICY activities_delete ON morbac.activities FOR DELETE
USING (morbac.is_allowed(morbac.current_user_id(), morbac.current_org_id(), 'delete',
morbac.get_config('system_view.activities')));
-- morbac.views (global use current session org for writes)
-- morbac.views (global - use current session org for writes)
ALTER TABLE morbac.views ENABLE ROW LEVEL SECURITY;
CREATE POLICY views_select ON morbac.views FOR SELECT
@@ -180,14 +180,14 @@ CREATE POLICY user_rules_delete ON morbac.user_rules FOR DELETE
USING (morbac.is_allowed(morbac.current_user_id(), org_id, 'delete',
morbac.get_config('system_view.user_rules')));
-- morbac.system_principals (no org_id SELECT only; INSERT/UPDATE/DELETE reserved for DB owner)
-- morbac.system_principals (no org_id - SELECT only; INSERT/UPDATE/DELETE reserved for DB owner)
ALTER TABLE morbac.system_principals ENABLE ROW LEVEL SECURITY;
CREATE POLICY system_principals_select ON morbac.system_principals FOR SELECT
USING (morbac.is_allowed(morbac.current_user_id(), morbac.current_org_id(), 'read',
morbac.get_config('system_view.system_principals')));
-- morbac.global_rules (no org_id use current session org for write checks)
-- morbac.global_rules (no org_id - use current session org for write checks)
ALTER TABLE morbac.global_rules ENABLE ROW LEVEL SECURITY;
CREATE POLICY global_rules_select ON morbac.global_rules FOR SELECT
+1 -1
View File
@@ -34,7 +34,7 @@ CREATE INDEX idx_user_rules_activity_view ON morbac.user_rules(activity, view);
CREATE INDEX idx_user_rules_modality ON morbac.user_rules(modality);
CREATE INDEX idx_user_rules_lookup ON morbac.user_rules(user_id, org_id, activity, modality, view);
COMMENT ON TABLE morbac.user_rules IS 'Direct user-level rules grant or prohibit access for a specific user, bypassing the role system';
COMMENT ON TABLE morbac.user_rules IS 'Direct user-level rules - grant or prohibit access for a specific user, bypassing the role system';
COMMENT ON COLUMN morbac.user_rules.user_id IS 'User this rule applies to directly';
COMMENT ON COLUMN morbac.user_rules.org_id IS 'Org where the resource resides; NULL targets unattributed (no-org) objects';
COMMENT ON COLUMN morbac.user_rules.modality IS 'Deontic modality: permission, prohibition, obligation, recommendation';
+1 -1
View File
@@ -24,7 +24,7 @@ COMMENT ON TABLE morbac.view_hierarchy IS 'View hierarchy - senior views inherit
COMMENT ON COLUMN morbac.view_hierarchy.senior_view IS 'Senior view (more specific)';
COMMENT ON COLUMN morbac.view_hierarchy.junior_view IS 'Junior view (more general)';
-- Default system view names match system_view.* config keys.
-- Default system view names - match system_view.* config keys.
-- Override config values to rename; the new name must be seeded here too.
INSERT INTO morbac.views (name, description) VALUES
('orgs', 'Organizations table'),
+23 -23
View File
@@ -1,11 +1,11 @@
-- =============================================================================
-- Test Setup GlobalTech Inc. Company Scenario
-- Test Setup - GlobalTech Inc. Company Scenario
-- =============================================================================
-- This file establishes the full company structure used across all test files:
--
-- GlobalTech HQ (root)
-- ├── Engineering Dept (child)
-- └── Sales Dept (child)
-- +-- Engineering Dept (child)
-- \-- Sales Dept (child)
--
-- Role hierarchy in GlobalTech (senior -> junior, i.e. senior inherits junior perms):
-- ceo -> director -> manager -> employee -> intern
@@ -15,18 +15,18 @@
-- tech_lead -> engineer
--
-- Users:
-- Alice CEO at GlobalTech
-- Bob Director at GlobalTech
-- Carol Manager at GlobalTech
-- Dave Employee at GlobalTech
-- Eve Intern at GlobalTech
-- Frank Contractor at GlobalTech
-- Grace HR Manager at GlobalTech
-- Heidi Auditor at GlobalTech
-- Ivan Accountant at GlobalTech
-- Judy Engineer at Engineering + Sales Rep at Sales (multi-org)
-- Karl No role (unauthorized user)
-- Leo Employee at GlobalTech (used for delegation target)
-- Alice - CEO at GlobalTech
-- Bob - Director at GlobalTech
-- Carol - Manager at GlobalTech
-- Dave - Employee at GlobalTech
-- Eve - Intern at GlobalTech
-- Frank - Contractor at GlobalTech
-- Grace - HR Manager at GlobalTech
-- Heidi - Auditor at GlobalTech
-- Ivan - Accountant at GlobalTech
-- Judy - Engineer at Engineering + Sales Rep at Sales (multi-org)
-- Karl - No role (unauthorized user)
-- Leo - Employee at GlobalTech (used for delegation target)
-- =============================================================================
-- Clean slate
@@ -55,7 +55,7 @@ WHERE id IN (
'10000000-0000-0000-0000-000000000003'
);
SELECT o.name, COALESCE(p.name, '') AS parent
SELECT o.name, COALESCE(p.name, ' - ') AS parent
FROM morbac.orgs o LEFT JOIN morbac.orgs p ON o.parent_id = p.id
ORDER BY o.parent_id NULLS FIRST, o.name;
@@ -122,7 +122,7 @@ JOIN morbac.roles jr ON jr.id = rh.junior_role_id
ORDER BY sr.name;
-- ---------------------------------------------------------------------------
-- USER ROLE ASSIGNMENTS
-- USER - ROLE ASSIGNMENTS
-- ---------------------------------------------------------------------------
\echo ''
\echo '=== Setup: User-Role Assignments ==='
@@ -155,7 +155,7 @@ INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES
('30000000-0000-0000-0000-000000000010', '20000000-0002-0000-0000-000000000002', '10000000-0000-0000-0000-000000000002'),
('30000000-0000-0000-0000-000000000010', '20000000-0003-0000-0000-000000000002', '10000000-0000-0000-0000-000000000003');
-- Karl: no role (unauthorized user intentionally not assigned any role)
-- Karl: no role (unauthorized user - intentionally not assigned any role)
SELECT ur.user_id, r.name AS role, o.name AS organization
FROM morbac.user_roles ur
@@ -214,7 +214,7 @@ BEGIN RETURN TRUE; END;
$$;
INSERT INTO morbac.contexts (name, description, evaluator) VALUES
('business_hours', 'During business hours (MonFri 917)', 'morbac.ctx_business_hours'::regproc),
('business_hours', 'During business hours (Mon-Fri 9-17)', 'morbac.ctx_business_hours'::regproc),
('after_hours', 'Outside business hours', 'morbac.ctx_after_hours'::regproc),
('end_of_quarter', 'End-of-quarter reporting window', 'morbac.ctx_end_of_quarter'::regproc);
@@ -274,7 +274,7 @@ JOIN morbac.contexts c ON c.name = v.context_name;
-- Disable auth cache so state changes between calls are always reflected
SELECT morbac.set_config('cache_ttl_seconds', '0');
-- morbac.t(label, actual, expected) boolean assertion
-- morbac.t(label, actual, expected) - boolean assertion
CREATE OR REPLACE FUNCTION morbac.t(label TEXT, actual BOOLEAN, expect BOOLEAN)
RETURNS TEXT LANGUAGE sql STABLE AS $$
SELECT CASE WHEN actual IS NOT DISTINCT FROM expect
@@ -284,7 +284,7 @@ RETURNS TEXT LANGUAGE sql STABLE AS $$
END;
$$;
-- morbac.t_null(label, actual) assert value is NULL
-- morbac.t_null(label, actual) - assert value is NULL
CREATE OR REPLACE FUNCTION morbac.t_null(label TEXT, actual TEXT)
RETURNS TEXT LANGUAGE sql STABLE AS $$
SELECT CASE WHEN actual IS NULL
@@ -293,7 +293,7 @@ RETURNS TEXT LANGUAGE sql STABLE AS $$
END;
$$;
-- morbac.t_not_null(label, actual) assert value is NOT NULL
-- morbac.t_not_null(label, actual) - assert value is NOT NULL
CREATE OR REPLACE FUNCTION morbac.t_not_null(label TEXT, actual TEXT)
RETURNS TEXT LANGUAGE sql STABLE AS $$
SELECT CASE WHEN actual IS NOT NULL
@@ -302,7 +302,7 @@ RETURNS TEXT LANGUAGE sql STABLE AS $$
END;
$$;
-- morbac.t_eq(label, actual, expected) assert two numeric values are equal
-- morbac.t_eq(label, actual, expected) - assert two numeric values are equal
CREATE OR REPLACE FUNCTION morbac.t_eq(label TEXT, actual NUMERIC, expect NUMERIC)
RETURNS TEXT LANGUAGE sql STABLE AS $$
SELECT CASE WHEN actual = expect
+40 -40
View File
@@ -5,25 +5,25 @@
-- No activity or view hierarchy is active yet (added in 02_hierarchies.sql).
--
-- Users and their roles at GlobalTech HQ:
-- Alice ceo (permission: inherits all via hierarchy)
-- Bob director (inherits manager -> employee -> intern)
-- Carol manager (inherits employee -> intern)
-- Dave employee (inherits intern)
-- Eve intern
-- Frank contractor (has prohibition on financial/hr data)
-- Grace hr_manager
-- Heidi auditor
-- Ivan accountant
-- Judy engineer@Engineering + sales_rep@Sales (multi-org)
-- Karl no role
-- Leo employee
-- Alice - ceo (permission: inherits all via hierarchy)
-- Bob - director (inherits manager -> employee -> intern)
-- Carol - manager (inherits employee -> intern)
-- Dave - employee (inherits intern)
-- Eve - intern
-- Frank - contractor (has prohibition on financial/hr data)
-- Grace - hr_manager
-- Heidi - auditor
-- Ivan - accountant
-- Judy - engineer@Engineering + sales_rep@Sales (multi-org)
-- Karl - no role
-- Leo - employee
--
-- Prerequisites: 00_setup.sql
-- =============================================================================
\echo ''
\echo '================================================================'
\echo '01 CORE AUTHORIZATION DECISIONS'
\echo '01 - CORE AUTHORIZATION DECISIONS'
\echo '================================================================'
-- ---------------------------------------------------------------------------
@@ -32,7 +32,7 @@
\echo ''
\echo '--- 1. Basic permission grants ---'
-- Dave (employee) can read documents has explicit permission
-- Dave (employee) can read documents - has explicit permission
SELECT morbac.t('Dave (employee) reads documents',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000004'::uuid,
@@ -40,7 +40,7 @@ SELECT morbac.t('Dave (employee) reads documents',
'read', 'documents'
), TRUE);
-- Dave (employee) can write documents context is business_hours (evaluates TRUE)
-- Dave (employee) can write documents - context is business_hours (evaluates TRUE)
SELECT morbac.t('Dave (employee) writes documents [business_hours context=true]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000004'::uuid,
@@ -89,12 +89,12 @@ SELECT morbac.t('Ivan (accountant) writes financial_data',
), TRUE);
-- ---------------------------------------------------------------------------
-- Section 2: Default deny no rule exists for the combination
-- Section 2: Default deny - no rule exists for the combination
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 2. Default deny (no permission rule) ---'
-- Dave (employee) cannot delete documents no delete permission for employee
-- Dave (employee) cannot delete documents - no delete permission for employee
SELECT morbac.t('Dave (employee) deletes documents [no permission]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000004'::uuid,
@@ -102,7 +102,7 @@ SELECT morbac.t('Dave (employee) deletes documents [no permission]',
'delete', 'documents'
), FALSE);
-- Eve (intern) cannot read documents intern only has public_data permission
-- Eve (intern) cannot read documents - intern only has public_data permission
SELECT morbac.t('Eve (intern) reads documents [intern has no docs permission]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000005'::uuid,
@@ -110,7 +110,7 @@ SELECT morbac.t('Eve (intern) reads documents [intern has no docs permission]',
'read', 'documents'
), FALSE);
-- Dave (employee) cannot read financial_data no rule for employee -> financial_data
-- Dave (employee) cannot read financial_data - no rule for employee -> financial_data
SELECT morbac.t('Dave (employee) reads financial_data [no permission before view hierarchy]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000004'::uuid,
@@ -118,7 +118,7 @@ SELECT morbac.t('Dave (employee) reads financial_data [no permission before view
'read', 'financial_data'
), FALSE);
-- Grace (hr_manager) cannot read audit_logs no rule for hr_manager -> audit_logs
-- Grace (hr_manager) cannot read audit_logs - no rule for hr_manager -> audit_logs
SELECT morbac.t('Grace (hr_manager) reads audit_logs [no permission]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000007'::uuid,
@@ -126,7 +126,7 @@ SELECT morbac.t('Grace (hr_manager) reads audit_logs [no permission]',
'read', 'audit_logs'
), FALSE);
-- Dave (employee) cannot approve documents no approve permission for employee
-- Dave (employee) cannot approve documents - no approve permission for employee
SELECT morbac.t('Dave (employee) approves documents [no permission]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000004'::uuid,
@@ -135,12 +135,12 @@ SELECT morbac.t('Dave (employee) approves documents [no permission]',
), FALSE);
-- ---------------------------------------------------------------------------
-- Section 3: Default deny user has no role at all
-- Section 3: Default deny - user has no role at all
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 3. Default deny (user has no role) ---'
-- Karl has no role anywhere all actions denied
-- Karl has no role anywhere - all actions denied
SELECT morbac.t('Karl (no role) reads documents',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000011'::uuid,
@@ -148,7 +148,7 @@ SELECT morbac.t('Karl (no role) reads documents',
'read', 'documents'
), FALSE);
-- Completely unknown user UUID EXPECT FALSE
-- Completely unknown user UUID - EXPECT FALSE
SELECT morbac.t('Unknown user reads documents',
morbac.is_allowed_nocache(
'ffffffff-ffff-ffff-ffff-ffffffffffff'::uuid,
@@ -162,7 +162,7 @@ SELECT morbac.t('Unknown user reads documents',
\echo ''
\echo '--- 4. Prohibition overrides permission ---'
-- Frank (contractor) reads documents permission granted, no prohibition
-- Frank (contractor) reads documents - permission granted, no prohibition
SELECT morbac.t('Frank (contractor) reads documents [has permission]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000006'::uuid,
@@ -170,7 +170,7 @@ SELECT morbac.t('Frank (contractor) reads documents [has permission]',
'read', 'documents'
), TRUE);
-- Frank (contractor) reads financial_data PROHIBITED
-- Frank (contractor) reads financial_data - PROHIBITED
SELECT morbac.t('Frank (contractor) reads financial_data [prohibited]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000006'::uuid,
@@ -178,7 +178,7 @@ SELECT morbac.t('Frank (contractor) reads financial_data [prohibited]',
'read', 'financial_data'
), FALSE);
-- Frank (contractor) reads hr_data prohibited
-- Frank (contractor) reads hr_data - prohibited
SELECT morbac.t('Frank (contractor) reads hr_data [prohibited]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000006'::uuid,
@@ -211,7 +211,7 @@ WHERE org_id = '10000000-0000-0000-0000-000000000001'
AND activity = 'read' AND view = 'financial_data' AND modality = 'permission';
-- ---------------------------------------------------------------------------
-- Section 5: Context filtering rule only applies when context is TRUE
-- Section 5: Context filtering - rule only applies when context is TRUE
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 5. Context filtering ---'
@@ -256,7 +256,7 @@ WHERE org_id = '10000000-0000-0000-0000-000000000001'
AND activity = 'export' AND view = 'documents';
-- ---------------------------------------------------------------------------
-- Section 6: Wrong organization user has no role in the target org
-- Section 6: Wrong organization - user has no role in the target org
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 6. Wrong organization ---'
@@ -278,12 +278,12 @@ SELECT morbac.t('Judy (Engineering engineer) reads GlobalTech financial_data [no
), FALSE);
-- ---------------------------------------------------------------------------
-- Section 7: Multi-organization user access scoped to each org independently
-- Section 7: Multi-organization user - access scoped to each org independently
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 7. Multi-org user (Judy) ---'
-- Judy is engineer at Engineering can read documents there
-- Judy is engineer at Engineering - can read documents there
SELECT morbac.t('Judy (engineer) reads Engineering documents',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000010'::uuid,
@@ -291,7 +291,7 @@ SELECT morbac.t('Judy (engineer) reads Engineering documents',
'read', 'documents'
), TRUE);
-- Judy is sales_rep at Sales can write contracts there
-- Judy is sales_rep at Sales - can write contracts there
SELECT morbac.t('Judy (sales_rep) writes Sales contracts',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000010'::uuid,
@@ -313,7 +313,7 @@ SELECT morbac.t('Judy writes documents at Sales [engineer perms dont carry over]
\echo ''
\echo '--- 8. Role hierarchy inheritance ---'
-- Carol (manager) inherits employee permissions can read documents (employee perm)
-- Carol (manager) inherits employee permissions - can read documents (employee perm)
SELECT morbac.t('Carol (manager, inherits employee) reads documents',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000003'::uuid,
@@ -321,7 +321,7 @@ SELECT morbac.t('Carol (manager, inherits employee) reads documents',
'read', 'documents'
), TRUE);
-- Carol (manager) has own permission can approve documents
-- Carol (manager) has own permission - can approve documents
SELECT morbac.t('Carol (manager) approves documents [own permission]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000003'::uuid,
@@ -329,7 +329,7 @@ SELECT morbac.t('Carol (manager) approves documents [own permission]',
'approve', 'documents'
), TRUE);
-- Bob (director) inherits manager -> employee chain can read documents
-- Bob (director) inherits manager -> employee chain - can read documents
SELECT morbac.t('Bob (director, inherits manager+employee) reads documents',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000002'::uuid,
@@ -337,7 +337,7 @@ SELECT morbac.t('Bob (director, inherits manager+employee) reads documents',
'read', 'documents'
), TRUE);
-- Bob (director) inherits manager can approve documents
-- Bob (director) inherits manager - can approve documents
SELECT morbac.t('Bob (director, inherits manager) approves documents',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000002'::uuid,
@@ -345,7 +345,7 @@ SELECT morbac.t('Bob (director, inherits manager) approves documents',
'approve', 'documents'
), TRUE);
-- Alice (CEO) inherits the entire hierarchy can do everything below
-- Alice (CEO) inherits the entire hierarchy - can do everything below
SELECT morbac.t('Alice (CEO, inherits all) reads documents',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000001'::uuid,
@@ -367,7 +367,7 @@ SELECT morbac.t('Alice (CEO, inherits all) deletes documents',
'delete', 'documents'
), TRUE);
-- Eve (intern) cannot approve intern has no approve permission
-- Eve (intern) cannot approve - intern has no approve permission
SELECT morbac.t('Eve (intern) approves documents [intern has no approve permission]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000005'::uuid,
@@ -464,7 +464,7 @@ SELECT morbac.t('Dave reads contracts: prohibition priority=10 beats permission
-- Equal priorities: prohibition wins (modality tiebreaker)
UPDATE morbac.rules SET priority = 5 WHERE id = 'e0000000-0000-0000-0000-000000000001';
SELECT morbac.t('Dave reads contracts: equal priority prohibition wins by modality precedence [denied]',
SELECT morbac.t('Dave reads contracts: equal priority - prohibition wins by modality precedence [denied]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000004'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
@@ -477,7 +477,7 @@ UPDATE morbac.rules SET priority = NULL WHERE id IN (
'e0000000-0000-0000-0000-000000000002'
);
SELECT morbac.t('Dave reads contracts: no priority set prohibition wins by default [denied]',
SELECT morbac.t('Dave reads contracts: no priority set - prohibition wins by default [denied]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000004'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid,
+38 -38
View File
@@ -2,10 +2,10 @@
-- Hierarchy Tests
-- =============================================================================
-- Tests all four hierarchy types:
-- 1. Role hierarchy senior roles inherit permissions of junior roles (transitive)
-- 2. Activity hierarchy requesting a senior activity also matches junior-activity rules
-- 3. View hierarchy requesting a senior view also matches junior-view rules
-- 4. Org hierarchy get_org_ancestors / get_org_descendants traversal
-- 1. Role hierarchy - senior roles inherit permissions of junior roles (transitive)
-- 2. Activity hierarchy - requesting a senior activity also matches junior-activity rules
-- 3. View hierarchy - requesting a senior view also matches junior-view rules
-- 4. Org hierarchy - get_org_ancestors / get_org_descendants traversal
--
-- Hierarchy semantics in this system:
-- Activity: (senior='write', junior='read') means get_effective_activities('write')
@@ -19,50 +19,50 @@
\echo ''
\echo '================================================================'
\echo '02 HIERARCHIES'
\echo '02 - HIERARCHIES'
\echo '================================================================'
-- ---------------------------------------------------------------------------
-- Section 1: Role hierarchy get_effective_roles and get_inherited_roles
-- Section 1: Role hierarchy - get_effective_roles and get_inherited_roles
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 1. Role hierarchy introspection ---'
-- Eve (intern) direct assignment only 1 effective role
-- Eve (intern) direct assignment only - 1 effective role
SELECT morbac.t_eq('Eve has 1 effective role (intern only)',
(SELECT COUNT(*) FROM morbac.get_effective_roles(
'30000000-0000-0000-0000-000000000005'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid
))::bigint, 1);
-- Dave (employee) has employee + intern via hierarchy 2 effective roles
-- Dave (employee) has employee + intern via hierarchy - 2 effective roles
SELECT morbac.t_eq('Dave has 2 effective roles (employee, intern)',
(SELECT COUNT(*) FROM morbac.get_effective_roles(
'30000000-0000-0000-0000-000000000004'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid
))::bigint, 2);
-- Carol (manager) has manager + employee + intern via hierarchy 3 effective roles
-- Carol (manager) has manager + employee + intern via hierarchy - 3 effective roles
SELECT morbac.t_eq('Carol has 3 effective roles (manager, employee, intern)',
(SELECT COUNT(*) FROM morbac.get_effective_roles(
'30000000-0000-0000-0000-000000000003'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid
))::bigint, 3);
-- Alice (CEO) has ceo, director, manager, employee, intern 5 effective roles
-- Alice (CEO) has ceo, director, manager, employee, intern - 5 effective roles
SELECT morbac.t_eq('Alice has 5 effective roles (ceo through intern)',
(SELECT COUNT(*) FROM morbac.get_effective_roles(
'30000000-0000-0000-0000-000000000001'::uuid,
'10000000-0000-0000-0000-000000000001'::uuid
))::bigint, 5);
-- get_inherited_roles for manager manager itself + employee + intern = 3
-- get_inherited_roles for manager - manager itself + employee + intern = 3
SELECT morbac.t_eq('get_inherited_roles(manager) returns 3 roles (manager, employee, intern)',
(SELECT COUNT(*) FROM morbac.get_inherited_roles(
'20000000-0001-0000-0000-000000000003'::uuid
))::bigint, 3);
-- get_inherited_roles for ceo entire chain = 5
-- get_inherited_roles for ceo - entire chain = 5
SELECT morbac.t_eq('get_inherited_roles(ceo) returns 5 roles (ceo through intern)',
(SELECT COUNT(*) FROM morbac.get_inherited_roles(
'20000000-0001-0000-0000-000000000001'::uuid
@@ -83,10 +83,10 @@ SELECT morbac.t('intern role appears in manager inherited roles',
), TRUE);
-- ---------------------------------------------------------------------------
-- Section 2: Role hierarchy authorization via inheritance (transitive)
-- Section 2: Role hierarchy - authorization via inheritance (transitive)
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 2. Role hierarchy authorization via inheritance ---'
\echo '--- 2. Role hierarchy - authorization via inheritance ---'
-- Carol (manager) has employee permission (read documents) via 1-level inheritance
SELECT morbac.t('Carol (manager, 1-level inherit) reads documents',
@@ -128,7 +128,7 @@ SELECT morbac.t('Carol (manager) approves documents (own perm)',
'approve', 'documents'
), TRUE);
-- Eve (intern) cannot approve no role above intern has approve perm
-- Eve (intern) cannot approve - no role above intern has approve perm
SELECT morbac.t('Eve (intern) approves documents [inheritance only goes up in seniority]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000005'::uuid,
@@ -306,7 +306,7 @@ SELECT morbac.t_eq('get_effective_activities(export) returns 2 (export, read)',
-- Rule: employee has 'read documents' permission.
-- With hierarchy (write->read), requesting 'write' also matches the 'read' rule.
-- Dave (employee) requests 'write' matches 'read' rule via write->read hierarchy
-- Dave (employee) requests 'write' - matches 'read' rule via write->read hierarchy
SELECT morbac.t('Dave (employee, has read perm) writes docs via activity hierarchy',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000004'::uuid,
@@ -314,7 +314,7 @@ SELECT morbac.t('Dave (employee, has read perm) writes docs via activity hierarc
'write', 'documents'
), TRUE);
-- Dave (employee) requests 'delete' delete->write->read chain, 'read' rule matches
-- Dave (employee) requests 'delete' - delete->write->read chain, 'read' rule matches
SELECT morbac.t('Dave (employee, has read perm) deletes docs via delete->write->read hierarchy',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000004'::uuid,
@@ -322,7 +322,7 @@ SELECT morbac.t('Dave (employee, has read perm) deletes docs via delete->write->
'delete', 'documents'
), TRUE);
-- Dave (employee) requests 'export' export->read, 'read' rule matches
-- Dave (employee) requests 'export' - export->read, 'read' rule matches
SELECT morbac.t('Dave (employee, has read perm) exports docs via export->read hierarchy',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000004'::uuid,
@@ -330,7 +330,7 @@ SELECT morbac.t('Dave (employee, has read perm) exports docs via export->read hi
'export', 'documents'
), TRUE);
-- Eve (intern) has only 'read public_data' requesting 'write public_data' also matches
-- Eve (intern) has only 'read public_data' - requesting 'write public_data' also matches
SELECT morbac.t('Eve (intern, has read perm) writes public_data via activity hierarchy',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000005'::uuid,
@@ -338,7 +338,7 @@ SELECT morbac.t('Eve (intern, has read perm) writes public_data via activity hie
'write', 'public_data'
), TRUE);
-- 'audit' is not in the hierarchy no junior, no senior no match for employee
-- 'audit' is not in the hierarchy - no junior, no senior - no match for employee
SELECT morbac.t('Dave (employee) audits documents [audit not in hierarchy, no permission]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000004'::uuid,
@@ -385,7 +385,7 @@ SELECT morbac.t('documents is in effective views of financial_data',
-- requesting 'financial_data' -> get_effective_views('financial_data') = {financial_data, documents}
-- the 'documents' rule matches -> access granted
-- Dave (employee) reads financial_data matches employee's 'read documents' rule via view hierarchy
-- Dave (employee) reads financial_data - matches employee's 'read documents' rule via view hierarchy
SELECT morbac.t('Dave (employee, has read documents) reads financial_data via view hierarchy',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000004'::uuid,
@@ -393,7 +393,7 @@ SELECT morbac.t('Dave (employee, has read documents) reads financial_data via vi
'read', 'financial_data'
), TRUE);
-- Dave (employee) reads hr_data matches 'read documents' via view hierarchy
-- Dave (employee) reads hr_data - matches 'read documents' via view hierarchy
SELECT morbac.t('Dave (employee, has read documents) reads hr_data via view hierarchy',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000004'::uuid,
@@ -401,7 +401,7 @@ SELECT morbac.t('Dave (employee, has read documents) reads hr_data via view hier
'read', 'hr_data'
), TRUE);
-- Frank (contractor) reads documents permitted
-- Frank (contractor) reads documents - permitted
SELECT morbac.t('Frank (contractor) reads documents [has permission]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000006'::uuid,
@@ -409,8 +409,8 @@ SELECT morbac.t('Frank (contractor) reads documents [has permission]',
'read', 'documents'
), TRUE);
-- Frank (contractor) reads financial_data matches contractor's 'read documents' rule via view hierarchy
-- BUT contractor has a PROHIBITION on financial_data prohibition wins
-- Frank (contractor) reads financial_data - matches contractor's 'read documents' rule via view hierarchy
-- BUT contractor has a PROHIBITION on financial_data - prohibition wins
SELECT morbac.t('Frank (contractor) reads financial_data [prohibition overrides view-hierarchy match]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000006'::uuid,
@@ -436,7 +436,7 @@ SELECT morbac.t_eq('Sales Dept has 2 ancestors (self + GlobalTech HQ)',
'10000000-0000-0000-0000-000000000003'::uuid
))::bigint, 2);
-- GlobalTech HQ is the root only 1 ancestor (itself)
-- GlobalTech HQ is the root - only 1 ancestor (itself)
SELECT morbac.t_eq('GlobalTech HQ (root) has 1 ancestor (itself only)',
(SELECT COUNT(*) FROM morbac.get_org_ancestors(
'10000000-0000-0000-0000-000000000001'::uuid
@@ -478,17 +478,17 @@ SELECT morbac.t('Dave (GlobalTech employee) reads Engineering docs [no cross-org
), FALSE);
-- ---------------------------------------------------------------------------
-- Section 6: get_org_scope named scope helper
-- Section 6: get_org_scope - named scope helper
-- ---------------------------------------------------------------------------
-- Org tree used in tests:
-- GlobalTech HQ (root) id: 10000000-0000-0000-0000-000000000001
-- ├── Engineering Dept id: 10000000-0000-0000-0000-000000000002
-- └── Sales Dept id: 10000000-0000-0000-0000-000000000003
-- +-- Engineering Dept id: 10000000-0000-0000-0000-000000000002
-- \-- Sales Dept id: 10000000-0000-0000-0000-000000000003
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 6. get_org_scope ---'
-- 'self' always returns exactly the org itself
-- 'self' - always returns exactly the org itself
SELECT morbac.t_eq('scope self (root) returns 1 row',
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'self'))::bigint, 1);
@@ -501,7 +501,7 @@ SELECT morbac.t('scope self returns the org itself at depth 0',
WHERE org_id = '10000000-0000-0000-0000-000000000002' AND depth = 0
), TRUE);
-- 'children' direct children only (depth = 1 descendants)
-- 'children' - direct children only (depth = 1 descendants)
SELECT morbac.t_eq('scope children of root returns 2 rows (Engineering + Sales)',
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'children'))::bigint, 2);
@@ -520,7 +520,7 @@ SELECT morbac.t('scope children includes Engineering at depth 1',
WHERE org_id = '10000000-0000-0000-0000-000000000002' AND depth = 1
), TRUE);
-- 'descendants' all descendants excluding self
-- 'descendants' - all descendants excluding self
SELECT morbac.t_eq('scope descendants of root returns 2 rows (Engineering + Sales, no self)',
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'descendants'))::bigint, 2);
@@ -533,7 +533,7 @@ SELECT morbac.t('scope descendants does not include self',
SELECT morbac.t_eq('scope descendants of leaf returns 0 rows',
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000002'::uuid, 'descendants'))::bigint, 0);
-- 'subtree' self + all descendants
-- 'subtree' - self + all descendants
SELECT morbac.t_eq('scope subtree of root returns 3 rows (self + Engineering + Sales)',
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'subtree'))::bigint, 3);
@@ -546,7 +546,7 @@ SELECT morbac.t('scope subtree includes self at depth 0',
SELECT morbac.t_eq('scope subtree of leaf returns 1 row (self only)',
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000002'::uuid, 'subtree'))::bigint, 1);
-- 'parent' direct parent only
-- 'parent' - direct parent only
SELECT morbac.t_eq('scope parent of Engineering returns 1 row (GlobalTech HQ)',
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000002'::uuid, 'parent'))::bigint, 1);
@@ -559,7 +559,7 @@ SELECT morbac.t('scope parent of Engineering returns GlobalTech HQ at depth 1',
SELECT morbac.t_eq('scope parent of root returns 0 rows (no parent)',
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'parent'))::bigint, 0);
-- 'ancestors' all ancestors excluding self
-- 'ancestors' - all ancestors excluding self
SELECT morbac.t_eq('scope ancestors of Engineering returns 1 row (GlobalTech HQ only)',
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000002'::uuid, 'ancestors'))::bigint, 1);
@@ -572,7 +572,7 @@ SELECT morbac.t('scope ancestors does not include self',
SELECT morbac.t_eq('scope ancestors of root returns 0 rows',
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'ancestors'))::bigint, 0);
-- 'lineage' self + all ancestors
-- 'lineage' - self + all ancestors
SELECT morbac.t_eq('scope lineage of Engineering returns 2 rows (self + GlobalTech HQ)',
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000002'::uuid, 'lineage'))::bigint, 2);
@@ -585,7 +585,7 @@ SELECT morbac.t('scope lineage includes self at depth 0',
SELECT morbac.t_eq('scope lineage of root returns 1 row (self only)',
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'lineage'))::bigint, 1);
-- 'root' topmost ancestor only
-- 'root' - topmost ancestor only
SELECT morbac.t_eq('scope root of Engineering returns 1 row',
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000002'::uuid, 'root'))::bigint, 1);
@@ -604,7 +604,7 @@ SELECT morbac.t('scope root of root returns the org itself',
WHERE org_id = '10000000-0000-0000-0000-000000000001'
), TRUE);
-- p_max_depth depth limiting
-- p_max_depth - depth limiting
SELECT morbac.t_eq('scope subtree max_depth=0 returns only self',
(SELECT COUNT(*) FROM morbac.get_org_scope('10000000-0000-0000-0000-000000000001'::uuid, 'subtree', 0))::bigint, 1);
+8 -8
View File
@@ -20,11 +20,11 @@
\echo ''
\echo '================================================================'
\echo '03 DELEGATION'
\echo '03 - DELEGATION'
\echo '================================================================'
-- ---------------------------------------------------------------------------
-- Section 1: Baseline Leo (employee) before any delegation
-- Section 1: Baseline - Leo (employee) before any delegation
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 1. Baseline: Leo before delegation ---'
@@ -37,7 +37,7 @@ SELECT morbac.t('Leo (employee) reads documents before delegation',
'read', 'documents'
), TRUE);
-- Leo cannot approve documents that requires manager role
-- Leo cannot approve documents - that requires manager role
SELECT morbac.t('Leo (employee) approves documents before delegation [no manager perm]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000012'::uuid,
@@ -66,7 +66,7 @@ SELECT morbac.t('Leo (employee) does not have approve permission before delegati
), FALSE);
-- ---------------------------------------------------------------------------
-- Section 2: Active delegation Carol delegates manager role to Leo
-- Section 2: Active delegation - Carol delegates manager role to Leo
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 2. Active delegation: Carol -> Leo (manager role, 1 day) ---'
@@ -83,7 +83,7 @@ VALUES (
now() + interval '1 day'
);
-- Leo now has delegated manager role can approve documents
-- Leo now has delegated manager role - can approve documents
SELECT morbac.t('Leo (delegated manager) approves documents',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000012'::uuid,
@@ -217,7 +217,7 @@ VALUES (
TRUE -- revoked
);
-- Dave should NOT get accountant from revoked delegation verify revoked flag is set
-- Dave should NOT get accountant from revoked delegation - verify revoked flag is set
-- Note: employee already has write financial_data via view hierarchy;
-- so we verify the delegation is actually revoked in the DB.
SELECT morbac.t('Revoked accountant delegation has revoked=TRUE in DB',
@@ -296,7 +296,7 @@ INSERT INTO morbac.delegations
(id, delegator_id, delegatee_id, role_id, org_id, valid_from, valid_until)
VALUES (
'de000001-0000-0000-0000-000000000005',
'30000000-0000-0000-0000-000000000004', -- Dave (employee does NOT hold manager)
'30000000-0000-0000-0000-000000000004', -- Dave (employee - does NOT hold manager)
'30000000-0000-0000-0000-000000000005', -- Eve
'20000000-0001-0000-0000-000000000003', -- manager role
'10000000-0000-0000-0000-000000000001',
@@ -330,7 +330,7 @@ SELECT morbac.t('Invalid delegation not in Eve comprehensive roles',
\echo ''
\echo '--- 8. get_comprehensive_roles source reporting ---'
-- Alice (CEO, direct) source should be 'direct'
-- Alice (CEO, direct) - source should be 'direct'
SELECT morbac.t('Alice CEO role has source=direct in comprehensive roles',
EXISTS(
SELECT 1 FROM morbac.get_comprehensive_roles(
+30 -30
View File
@@ -2,10 +2,10 @@
-- Constraint Tests
-- =============================================================================
-- Tests business constraints:
-- 1. Separation of Duty (SoD) mutually exclusive roles
-- 2. Negative role assignments explicit blocking of a role
-- 3. Role cardinality constraints min/max users per role
-- 4. Rule conflict detection modality conflicts on same tuple
-- 1. Separation of Duty (SoD) - mutually exclusive roles
-- 2. Negative role assignments - explicit blocking of a role
-- 3. Role cardinality constraints - min/max users per role
-- 4. Rule conflict detection - modality conflicts on same tuple
--
-- Scenario:
-- - auditor and accountant are mutually exclusive (no one can hold both)
@@ -19,11 +19,11 @@
\echo ''
\echo '================================================================'
\echo '04 CONSTRAINTS'
\echo '04 - CONSTRAINTS'
\echo '================================================================'
-- ---------------------------------------------------------------------------
-- Section 1: Separation of Duty define conflict
-- Section 1: Separation of Duty - define conflict
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 1. Separation of Duty setup ---'
@@ -49,7 +49,7 @@ SELECT morbac.t_eq('SoD conflict between auditor and accountant created',
\echo ''
\echo '--- 2. SoD violation detection ---'
-- Heidi (auditor) check if assigning accountant role would violate SoD
-- Heidi (auditor) - check if assigning accountant role would violate SoD
SELECT morbac.t('Assigning accountant to Heidi (auditor) violates SoD',
morbac.check_sod_violation(
'30000000-0000-0000-0000-000000000008'::uuid, -- Heidi
@@ -57,7 +57,7 @@ SELECT morbac.t('Assigning accountant to Heidi (auditor) violates SoD',
'10000000-0000-0000-0000-000000000001'::uuid
), TRUE);
-- Ivan (accountant) check if assigning auditor role would violate SoD
-- Ivan (accountant) - check if assigning auditor role would violate SoD
SELECT morbac.t('Assigning auditor to Ivan (accountant) violates SoD [symmetric]',
morbac.check_sod_violation(
'30000000-0000-0000-0000-000000000009'::uuid, -- Ivan
@@ -65,8 +65,8 @@ SELECT morbac.t('Assigning auditor to Ivan (accountant) violates SoD [symmetric]
'10000000-0000-0000-0000-000000000001'::uuid
), TRUE);
-- Dave (employee) check if assigning accountant would violate SoD
-- Dave is not an auditor no conflict
-- Dave (employee) - check if assigning accountant would violate SoD
-- Dave is not an auditor - no conflict
SELECT morbac.t('Assigning accountant to Dave (not an auditor) does not violate SoD',
morbac.check_sod_violation(
'30000000-0000-0000-0000-000000000004'::uuid, -- Dave
@@ -74,7 +74,7 @@ SELECT morbac.t('Assigning accountant to Dave (not an auditor) does not violate
'10000000-0000-0000-0000-000000000001'::uuid
), FALSE);
-- Heidi (auditor) assigning a non-conflicting role (manager) is fine
-- Heidi (auditor) - assigning a non-conflicting role (manager) is fine
SELECT morbac.t('Assigning manager to Heidi (auditor) does not violate SoD',
morbac.check_sod_violation(
'30000000-0000-0000-0000-000000000008'::uuid, -- Heidi
@@ -126,7 +126,7 @@ VALUES (
'Frank is a contractor and must not gain employee-level access'
);
-- Frank's employee role is negated verify via get_comprehensive_roles
-- Frank's employee role is negated - verify via get_comprehensive_roles
-- employee role must not appear (negated by negative assignment)
SELECT morbac.t('Frank: employee role excluded by negative assignment',
NOT EXISTS(
@@ -148,7 +148,7 @@ SELECT morbac.t('Frank: contractor role still present after employee negated',
), TRUE);
-- Negative assignment on a role the user never had is harmless
-- Karl has no role adding negative assignment for manager is a no-op
-- Karl has no role - adding negative assignment for manager is a no-op
INSERT INTO morbac.negative_role_assignments (user_id, role_id, org_id, reason)
VALUES (
'30000000-0000-0000-0000-000000000011', -- Karl
@@ -181,7 +181,7 @@ WHERE user_id = '30000000-0000-0000-0000-000000000006'
\echo ''
\echo '--- 4. Role cardinality constraints ---'
-- Set a cardinality constraint: compliance_officer role min 1, max 2
-- Set a cardinality constraint: compliance_officer role - min 1, max 2
INSERT INTO morbac.role_cardinality (role_id, min_users, max_users, description)
VALUES (
'20000000-0001-0000-0000-000000000010', -- compliance_officer
@@ -189,8 +189,8 @@ VALUES (
'Compliance officer role: at least 1, at most 2'
);
-- Currently 0 users have compliance_officer adding one should be fine (0 < max=2)
SELECT morbac.t_null('Adding first compliance_officer (0 users, max=2) no violation',
-- Currently 0 users have compliance_officer - adding one should be fine (0 < max=2)
SELECT morbac.t_null('Adding first compliance_officer (0 users, max=2) - no violation',
morbac.check_cardinality_violation(
'20000000-0001-0000-0000-000000000010'::uuid,
TRUE -- adding
@@ -201,15 +201,15 @@ INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES
('30000000-0000-0000-0000-000000000004', '20000000-0001-0000-0000-000000000010', '10000000-0000-0000-0000-000000000001'),
('30000000-0000-0000-0000-000000000005', '20000000-0001-0000-0000-000000000010', '10000000-0000-0000-0000-000000000001');
-- Now 2 users at max. Trying to add a 3rd should violate
SELECT morbac.t_not_null('Adding 3rd compliance_officer (2 users, max=2) violation returned',
-- Now 2 users - at max. Trying to add a 3rd should violate
SELECT morbac.t_not_null('Adding 3rd compliance_officer (2 users, max=2) - violation returned',
morbac.check_cardinality_violation(
'20000000-0001-0000-0000-000000000010'::uuid,
TRUE -- adding
));
-- Removing one 2 users, min=1 removing leaves 1 which is min=1, should be fine
SELECT morbac.t_null('Removing from 2 compliance_officers (min=1) no violation (still above min)',
-- Removing one - 2 users, min=1 - removing leaves 1 which is >= min=1, should be fine
SELECT morbac.t_null('Removing from 2 compliance_officers (min=1) - no violation (still above min)',
morbac.check_cardinality_violation(
'20000000-0001-0000-0000-000000000010'::uuid,
FALSE -- removing
@@ -221,21 +221,21 @@ WHERE user_id = '30000000-0000-0000-0000-000000000005'
AND role_id = '20000000-0001-0000-0000-000000000010';
-- 1 user remaining = min. Removing the last one would violate min=1
SELECT morbac.t_not_null('Removing last compliance_officer (1 user, min=1) violation returned',
SELECT morbac.t_not_null('Removing last compliance_officer (1 user, min=1) - violation returned',
morbac.check_cardinality_violation(
'20000000-0001-0000-0000-000000000010'::uuid,
FALSE -- removing
));
-- Adding again after being at 1 1 user, max=2 ok
SELECT morbac.t_null('Adding when at 1 compliance_officer (max=2) no violation',
-- Adding again after being at 1 - 1 user, max=2 - ok
SELECT morbac.t_null('Adding when at 1 compliance_officer (max=2) - no violation',
morbac.check_cardinality_violation(
'20000000-0001-0000-0000-000000000010'::uuid,
TRUE -- adding
));
-- Role with no cardinality constraint no violation for any operation
SELECT morbac.t_null('Checking cardinality for employee role (no constraint) no violation',
-- Role with no cardinality constraint - no violation for any operation
SELECT morbac.t_null('Checking cardinality for employee role (no constraint) - no violation',
morbac.check_cardinality_violation(
'20000000-0001-0000-0000-000000000004'::uuid, -- employee
TRUE
@@ -263,8 +263,8 @@ VALUES (
'permission'
);
-- No conflict yet only a permission exists
SELECT morbac.t_eq('detect_rule_conflicts: permission alone no conflicts',
-- No conflict yet - only a permission exists
SELECT morbac.t_eq('detect_rule_conflicts: permission alone - no conflicts',
(SELECT COUNT(*) FROM morbac.detect_rule_conflicts(
'10000000-0000-0000-0000-000000000001',
'20000000-0001-0000-0000-000000000004',
@@ -309,7 +309,7 @@ SELECT morbac.t('detect_rule_conflicts: conflicting rule is the permission',
WHERE conflicting_modality = 'permission'
), TRUE);
-- Insert an obligation for the same tuple conflicts with the prohibition
-- Insert an obligation for the same tuple - conflicts with the prohibition
INSERT INTO morbac.rules (id, org_id, role_id, activity, view, context_id, modality)
VALUES (
'f0000000-0000-0000-0000-000000000003',
@@ -331,7 +331,7 @@ SELECT morbac.t_eq('detect_rule_conflicts: obligation conflicts with existing pr
))::bigint,
1);
-- Insert a recommendation conflicts with both obligation and prohibition
-- Insert a recommendation - conflicts with both obligation and prohibition
INSERT INTO morbac.rules (id, org_id, role_id, activity, view, context_id, modality)
VALUES (
'f0000000-0000-0000-0000-000000000004',
@@ -354,7 +354,7 @@ SELECT morbac.t_eq('detect_rule_conflicts: recommendation conflicts with prohibi
2);
-- No conflict between permission and recommendation (they coexist meaningfully)
SELECT morbac.t_eq('detect_rule_conflicts: permission vs recommendation no conflict',
SELECT morbac.t_eq('detect_rule_conflicts: permission vs recommendation - no conflict',
(SELECT COUNT(*) FROM morbac.detect_rule_conflicts(
'10000000-0000-0000-0000-000000000001',
'20000000-0001-0000-0000-000000000004',
+14 -14
View File
@@ -9,7 +9,7 @@
-- 3. Expired rule (valid_until in the past) -> denied
-- 4. Future rule (valid_from in the future) -> denied
-- 5. Active time window (valid_from past, valid_until future) -> allowed
-- 6. Multiple rules for same combination only active ones count
-- 6. Multiple rules for same combination - only active ones count
-- 7. Expired prohibition: no longer blocks access after it expires
-- 8. Temporal rules interact correctly with role hierarchy
--
@@ -21,7 +21,7 @@
\echo ''
\echo '================================================================'
\echo '05 TEMPORAL CONSTRAINTS'
\echo '05 - TEMPORAL CONSTRAINTS'
\echo '================================================================'
-- ---------------------------------------------------------------------------
@@ -37,35 +37,35 @@ INSERT INTO morbac.views (name, description) VALUES
\echo '--- 1. is_rule_valid() helper ---'
-- No bounds: always valid
SELECT morbac.t('is_rule_valid(NULL, NULL) always valid',
SELECT morbac.t('is_rule_valid(NULL, NULL) - always valid',
morbac.is_rule_valid(NULL::timestamptz, NULL::timestamptz), TRUE);
-- Past valid_from, no valid_until: currently active
SELECT morbac.t('is_rule_valid(past, NULL) started in past, no end',
SELECT morbac.t('is_rule_valid(past, NULL) - started in past, no end',
morbac.is_rule_valid('2000-01-01'::timestamptz, NULL), TRUE);
-- Future valid_from: not yet active
SELECT morbac.t('is_rule_valid(future, NULL) not yet started',
SELECT morbac.t('is_rule_valid(future, NULL) - not yet started',
morbac.is_rule_valid('2099-01-01'::timestamptz, NULL), FALSE);
-- Past valid_until: expired
SELECT morbac.t('is_rule_valid(NULL, past) already expired',
SELECT morbac.t('is_rule_valid(NULL, past) - already expired',
morbac.is_rule_valid(NULL, '2000-01-01'::timestamptz), FALSE);
-- Future valid_until, no valid_from: currently active
SELECT morbac.t('is_rule_valid(NULL, future) no start, future end',
SELECT morbac.t('is_rule_valid(NULL, future) - no start, future end',
morbac.is_rule_valid(NULL, '2099-01-01'::timestamptz), TRUE);
-- Active window: past start, future end
SELECT morbac.t('is_rule_valid(past, future) within active window',
SELECT morbac.t('is_rule_valid(past, future) - within active window',
morbac.is_rule_valid('2000-01-01'::timestamptz, '2099-01-01'::timestamptz), TRUE);
-- Fully past window (both start and end in the past)
SELECT morbac.t('is_rule_valid(past_start, past_end) entirely expired',
SELECT morbac.t('is_rule_valid(past_start, past_end) - entirely expired',
morbac.is_rule_valid('2000-01-01'::timestamptz, '2001-01-01'::timestamptz), FALSE);
-- Fully future window (both start and end in the future)
SELECT morbac.t('is_rule_valid(future_start, future_end) entirely in the future',
SELECT morbac.t('is_rule_valid(future_start, future_end) - entirely in the future',
morbac.is_rule_valid('2090-01-01'::timestamptz, '2099-01-01'::timestamptz), FALSE);
-- ---------------------------------------------------------------------------
@@ -105,7 +105,7 @@ SELECT morbac.t('is_active = FALSE after valid_until set to past',
DELETE FROM morbac.rules WHERE activity = 'audit' AND view = 'temp_view';
-- ---------------------------------------------------------------------------
-- Section 3: Expired rule valid_until in the past
-- Section 3: Expired rule - valid_until in the past
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 3. Expired rule (valid_until in the past) ---'
@@ -129,7 +129,7 @@ SELECT morbac.t('Dave (employee) exports temp_view via expired rule [denied]',
), FALSE);
-- ---------------------------------------------------------------------------
-- Section 4: Future rule valid_from in the future
-- Section 4: Future rule - valid_from in the future
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 4. Future rule (valid_from in the future) ---'
@@ -209,7 +209,7 @@ SELECT morbac.t('Dave exports temp_view (export rule now active)',
), TRUE);
-- ---------------------------------------------------------------------------
-- Section 7: Temporal prohibition expired prohibition no longer blocks
-- Section 7: Temporal prohibition - expired prohibition no longer blocks
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 7. Expired prohibition no longer blocks ---'
@@ -295,7 +295,7 @@ SELECT morbac.t('Carol (manager) approves temp_view [temporal rule, active]',
'approve', 'temp_view'
), TRUE);
-- Alice (CEO) inherits from manager should also get the temporal permission
-- Alice (CEO) inherits from manager - should also get the temporal permission
SELECT morbac.t('Alice (CEO, inherits manager) approves temp_view [temporal rule, active]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000001'::uuid,
+5 -5
View File
@@ -22,7 +22,7 @@
\echo ''
\echo '================================================================'
\echo '06 CROSS-ORGANIZATIONAL RULES'
\echo '06 - CROSS-ORGANIZATIONAL RULES'
\echo '================================================================'
-- ---------------------------------------------------------------------------
@@ -53,7 +53,7 @@ SELECT morbac.t('Nina has eng_auditor role at Engineering',
), TRUE);
-- ---------------------------------------------------------------------------
-- Section 1: No cross-org rule access between orgs is denied by default
-- Section 1: No cross-org rule - access between orgs is denied by default
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 1. No cross-org rule: access denied by default ---'
@@ -75,7 +75,7 @@ SELECT morbac.t('Judy (Engineering engineer) reads GlobalTech financial_data [no
), FALSE);
-- ---------------------------------------------------------------------------
-- Section 2: Cross-org permission Sales sales_rep reads GlobalTech reports
-- Section 2: Cross-org permission - Sales sales_rep reads GlobalTech reports
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 2. Cross-org permission ---'
@@ -123,7 +123,7 @@ SELECT morbac.t('Judy (Sales sales_rep) reads GlobalTech documents [no rule for
\echo ''
\echo '--- 3. Role must be held in source org ---'
-- Karl has no role anywhere cannot use the Sales->GlobalTech cross-org rule
-- Karl has no role anywhere - cannot use the Sales->GlobalTech cross-org rule
SELECT morbac.t('Karl (no role) reads GlobalTech reports via cross-org rule [no role in source]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000011'::uuid,
@@ -140,7 +140,7 @@ SELECT morbac.t('Karl (no role) reads GlobalTech documents [no access anywhere]'
), FALSE);
-- ---------------------------------------------------------------------------
-- Section 4: Cross-org prohibition blocks access even with regular permission
-- Section 4: Cross-org prohibition - blocks access even with regular permission
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 4. Cross-org prohibition ---'
+3 -3
View File
@@ -19,7 +19,7 @@
\echo ''
\echo '================================================================'
\echo '07 AUDIT LOGGING'
\echo '07 - AUDIT LOGGING'
\echo '================================================================'
-- Clear any existing audit log entries to start fresh
@@ -160,7 +160,7 @@ DELETE FROM morbac.rules WHERE id = 'a0000000-0000-0000-0000-000000000001';
\echo ''
\echo '--- 5. Query audit log by record_id ---'
-- The test rule had INSERT, UPDATE, DELETE should be 3 entries
-- The test rule had INSERT, UPDATE, DELETE - should be 3 entries
SELECT morbac.t_eq('Audit log has 3 entries for test rule record (INSERT + UPDATE + DELETE)',
(SELECT COUNT(*) FROM morbac.audit_log
WHERE table_name = 'rules'
@@ -245,7 +245,7 @@ BEGIN
END;
$$;
-- Perform another user_roles change should NOT be logged
-- Perform another user_roles change - should NOT be logged
INSERT INTO morbac.user_roles (user_id, role_id, org_id)
VALUES (
'30000000-0000-0000-0000-000000000011',
+4 -4
View File
@@ -10,7 +10,7 @@
-- 2. Grant permissions via regular rules, verify access
-- 3. Prohibition overrides permission (standard engine behavior)
-- 4. Role hierarchy applies: senior role inherits permissions
-- 5. assign_role() / revoke_role() SoD/cardinality enforcement, RLS guards the INSERT/DELETE
-- 5. assign_role() / revoke_role() - SoD/cardinality enforcement, RLS guards the INSERT/DELETE
-- 6. RLS on morbac tables: session user cannot read/write without rules
-- 7. Rules are org-scoped
--
@@ -19,7 +19,7 @@
\echo ''
\echo '================================================================'
\echo '08 SYSTEM ACCESS'
\echo '08 - SYSTEM ACCESS'
\echo '================================================================'
-- ---------------------------------------------------------------------------
@@ -218,7 +218,7 @@ GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA morbac TO morbac_rls_tester;
SET SESSION AUTHORIZATION morbac_rls_tester;
-- Grace (hr_manager) has create/delete on user_roles RLS should allow
-- Grace (hr_manager) has create/delete on user_roles - RLS should allow
SET morbac.user_id = '30000000-0000-0000-0000-000000000007';
SET morbac.org_id = '10000000-0000-0000-0000-000000000001';
@@ -241,7 +241,7 @@ WHERE user_id = '30000000-0000-0000-0000-000000000011'
AND role_id = '20000000-0001-0000-0000-000000000005'
AND org_id = '10000000-0000-0000-0000-000000000001';
-- Dave (employee) has no rules for user_roles RLS should block
-- Dave (employee) has no rules for user_roles - RLS should block
SET morbac.user_id = '30000000-0000-0000-0000-000000000004';
SET morbac.org_id = '10000000-0000-0000-0000-000000000001';
+11 -11
View File
@@ -3,14 +3,14 @@
-- =============================================================================
-- Tests miscellaneous utility functions and advanced features:
--
-- 1. pending_obligations returns obligation rules for a user
-- 2. pending_recommendations returns recommendation rules for a user
-- 1. pending_obligations - returns obligation rules for a user
-- 2. pending_recommendations - returns recommendation rules for a user
-- 3. Obligations/recommendations do NOT affect is_allowed()
-- 3b. Conflict resolution: prohibition voids obligation; prohibition/obligation voids recommendation
-- 4. user_has_role checks if user holds a named role
-- 5. user_roles_in_org lists all roles for user in org
-- 6. eval_context evaluates context predicates directly
-- 7. Derived roles computed via evaluator function
-- 4. user_has_role - checks if user holds a named role
-- 5. user_roles_in_org - lists all roles for user in org
-- 6. eval_context - evaluates context predicates directly
-- 7. Derived roles - computed via evaluator function
-- 8. RLS helpers: get_user_orgs, current_org_ids, rls_check() org scoping
--
-- Prerequisites: 00_setup.sql -> 08_system_access.sql
@@ -18,7 +18,7 @@
\echo ''
\echo '================================================================'
\echo '09 UTILITIES, DERIVED ROLES, RLS'
\echo '09 - UTILITIES, DERIVED ROLES, RLS'
\echo '================================================================'
-- ---------------------------------------------------------------------------
@@ -139,7 +139,7 @@ SELECT morbac.t('Dave has recommendation for read audit_logs but no permission [
), FALSE);
-- ---------------------------------------------------------------------------
-- Section 3b: Conflict resolution prohibition voids obligation/recommendation
-- Section 3b: Conflict resolution - prohibition voids obligation/recommendation
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 3b. Conflict resolution: prohibition voids obligation and recommendation ---'
@@ -330,13 +330,13 @@ SELECT morbac.t('eval_context(end_of_quarter) = TRUE',
\echo ''
\echo '--- 7. Derived roles ---'
-- Create a derived role: 'senior_employee' dynamically granted to Dave (only)
-- Create a derived role: 'senior_employee' - dynamically granted to Dave (only)
INSERT INTO morbac.roles (id, org_id, name, description)
VALUES (
'20000000-0001-0000-0000-000000000011',
'10000000-0000-0000-0000-000000000001',
'senior_employee',
'Senior employee granted dynamically based on tenure'
'Senior employee - granted dynamically based on tenure'
);
-- Evaluator function: returns TRUE only for Dave at GlobalTech
@@ -379,7 +379,7 @@ SELECT morbac.t('Dave has senior_employee derived role in comprehensive roles',
AND source = 'derived'
), TRUE);
-- Eve (intern) does NOT satisfy the evaluator no derived role
-- Eve (intern) does NOT satisfy the evaluator - no derived role
SELECT morbac.t('Eve has no derived roles in comprehensive roles',
NOT EXISTS(
SELECT 1 FROM morbac.get_comprehensive_roles(
+5 -5
View File
@@ -2,8 +2,8 @@
-- Activity-View Binding Tests
-- =============================================================================
-- Tests opt-in activity-to-view restrictions:
-- 1. No bindings defined any view is allowed
-- 2. Binding defined listed view is allowed, unlisted view is blocked
-- 1. No bindings defined - any view is allowed
-- 2. Binding defined - listed view is allowed, unlisted view is blocked
-- 3. Blocking applies to cross_org_rules as well
-- 4. Removing all bindings lifts the restriction
--
@@ -12,11 +12,11 @@
\echo ''
\echo '================================================================'
\echo '10 ACTIVITY-VIEW BINDINGS'
\echo '10 - ACTIVITY-VIEW BINDINGS'
\echo '================================================================'
-- ---------------------------------------------------------------------------
-- Section 1: No bindings unconstrained
-- Section 1: No bindings - unconstrained
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 1. No bindings: any view is allowed ---'
@@ -39,7 +39,7 @@ SELECT morbac.t('No bindings: audit/documents rule inserted successfully',
DELETE FROM morbac.rules WHERE id = 'b0000000-0000-0000-0000-000000000001';
-- ---------------------------------------------------------------------------
-- Section 2: Binding defined listed view allowed, unlisted view blocked
-- Section 2: Binding defined - listed view allowed, unlisted view blocked
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 2. Binding defined: allowed view works, unlisted view blocked ---'
+13 -13
View File
@@ -3,18 +3,18 @@
-- =============================================================================
-- Tests rules.scope and cross_org_rules.source_org_id = NULL:
--
-- 1. scope='self' (default) exact org only, unchanged behavior
-- 2. scope='subtree' rule at root covers self + Engineering + Sales
-- 3. scope='descendants' covers Engineering + Sales but NOT GlobalTech itself
-- 4. scope='children' covers direct children only
-- 5. New org added after rule creation picked up automatically (cache invalidation)
-- 1. scope='self' (default) - exact org only, unchanged behavior
-- 2. scope='subtree' - rule at root covers self + Engineering + Sales
-- 3. scope='descendants' - covers Engineering + Sales but NOT GlobalTech itself
-- 4. scope='children' - covers direct children only
-- 5. New org added after rule creation - picked up automatically (cache invalidation)
--
-- Prerequisites: 00_setup.sql -> 10_activity_view_bindings.sql
-- =============================================================================
\echo ''
\echo '================================================================'
\echo '11 SCOPE RULES AND GLOBAL CROSS-ORG RULES'
\echo '11 - SCOPE RULES AND GLOBAL CROSS-ORG RULES'
\echo '================================================================'
-- Setup: create a dedicated role for scope tests (avoid polluting existing rules)
@@ -23,7 +23,7 @@ VALUES (
'20000000-0001-0000-0000-000000000012',
'10000000-0000-0000-0000-000000000001',
'analyst',
'Data analyst scope tests'
'Data analyst - scope tests'
);
-- Assign Karl (previously no role) as analyst at GlobalTech
@@ -35,7 +35,7 @@ VALUES (
);
-- ---------------------------------------------------------------------------
-- Section 1: scope='self' (default) exact org only
-- Section 1: scope='self' (default) - exact org only
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 1. scope=self (default) ---'
@@ -67,7 +67,7 @@ SELECT morbac.t('Karl (analyst, scope=self) reads reports in Engineering [denied
DELETE FROM morbac.rules WHERE id = 'c0000000-0000-0000-0000-000000000001';
-- ---------------------------------------------------------------------------
-- Section 2: scope='subtree' root + all descendants
-- Section 2: scope='subtree' - root + all descendants
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 2. scope=subtree ---'
@@ -106,7 +106,7 @@ SELECT morbac.t('Karl (analyst, scope=subtree) reads reports in Sales [allowed]'
DELETE FROM morbac.rules WHERE id = 'c0000000-0000-0000-0000-000000000002';
-- ---------------------------------------------------------------------------
-- Section 3: scope='descendants' children only, NOT self
-- Section 3: scope='descendants' - children only, NOT self
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 3. scope=descendants ---'
@@ -145,7 +145,7 @@ SELECT morbac.t('Karl (analyst, scope=descendants) reads reports in Sales [allow
DELETE FROM morbac.rules WHERE id = 'c0000000-0000-0000-0000-000000000003';
-- ---------------------------------------------------------------------------
-- Section 4: scope='children' direct children only
-- Section 4: scope='children' - direct children only
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 4. scope=children ---'
@@ -193,7 +193,7 @@ DELETE FROM morbac.rules WHERE id = 'c0000000-0000-0000-0000-000000000004';
DELETE FROM morbac.orgs WHERE id = '10000000-0000-0000-0000-000000000004';
-- ---------------------------------------------------------------------------
-- Section 5: New org added after rule creation scope picks it up automatically
-- Section 5: New org added after rule creation - scope picks it up automatically
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 5. Dynamic scope: new org covered automatically ---'
@@ -225,7 +225,7 @@ VALUES (
'10000000-0000-0000-0000-000000000001'
);
-- The scoped rule was defined before Legal Dept existed still covers it
-- The scoped rule was defined before Legal Dept existed - still covers it
SELECT morbac.t('Karl (analyst) reads documents in Legal Dept [new org, covered by subtree scope]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000011'::uuid,
+8 -8
View File
@@ -17,11 +17,11 @@
\echo ''
\echo '================================================================'
\echo '12 USER RULES'
\echo '12 - USER RULES'
\echo '================================================================'
-- ---------------------------------------------------------------------------
-- Section 1: No user rule Karl (no role) is denied
-- Section 1: No user rule - Karl (no role) is denied
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 1. No user rule: access denied ---'
@@ -41,7 +41,7 @@ SELECT morbac.t('Karl (no role) reads GlobalTech financial_data [no user rule]',
), FALSE);
-- ---------------------------------------------------------------------------
-- Section 2: Direct user permission Karl gets access without a role
-- Section 2: Direct user permission - Karl gets access without a role
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 2. Direct user permission ---'
@@ -78,7 +78,7 @@ SELECT morbac.t('Karl reads GlobalTech financial_data [documents rule covers it
'read', 'financial_data'
), TRUE);
-- contracts has no hierarchy relationship documents rule does not cover it
-- contracts has no hierarchy relationship - documents rule does not cover it
SELECT morbac.t('Karl reads GlobalTech contracts [no user rule, no hierarchy coverage]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000011'::uuid,
@@ -141,7 +141,7 @@ SELECT morbac.t('Eve (intern) reads GlobalTech public_data [user prohibition blo
'read', 'public_data'
), FALSE);
-- Dave (employee) is unaffected only Eve has the prohibition
-- Dave (employee) is unaffected - only Eve has the prohibition
SELECT morbac.t('Dave (employee) reads GlobalTech public_data [no user prohibition]',
morbac.is_allowed_nocache(
'30000000-0000-0000-0000-000000000004'::uuid,
@@ -150,7 +150,7 @@ SELECT morbac.t('Dave (employee) reads GlobalTech public_data [no user prohibiti
), TRUE);
-- ---------------------------------------------------------------------------
-- Section 5: Priority higher-priority user permission overrides prohibition
-- Section 5: Priority - higher-priority user permission overrides prohibition
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 5. Priority: user permission overrides user prohibition ---'
@@ -236,7 +236,7 @@ SELECT morbac.t('rls_check passes without target_user_id filter',
'30000000-0000-0000-0000-000000000004'::uuid
), TRUE);
-- Set target_user_id to Dave rows belonging to Dave pass
-- Set target_user_id to Dave - rows belonging to Dave pass
SET morbac.target_user_id = '30000000-0000-0000-0000-000000000004';
SELECT morbac.t('rls_check passes when row user_id matches target_user_id',
@@ -254,7 +254,7 @@ SELECT morbac.t('rls_check blocked when row user_id differs from target_user_id'
'30000000-0000-0000-0000-000000000001'::uuid
), FALSE);
-- No p_row_user_id passed user filter does not apply
-- No p_row_user_id passed - user filter does not apply
SELECT morbac.t('rls_check passes when no row user_id passed (filter skipped)',
morbac.rls_check(
'read', 'documents',
+10 -10
View File
@@ -37,7 +37,7 @@
\echo '================================================================'
-- ---------------------------------------------------------------------------
-- Section 1: No user_id set always FALSE
-- Section 1: No user_id set - always FALSE
-- ---------------------------------------------------------------------------
\echo ''
\echo '--- 1. No user_id: always FALSE ---'
@@ -64,19 +64,19 @@ SELECT morbac.t('rls_check without user_id, global row',
SET morbac.user_id = '30000000-0000-0000-0000-000000000011'; -- Karl
SET morbac.org_id = '10000000-0000-0000-0000-000000000001'; -- GlobalTech HQ
-- 2a: org-scoped row, matching org Karl has user_rule for read documents
-- 2a: org-scoped row, matching org - Karl has user_rule for read documents
SELECT morbac.t('rls_check single org, org row matches session org (Karl/documents)',
morbac.rls_check('read', 'documents',
'10000000-0000-0000-0000-000000000001'::uuid),
TRUE);
-- 2b: org-scoped row, different org blocked before is_allowed
-- 2b: org-scoped row, different org - blocked before is_allowed
SELECT morbac.t('rls_check single org, org row from different org (blocked)',
morbac.rls_check('read', 'documents',
'10000000-0000-0000-0000-000000000002'::uuid),
FALSE);
-- 2c: NULL row filtered out under a single org pin (orphan not requested)
-- 2c: NULL row - filtered out under a single org pin (orphan not requested)
SELECT morbac.t('rls_check single org, NULL row filtered out under org pin',
morbac.rls_check('read', 'documents', NULL),
FALSE);
@@ -98,19 +98,19 @@ RESET morbac.org_id;
SET morbac.user_id = '30000000-0000-0000-0000-000000000011'; -- Karl
SET morbac.org_ids = '["10000000-0000-0000-0000-000000000001"]'; -- [GlobalTech HQ]
-- 3a: org-scoped row in the list Karl has user_rule for read documents in GlobalTech
-- 3a: org-scoped row in the list - Karl has user_rule for read documents in GlobalTech
SELECT morbac.t('rls_check org_ids, org row in list (Karl/documents/GlobalTech)',
morbac.rls_check('read', 'documents',
'10000000-0000-0000-0000-000000000001'::uuid),
TRUE);
-- 3b: org-scoped row not in the list blocked
-- 3b: org-scoped row not in the list - blocked
SELECT morbac.t('rls_check org_ids, org row not in list (blocked)',
morbac.rls_check('read', 'documents',
'10000000-0000-0000-0000-000000000002'::uuid),
FALSE);
-- 3c: NULL row, list WITHOUT null marker filtered out even with a global grant
-- 3c: NULL row, list WITHOUT null marker - filtered out even with a global grant
INSERT INTO morbac.global_rules (user_id, activity, view, context_id, modality)
VALUES (
'30000000-0000-0000-0000-000000000011', -- Karl
@@ -123,7 +123,7 @@ SELECT morbac.t('rls_check org_ids without null marker, NULL row filtered out de
morbac.rls_check('read', 'contracts', NULL),
FALSE);
-- 3c2: NULL row, list WITH null marker + global permission allowed
-- 3c2: NULL row, list WITH null marker + global permission - allowed
SET morbac.org_ids = '["10000000-0000-0000-0000-000000000001", null]';
SELECT morbac.t('rls_check org_ids with null marker, NULL row + global permission',
@@ -167,7 +167,7 @@ RESET morbac.org_ids;
SET morbac.user_id = '30000000-0000-0000-0000-000000000011'; -- Karl
-- 4a: org-scoped row uses row's org_id (Karl has user_rule in GlobalTech)
-- 4a: org-scoped row - uses row's org_id (Karl has user_rule in GlobalTech)
SELECT morbac.t('rls_check no org context, org row: uses row org (Karl/documents/GlobalTech)',
morbac.rls_check('read', 'documents',
'10000000-0000-0000-0000-000000000001'::uuid),
@@ -179,7 +179,7 @@ SELECT morbac.t('rls_check no org context, org row: no permission in row org (En
'10000000-0000-0000-0000-000000000002'::uuid),
FALSE);
-- 4b: global row + global permission — now goes to is_allowed(Karl, NULL, ...) global_rules only
-- 4b: NULL row + global permission - is_allowed(Karl, NULL, ...) sees unattributed + global rules
INSERT INTO morbac.global_rules (user_id, activity, view, context_id, modality)
VALUES (
'30000000-0000-0000-0000-000000000011', -- Karl