feat(system): introduce global rules and system principals for easier system account handling
This commit is contained in:
@@ -43,8 +43,8 @@ install: build
|
|||||||
@cp $(OUTPUT_DEV_FILENAME) $(OUTPUT_RELEASE_FILENAME)
|
@cp $(OUTPUT_DEV_FILENAME) $(OUTPUT_RELEASE_FILENAME)
|
||||||
@./tools/install.sh $(PROJECT_FILENAME) $(PROJECT_VERSION)
|
@./tools/install.sh $(PROJECT_FILENAME) $(PROJECT_VERSION)
|
||||||
|
|
||||||
.PHONY: check
|
.PHONY: test
|
||||||
check: install
|
test: install
|
||||||
@echo "Running tests..."
|
@echo "Running tests..."
|
||||||
@dropdb morbac_test 2>/dev/null || true
|
@dropdb morbac_test 2>/dev/null || true
|
||||||
@createdb morbac_test
|
@createdb morbac_test
|
||||||
@@ -89,8 +89,8 @@ docker-uninstall:
|
|||||||
@./tools/docker_uninstall.sh $(DOCKER_CONTAINER) $(PROJECT_FILENAME)
|
@./tools/docker_uninstall.sh $(DOCKER_CONTAINER) $(PROJECT_FILENAME)
|
||||||
|
|
||||||
# Build and run tests in Docker container
|
# Build and run tests in Docker container
|
||||||
.PHONY: docker-check
|
.PHONY: docker-test
|
||||||
docker-check: docker-install
|
docker-test: docker-install
|
||||||
@echo "Running tests in Docker..."
|
@echo "Running tests in Docker..."
|
||||||
@./tools/docker_test.sh $(DOCKER_CONTAINER) morbac_test postgres
|
@./tools/docker_test.sh $(DOCKER_CONTAINER) morbac_test postgres
|
||||||
|
|
||||||
|
|||||||
+125
-6
@@ -39,10 +39,13 @@ flowchart TD
|
|||||||
D --> G
|
D --> G
|
||||||
E --> G
|
E --> G
|
||||||
F --> G
|
F --> G
|
||||||
G --> H[Find Highest-Priority Prohibition]
|
G --> SP{System Principal?}
|
||||||
G --> I[Find Highest-Priority Permission]
|
SP -->|Yes| I[Find Highest-Priority Permission]
|
||||||
H --> J{Compare Priorities}
|
SP -->|No| H[Find Highest-Priority Prohibition]
|
||||||
I --> J
|
H --> H2[+ Global Prohibitions step 3.5]
|
||||||
|
I --> I2[+ Global Permissions step 6.5]
|
||||||
|
H2 --> J{Compare Priorities}
|
||||||
|
I2 --> J
|
||||||
J -->|Permission priority > Prohibition priority| K[ALLOW]
|
J -->|Permission priority > Prohibition priority| K[ALLOW]
|
||||||
J -->|Prohibition exists, no higher-priority permission| L[DENY]
|
J -->|Prohibition exists, no higher-priority permission| L[DENY]
|
||||||
J -->|No prohibition, permission found| K
|
J -->|No prohibition, permission found| K
|
||||||
@@ -88,6 +91,11 @@ erDiagram
|
|||||||
views ||--o{ activity_view_bindings : "allowed_for"
|
views ||--o{ activity_view_bindings : "allowed_for"
|
||||||
|
|
||||||
contexts ||--o{ rules : "condition"
|
contexts ||--o{ rules : "condition"
|
||||||
|
contexts ||--o{ global_rules : "condition"
|
||||||
|
|
||||||
|
activities ||--o{ global_rules : "action"
|
||||||
|
views ||--o{ global_rules : "target"
|
||||||
|
system_principals ||--o{ global_rules : "ruleset"
|
||||||
|
|
||||||
roles ||--o{ delegations : "delegated"
|
roles ||--o{ delegations : "delegated"
|
||||||
roles ||--o{ negative_role_assignments : "prohibited"
|
roles ||--o{ negative_role_assignments : "prohibited"
|
||||||
@@ -188,6 +196,30 @@ Inter-organizational access rules.
|
|||||||
|
|
||||||
**Behavior:** Allows roles in a source organization to access resources in a target organization.
|
**Behavior:** Allows roles in a source organization to access resources in a target organization.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
**Key columns:**
|
||||||
|
- `user_id`: UUID of the service account
|
||||||
|
- `description`: Human-readable label
|
||||||
|
|
||||||
|
**Behavior:** System principals bypass all prohibition evaluation. Only the DB owner can insert or remove entries (no RLS write policies).
|
||||||
|
|
||||||
|
**morbac.global_rules**
|
||||||
|
|
||||||
|
System-wide rules with no org or role binding.
|
||||||
|
|
||||||
|
**Key columns:**
|
||||||
|
- `user_id`: NULL = all users; non-NULL = specific user
|
||||||
|
- `activity`: NULL = any activity; non-NULL = specific activity (hierarchy applies)
|
||||||
|
- `view`: NULL = any view; non-NULL = specific view (hierarchy applies)
|
||||||
|
- `context_id`: Contextual condition
|
||||||
|
- `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.
|
||||||
|
|
||||||
**morbac.activity_view_bindings**
|
**morbac.activity_view_bindings**
|
||||||
|
|
||||||
Optional whitelist of valid (activity, view) pairs for rule creation.
|
Optional whitelist of valid (activity, view) pairs for rule creation.
|
||||||
@@ -556,6 +588,93 @@ SELECT morbac.is_allowed(auditor_id, subsidiary_id, 'read', 'financials'); -- TR
|
|||||||
| Role in org A covers org A's descendants | `rules.scope = 'subtree'` (or other scope) |
|
| Role in org A covers org A's descendants | `rules.scope = 'subtree'` (or other scope) |
|
||||||
| Role in org A accesses a different org B | `cross_org_rules` with `source_org_id = A` |
|
| Role in org A accesses a different org B | `cross_org_rules` with `source_org_id = A` |
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
**Table:** `morbac.global_rules`
|
||||||
|
|
||||||
|
| Column | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `user_id` | NULL = all users; UUID = specific user |
|
||||||
|
| `activity` | NULL = any activity; specific name = that activity (hierarchy applies) |
|
||||||
|
| `view` | NULL = any view; specific name = that view (hierarchy applies) |
|
||||||
|
| `modality` | `permission` or `prohibition` |
|
||||||
|
| `priority` | Optional; same semantics as `morbac.rules` |
|
||||||
|
|
||||||
|
**Evaluation order:** Global prohibitions are evaluated at step 3.5 (after local, cross-org, and user-level prohibitions). Global permissions at step 6.5 (after all other permission sources). Both contribute to the same priority accumulators used in step 7 resolution.
|
||||||
|
|
||||||
|
**Use case: system account that reads all resources**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO morbac.global_rules (user_id, activity, view, context_id, modality)
|
||||||
|
VALUES (
|
||||||
|
:system_account_uuid,
|
||||||
|
'read', NULL,
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||||
|
'permission'
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
`view = NULL` matches every view. Use `activity = NULL` too if the account needs all actions.
|
||||||
|
|
||||||
|
**Use case: protect service accounts from modification by anyone**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO morbac.views (name) VALUES ('service_accounts');
|
||||||
|
|
||||||
|
INSERT INTO morbac.global_rules (user_id, activity, view, context_id, modality, priority)
|
||||||
|
SELECT NULL, a.act, 'service_accounts',
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||||
|
'prohibition', 100
|
||||||
|
FROM (VALUES ('delete'), ('update')) AS a(act);
|
||||||
|
```
|
||||||
|
|
||||||
|
Priority 100 ensures this prohibition overrides any role-based permission. To exempt a specific user, add a subject-specific permission with higher priority via `morbac.user_rules`.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
**Table:** `morbac.system_principals`
|
||||||
|
|
||||||
|
| Column | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `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):**
|
||||||
|
|
||||||
|
| Table | Blocked operations |
|
||||||
|
|---|---|
|
||||||
|
| `user_roles` | INSERT, UPDATE, DELETE |
|
||||||
|
| `user_rules` | INSERT, UPDATE, DELETE |
|
||||||
|
| `negative_role_assignments` | INSERT, UPDATE, DELETE |
|
||||||
|
| `delegations` | INSERT, UPDATE involving the principal |
|
||||||
|
| `global_rules` | All operations where `user_id` matches a system principal |
|
||||||
|
|
||||||
|
**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:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Register the service account (DB owner only)
|
||||||
|
INSERT INTO morbac.system_principals (user_id, description)
|
||||||
|
VALUES (:service_uuid, 'Backend API worker');
|
||||||
|
|
||||||
|
-- Grant full access (immutable after insert)
|
||||||
|
INSERT INTO morbac.global_rules (user_id, activity, view, context_id, modality)
|
||||||
|
VALUES (:service_uuid, NULL, NULL,
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'always'), 'permission');
|
||||||
|
|
||||||
|
-- Or restrict to specific operations
|
||||||
|
INSERT INTO morbac.global_rules (user_id, activity, view, context_id, modality)
|
||||||
|
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.
|
||||||
|
|
||||||
### Administration
|
### 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.
|
||||||
@@ -568,7 +687,7 @@ The database owner (superuser) bypasses RLS by default — use that privilege on
|
|||||||
|
|
||||||
**System view names**
|
**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`) at install time.
|
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:
|
||||||
|
|
||||||
@@ -732,7 +851,7 @@ WHERE table_name = 'rules'
|
|||||||
|
|
||||||
### Authorization Functions
|
### Authorization Functions
|
||||||
|
|
||||||
**`is_allowed(user_id, org_id, activity, view)`**: Main authorization decision. Returns BOOLEAN. Checks prohibitions first, then permissions, defaults to deny.
|
**`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.
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
SELECT morbac.is_allowed(user_uuid, org_uuid, 'read', 'documents');
|
SELECT morbac.is_allowed(user_uuid, org_uuid, 'read', 'documents');
|
||||||
|
|||||||
@@ -125,6 +125,41 @@ CREATE TRIGGER trg_invalidate_cache_user_rules
|
|||||||
AFTER INSERT OR UPDATE OR DELETE ON morbac.user_rules
|
AFTER INSERT OR UPDATE OR DELETE ON morbac.user_rules
|
||||||
FOR EACH ROW EXECUTE FUNCTION morbac.invalidate_cache_on_user_rule_change();
|
FOR EACH ROW EXECUTE FUNCTION morbac.invalidate_cache_on_user_rule_change();
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
DELETE FROM morbac.auth_cache;
|
||||||
|
RETURN COALESCE(NEW, OLD);
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
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
|
||||||
|
CREATE OR REPLACE FUNCTION morbac.invalidate_cache_on_system_principal_change()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_user_id UUID;
|
||||||
|
BEGIN
|
||||||
|
v_user_id := CASE WHEN TG_OP = 'DELETE' THEN OLD.user_id ELSE NEW.user_id END;
|
||||||
|
DELETE FROM morbac.auth_cache WHERE user_id = v_user_id;
|
||||||
|
RETURN COALESCE(NEW, OLD);
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_invalidate_cache_system_principals
|
||||||
|
AFTER INSERT OR UPDATE OR DELETE ON morbac.system_principals
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION morbac.invalidate_cache_on_system_principal_change();
|
||||||
|
|
||||||
-- Refresh materialized hierarchy views when hierarchies change
|
-- Refresh materialized hierarchy views when hierarchies change
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION morbac.refresh_on_hierarchy_change()
|
CREATE OR REPLACE FUNCTION morbac.refresh_on_hierarchy_change()
|
||||||
|
|||||||
@@ -32,7 +32,14 @@ DECLARE
|
|||||||
v_rule RECORD;
|
v_rule RECORD;
|
||||||
v_max_prohibition_priority INTEGER := NULL;
|
v_max_prohibition_priority INTEGER := NULL;
|
||||||
v_max_permission_priority INTEGER := NULL;
|
v_max_permission_priority INTEGER := NULL;
|
||||||
|
v_is_system_principal BOOLEAN := FALSE;
|
||||||
BEGIN
|
BEGIN
|
||||||
|
SELECT TRUE INTO v_is_system_principal
|
||||||
|
FROM morbac.system_principals WHERE user_id = p_user_id;
|
||||||
|
|
||||||
|
-- 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
|
FOR v_rule IN
|
||||||
SELECT r.context_id, COALESCE(r.priority, 0) AS prio
|
SELECT r.context_id, COALESCE(r.priority, 0) AS prio
|
||||||
@@ -91,6 +98,27 @@ BEGIN
|
|||||||
END IF;
|
END IF;
|
||||||
END LOOP;
|
END LOOP;
|
||||||
|
|
||||||
|
-- STEP 3.5: Global prohibitions
|
||||||
|
FOR v_rule IN
|
||||||
|
SELECT gr.context_id, COALESCE(gr.priority, 0) AS prio
|
||||||
|
FROM morbac.global_rules gr
|
||||||
|
WHERE (gr.user_id IS NULL OR gr.user_id = p_user_id)
|
||||||
|
AND gr.modality = 'prohibition'
|
||||||
|
AND (gr.activity IS NULL OR gr.activity IN (SELECT activity FROM morbac.get_effective_activities(p_activity)))
|
||||||
|
AND (gr.view IS NULL OR gr.view IN (SELECT view FROM morbac.get_effective_views(p_view)))
|
||||||
|
AND morbac.is_rule_valid(gr.valid_from, gr.valid_until)
|
||||||
|
ORDER BY COALESCE(gr.priority, 0) DESC
|
||||||
|
LOOP
|
||||||
|
IF morbac.eval_context(v_rule.context_id) THEN
|
||||||
|
IF v_max_prohibition_priority IS NULL OR v_rule.prio > v_max_prohibition_priority THEN
|
||||||
|
v_max_prohibition_priority := v_rule.prio;
|
||||||
|
END IF;
|
||||||
|
EXIT;
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
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
|
FOR v_rule IN
|
||||||
SELECT r.context_id, COALESCE(r.priority, 0) AS prio
|
SELECT r.context_id, COALESCE(r.priority, 0) AS prio
|
||||||
@@ -149,6 +177,25 @@ BEGIN
|
|||||||
END IF;
|
END IF;
|
||||||
END LOOP;
|
END LOOP;
|
||||||
|
|
||||||
|
-- STEP 6.5: Global permissions
|
||||||
|
FOR v_rule IN
|
||||||
|
SELECT gr.context_id, COALESCE(gr.priority, 0) AS prio
|
||||||
|
FROM morbac.global_rules gr
|
||||||
|
WHERE (gr.user_id IS NULL OR gr.user_id = p_user_id)
|
||||||
|
AND gr.modality = 'permission'
|
||||||
|
AND (gr.activity IS NULL OR gr.activity IN (SELECT activity FROM morbac.get_effective_activities(p_activity)))
|
||||||
|
AND (gr.view IS NULL OR gr.view IN (SELECT view FROM morbac.get_effective_views(p_view)))
|
||||||
|
AND morbac.is_rule_valid(gr.valid_from, gr.valid_until)
|
||||||
|
ORDER BY COALESCE(gr.priority, 0) DESC
|
||||||
|
LOOP
|
||||||
|
IF morbac.eval_context(v_rule.context_id) THEN
|
||||||
|
IF v_max_permission_priority IS NULL OR v_rule.prio > v_max_permission_priority THEN
|
||||||
|
v_max_permission_priority := v_rule.prio;
|
||||||
|
END IF;
|
||||||
|
EXIT;
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
-- STEP 7: Priority resolution
|
-- STEP 7: Priority resolution
|
||||||
IF v_max_prohibition_priority IS NULL THEN
|
IF v_max_prohibition_priority IS NULL THEN
|
||||||
RETURN v_max_permission_priority IS NOT NULL;
|
RETURN v_max_permission_priority IS NOT NULL;
|
||||||
|
|||||||
+3
-1
@@ -23,7 +23,9 @@ INSERT INTO morbac.config (key, value, description) VALUES
|
|||||||
('system_view.views', 'views', 'View name for morbac.views table access control'),
|
('system_view.views', 'views', 'View name for morbac.views table access control'),
|
||||||
('system_view.delegations', 'delegations', 'View name for morbac.delegations table access control'),
|
('system_view.delegations', 'delegations', 'View name for morbac.delegations table access control'),
|
||||||
('system_view.cross_org_rules', 'cross_org_rules', 'View name for morbac.cross_org_rules table access control'),
|
('system_view.cross_org_rules', 'cross_org_rules', 'View name for morbac.cross_org_rules table access control'),
|
||||||
('system_view.user_rules', 'user_rules', 'View name for morbac.user_rules table access control');
|
('system_view.user_rules', 'user_rules', 'View name for morbac.user_rules table access control'),
|
||||||
|
('system_view.global_rules', 'global_rules', 'View name for morbac.global_rules table access control'),
|
||||||
|
('system_view.system_principals', 'system_principals', 'View name for morbac.system_principals table access control');
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION morbac.get_config(p_key TEXT)
|
CREATE OR REPLACE FUNCTION morbac.get_config(p_key TEXT)
|
||||||
RETURNS TEXT
|
RETURNS TEXT
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
-- Global rules: Rule(user_id, activity, view, context, modality)
|
||||||
|
--
|
||||||
|
-- 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.
|
||||||
|
--
|
||||||
|
-- Evaluated at steps 3.5 (prohibitions) and 6.5 (permissions) in is_allowed_nocache().
|
||||||
|
-- When activity/view are set, hierarchy resolution applies normally.
|
||||||
|
|
||||||
|
CREATE TABLE morbac.global_rules (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID,
|
||||||
|
activity TEXT REFERENCES morbac.activities(name) ON DELETE CASCADE,
|
||||||
|
view TEXT REFERENCES morbac.views(name) ON DELETE CASCADE,
|
||||||
|
context_id UUID NOT NULL REFERENCES morbac.contexts(id) ON DELETE CASCADE,
|
||||||
|
modality morbac.modality NOT NULL,
|
||||||
|
priority INTEGER,
|
||||||
|
valid_from TIMESTAMPTZ,
|
||||||
|
valid_until TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
metadata JSONB DEFAULT '{}'::jsonb,
|
||||||
|
CHECK (valid_until IS NULL OR valid_from IS NULL OR valid_until > valid_from),
|
||||||
|
UNIQUE(user_id, activity, view, context_id, modality)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_global_rules_user_id ON morbac.global_rules(user_id);
|
||||||
|
CREATE INDEX idx_global_rules_activity_view ON morbac.global_rules(activity, view);
|
||||||
|
CREATE INDEX idx_global_rules_modality ON morbac.global_rules(modality);
|
||||||
|
CREATE INDEX idx_global_rules_fast_lookup ON morbac.global_rules(modality)
|
||||||
|
INCLUDE (user_id, activity, view, context_id)
|
||||||
|
WHERE is_active = true;
|
||||||
|
|
||||||
|
COMMENT ON TABLE morbac.global_rules IS
|
||||||
|
'System-wide rules with no org or role binding. user_id NULL = all users; activity/view NULL = any activity/view.';
|
||||||
|
COMMENT ON COLUMN morbac.global_rules.user_id IS
|
||||||
|
'NULL = all users; non-NULL = this specific user only';
|
||||||
|
COMMENT ON COLUMN morbac.global_rules.activity IS
|
||||||
|
'NULL = any activity; non-NULL = specific activity (hierarchy applies)';
|
||||||
|
COMMENT ON COLUMN morbac.global_rules.view IS
|
||||||
|
'NULL = any view; non-NULL = specific view (hierarchy applies)';
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION morbac.global_rules_set_is_active()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.is_active := (
|
||||||
|
(NEW.valid_from IS NULL OR NEW.valid_from <= CURRENT_TIMESTAMP)
|
||||||
|
AND (NEW.valid_until IS NULL OR NEW.valid_until > CURRENT_TIMESTAMP)
|
||||||
|
);
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS trg_global_rules_set_is_active ON morbac.global_rules;
|
||||||
|
CREATE TRIGGER trg_global_rules_set_is_active
|
||||||
|
BEFORE INSERT OR UPDATE ON morbac.global_rules
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION morbac.global_rules_set_is_active();
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
-- System principals: backend service accounts defined at deploy time.
|
||||||
|
--
|
||||||
|
-- Registered user_ids are fully protected at the trigger level:
|
||||||
|
-- - No role assignments, revocations, or negative assignments
|
||||||
|
-- - No user_rules (permissions or prohibitions)
|
||||||
|
-- - No delegations involving them
|
||||||
|
-- - 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
|
||||||
|
-- can register or remove system principals (done in SQL at deploy time).
|
||||||
|
-- SELECT is gated by is_allowed() like all other system tables.
|
||||||
|
|
||||||
|
CREATE TABLE morbac.system_principals (
|
||||||
|
user_id UUID PRIMARY KEY,
|
||||||
|
description TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE morbac.system_principals IS
|
||||||
|
'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';
|
||||||
|
|
||||||
|
-- Shared guard for tables with a single user_id column
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION morbac.raise_if_system_principal()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_user_id UUID;
|
||||||
|
BEGIN
|
||||||
|
v_user_id := CASE WHEN TG_OP = 'DELETE' THEN OLD.user_id ELSE NEW.user_id END;
|
||||||
|
IF EXISTS (SELECT 1 FROM morbac.system_principals WHERE user_id = v_user_id) THEN
|
||||||
|
RAISE EXCEPTION 'operation blocked: % is a system principal', v_user_id;
|
||||||
|
END IF;
|
||||||
|
RETURN COALESCE(NEW, OLD);
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_system_principal_user_roles
|
||||||
|
BEFORE INSERT OR UPDATE OR DELETE ON morbac.user_roles
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION morbac.raise_if_system_principal();
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_system_principal_user_rules
|
||||||
|
BEFORE INSERT OR UPDATE OR DELETE ON morbac.user_rules
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION morbac.raise_if_system_principal();
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_system_principal_negative_roles
|
||||||
|
BEFORE INSERT OR UPDATE OR DELETE ON morbac.negative_role_assignments
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION morbac.raise_if_system_principal();
|
||||||
|
|
||||||
|
-- Delegations have two user_id columns
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION morbac.raise_if_system_principal_delegation()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM morbac.system_principals
|
||||||
|
WHERE user_id IN (NEW.delegator_id, NEW.delegatee_id)
|
||||||
|
) THEN
|
||||||
|
RAISE EXCEPTION 'operation blocked: delegations cannot involve system principals';
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_system_principal_delegations
|
||||||
|
BEFORE INSERT OR UPDATE ON morbac.delegations
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION morbac.raise_if_system_principal_delegation();
|
||||||
|
|
||||||
|
-- Global rules: all operations on rows belonging to a system principal are blocked.
|
||||||
|
-- This protects both the permission rules defined for them and prevents prohibitions
|
||||||
|
-- from being added against them. Define their rules at deploy time as the DB owner.
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION morbac.raise_if_system_principal_global_rule()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_user_id UUID;
|
||||||
|
BEGIN
|
||||||
|
v_user_id := CASE WHEN TG_OP = 'DELETE' THEN OLD.user_id ELSE NEW.user_id END;
|
||||||
|
IF v_user_id IS NOT NULL
|
||||||
|
AND EXISTS (SELECT 1 FROM morbac.system_principals WHERE user_id = v_user_id)
|
||||||
|
THEN
|
||||||
|
RAISE EXCEPTION 'operation blocked: global rules for system principal % are immutable', v_user_id;
|
||||||
|
END IF;
|
||||||
|
RETURN COALESCE(NEW, OLD);
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_system_principal_global_rules
|
||||||
|
BEFORE INSERT OR UPDATE OR DELETE ON morbac.global_rules
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION morbac.raise_if_system_principal_global_rule();
|
||||||
@@ -180,6 +180,32 @@ CREATE POLICY user_rules_delete ON morbac.user_rules FOR DELETE
|
|||||||
USING (morbac.is_allowed(morbac.current_user_id(), org_id, 'delete',
|
USING (morbac.is_allowed(morbac.current_user_id(), org_id, 'delete',
|
||||||
morbac.get_config('system_view.user_rules')));
|
morbac.get_config('system_view.user_rules')));
|
||||||
|
|
||||||
|
-- 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)
|
||||||
|
ALTER TABLE morbac.global_rules ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
CREATE POLICY global_rules_select ON morbac.global_rules FOR SELECT
|
||||||
|
USING (morbac.is_allowed(morbac.current_user_id(), morbac.current_org_id(), 'read',
|
||||||
|
morbac.get_config('system_view.global_rules')));
|
||||||
|
|
||||||
|
CREATE POLICY global_rules_insert ON morbac.global_rules FOR INSERT
|
||||||
|
WITH CHECK (morbac.is_allowed(morbac.current_user_id(), morbac.current_org_id(), 'create',
|
||||||
|
morbac.get_config('system_view.global_rules')));
|
||||||
|
|
||||||
|
CREATE POLICY global_rules_update ON morbac.global_rules FOR UPDATE
|
||||||
|
USING (morbac.is_allowed(morbac.current_user_id(), morbac.current_org_id(), 'update',
|
||||||
|
morbac.get_config('system_view.global_rules')));
|
||||||
|
|
||||||
|
CREATE POLICY global_rules_delete ON morbac.global_rules FOR DELETE
|
||||||
|
USING (morbac.is_allowed(morbac.current_user_id(), morbac.current_org_id(), 'delete',
|
||||||
|
morbac.get_config('system_view.global_rules')));
|
||||||
|
|
||||||
-- morbac.cross_org_rules
|
-- morbac.cross_org_rules
|
||||||
ALTER TABLE morbac.cross_org_rules ENABLE ROW LEVEL SECURITY;
|
ALTER TABLE morbac.cross_org_rules ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,384 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- Global Rules Tests
|
||||||
|
-- =============================================================================
|
||||||
|
-- Tests system-wide rules via morbac.global_rules.
|
||||||
|
--
|
||||||
|
-- Scenarios:
|
||||||
|
-- 1. No global rule: Karl (no role) is denied by default
|
||||||
|
-- 2. Global permission (user_id=NULL): all users gain access
|
||||||
|
-- 3. Global permission (user_id=uuid): only that user gains access
|
||||||
|
-- 4. Global prohibition (user_id=NULL): all users are denied regardless of role
|
||||||
|
-- 5. Global prohibition (user_id=uuid): only that user is denied
|
||||||
|
-- 6. Priority: global permission with higher priority overrides global prohibition
|
||||||
|
-- 7. Priority: high-priority global prohibition overrides role-based permission
|
||||||
|
-- 8. NULL activity/view wildcards (no hierarchy needed)
|
||||||
|
-- 9. Activity/view hierarchy applies when activity/view are set
|
||||||
|
-- 10. Temporal global rules (valid_from / valid_until)
|
||||||
|
--
|
||||||
|
-- Prerequisites: 00_setup.sql -> 12_user_rules.sql
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '================================================================'
|
||||||
|
\echo '13 -- GLOBAL RULES'
|
||||||
|
\echo '================================================================'
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Section 1: No global rule -- Karl (no role) is denied
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
\echo ''
|
||||||
|
\echo '--- 1. No global rule: access denied ---'
|
||||||
|
|
||||||
|
SELECT morbac.t('Karl (no role) reads GlobalTech contracts [no global rule]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'30000000-0000-0000-0000-000000000011'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'read', 'contracts'
|
||||||
|
), FALSE);
|
||||||
|
|
||||||
|
SELECT morbac.t('Karl (no role) reads Engineering contracts [no global rule]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'30000000-0000-0000-0000-000000000011'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000002'::uuid,
|
||||||
|
'read', 'contracts'
|
||||||
|
), FALSE);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Section 2: Global permission (user_id=NULL) -- all users gain access
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
\echo ''
|
||||||
|
\echo '--- 2. Global permission, user_id=NULL: all users ---'
|
||||||
|
|
||||||
|
INSERT INTO morbac.global_rules (user_id, activity, view, context_id, modality)
|
||||||
|
VALUES (
|
||||||
|
NULL,
|
||||||
|
'read', 'contracts',
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||||
|
'permission'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Karl (no role) can now read contracts in any org
|
||||||
|
SELECT morbac.t('Karl reads GlobalTech contracts [global permission, any user]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'30000000-0000-0000-0000-000000000011'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'read', 'contracts'
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
SELECT morbac.t('Karl reads Engineering contracts [global permission covers all orgs]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'30000000-0000-0000-0000-000000000011'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000002'::uuid,
|
||||||
|
'read', 'contracts'
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
-- Dave (employee) also benefits
|
||||||
|
SELECT morbac.t('Dave reads GlobalTech contracts [global permission, any user]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'read', 'contracts'
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
DELETE FROM morbac.global_rules WHERE user_id IS NULL AND activity = 'read' AND view = 'contracts';
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Section 3: Global permission (user_id=uuid) -- specific user only
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
\echo ''
|
||||||
|
\echo '--- 3. Global permission, user_id=Karl only ---'
|
||||||
|
|
||||||
|
INSERT INTO morbac.global_rules (user_id, activity, view, context_id, modality)
|
||||||
|
VALUES (
|
||||||
|
'30000000-0000-0000-0000-000000000011', -- Karl
|
||||||
|
'read', 'contracts',
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||||
|
'permission'
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT morbac.t('Karl reads GlobalTech contracts [user_id-specific global permission]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'30000000-0000-0000-0000-000000000011'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'read', 'contracts'
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
-- Dave has no role permission on contracts and no global rule for him
|
||||||
|
SELECT morbac.t('Dave reads GlobalTech contracts [user_id-specific rule does not cover Dave]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'read', 'contracts'
|
||||||
|
), FALSE);
|
||||||
|
|
||||||
|
DELETE FROM morbac.global_rules
|
||||||
|
WHERE user_id = '30000000-0000-0000-0000-000000000011'
|
||||||
|
AND activity = 'read' AND view = 'contracts';
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Section 4: Global prohibition (user_id=NULL) -- all users denied regardless of role
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
\echo ''
|
||||||
|
\echo '--- 4. Global prohibition, user_id=NULL: all users denied ---'
|
||||||
|
|
||||||
|
-- Dave (employee) has a role-based permission on public_data from setup
|
||||||
|
SELECT morbac.t('Dave reads GlobalTech public_data [role permission before global prohibition]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'read', 'public_data'
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
INSERT INTO morbac.global_rules (user_id, activity, view, context_id, modality)
|
||||||
|
VALUES (
|
||||||
|
NULL,
|
||||||
|
'read', 'public_data',
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||||
|
'prohibition'
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT morbac.t('Dave reads GlobalTech public_data [global prohibition blocks role permission]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'read', 'public_data'
|
||||||
|
), FALSE);
|
||||||
|
|
||||||
|
-- Karl (no role) is also denied
|
||||||
|
SELECT morbac.t('Karl reads GlobalTech public_data [global prohibition, no role]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'30000000-0000-0000-0000-000000000011'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'read', 'public_data'
|
||||||
|
), FALSE);
|
||||||
|
|
||||||
|
-- Alice (CEO, highest role) is also denied
|
||||||
|
SELECT morbac.t('Alice reads GlobalTech public_data [global prohibition overrides CEO role]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'30000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'read', 'public_data'
|
||||||
|
), FALSE);
|
||||||
|
|
||||||
|
DELETE FROM morbac.global_rules WHERE user_id IS NULL AND activity = 'read' AND view = 'public_data';
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Section 5: Global prohibition (user_id=uuid) -- specific user denied only
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
\echo ''
|
||||||
|
\echo '--- 5. Global prohibition, user_id=Dave only ---'
|
||||||
|
|
||||||
|
INSERT INTO morbac.global_rules (user_id, activity, view, context_id, modality)
|
||||||
|
VALUES (
|
||||||
|
'30000000-0000-0000-0000-000000000004', -- Dave
|
||||||
|
'read', 'public_data',
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||||
|
'prohibition'
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT morbac.t('Dave reads GlobalTech public_data [user_id-specific global prohibition]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'read', 'public_data'
|
||||||
|
), FALSE);
|
||||||
|
|
||||||
|
-- Carol (manager) is unaffected
|
||||||
|
SELECT morbac.t('Carol reads GlobalTech public_data [global prohibition does not affect Carol]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'30000000-0000-0000-0000-000000000003'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'read', 'public_data'
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
DELETE FROM morbac.global_rules
|
||||||
|
WHERE user_id = '30000000-0000-0000-0000-000000000004'
|
||||||
|
AND activity = 'read' AND view = 'public_data';
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Section 6: Priority -- global permission overrides global prohibition
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
\echo ''
|
||||||
|
\echo '--- 6. Priority: global permission > global prohibition ---'
|
||||||
|
|
||||||
|
INSERT INTO morbac.global_rules (user_id, activity, view, context_id, modality, priority)
|
||||||
|
VALUES (
|
||||||
|
NULL,
|
||||||
|
'read', 'public_data',
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||||
|
'prohibition', 5
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO morbac.global_rules (user_id, activity, view, context_id, modality, priority)
|
||||||
|
VALUES (
|
||||||
|
'30000000-0000-0000-0000-000000000004', -- Dave
|
||||||
|
'read', 'public_data',
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||||
|
'permission', 10
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT morbac.t('Dave reads GlobalTech public_data [priority-10 global permission beats priority-5 global prohibition]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'read', 'public_data'
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
-- Karl has no user_id-specific permission -- global prohibition still blocks him
|
||||||
|
SELECT morbac.t('Karl reads GlobalTech public_data [global prohibition still blocks non-exempted user]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'30000000-0000-0000-0000-000000000011'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'read', 'public_data'
|
||||||
|
), FALSE);
|
||||||
|
|
||||||
|
DELETE FROM morbac.global_rules WHERE activity = 'read' AND view = 'public_data';
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Section 7: Priority -- high-priority global prohibition overrides role permission
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
\echo ''
|
||||||
|
\echo '--- 7. Priority: global prohibition > role permission ---'
|
||||||
|
|
||||||
|
-- Dave has role-based read on public_data (priority 0 by default)
|
||||||
|
SELECT morbac.t('Dave reads GlobalTech public_data [role permission, no global rule]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'read', 'public_data'
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
INSERT INTO morbac.global_rules (user_id, activity, view, context_id, modality, priority)
|
||||||
|
VALUES (
|
||||||
|
NULL,
|
||||||
|
'read', 'public_data',
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||||
|
'prohibition', 100
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT morbac.t('Dave reads GlobalTech public_data [priority-100 global prohibition beats role permission]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'read', 'public_data'
|
||||||
|
), FALSE);
|
||||||
|
|
||||||
|
DELETE FROM morbac.global_rules WHERE activity = 'read' AND view = 'public_data';
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Section 8: NULL activity/view wildcards
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
\echo ''
|
||||||
|
\echo '--- 8. NULL wildcards: activity=NULL and view=NULL ---'
|
||||||
|
|
||||||
|
-- view=NULL grants read on every view for Karl (no role)
|
||||||
|
INSERT INTO morbac.global_rules (user_id, activity, view, context_id, modality)
|
||||||
|
VALUES (
|
||||||
|
'30000000-0000-0000-0000-000000000011', -- Karl
|
||||||
|
'read', NULL,
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||||
|
'permission'
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT morbac.t('Karl reads GlobalTech contracts [view=NULL global permission]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'30000000-0000-0000-0000-000000000011'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'read', 'contracts'
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
SELECT morbac.t('Karl reads GlobalTech financial_data [view=NULL global permission]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'30000000-0000-0000-0000-000000000011'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'read', 'financial_data'
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
DELETE FROM morbac.global_rules
|
||||||
|
WHERE user_id = '30000000-0000-0000-0000-000000000011'
|
||||||
|
AND activity = 'read' AND view IS NULL;
|
||||||
|
|
||||||
|
-- activity=NULL and view=NULL grants everything
|
||||||
|
INSERT INTO morbac.global_rules (user_id, activity, view, context_id, modality)
|
||||||
|
VALUES (
|
||||||
|
'30000000-0000-0000-0000-000000000011', -- Karl
|
||||||
|
NULL, NULL,
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||||
|
'permission'
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT morbac.t('Karl deletes GlobalTech contracts [activity=NULL, view=NULL global permission]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'30000000-0000-0000-0000-000000000011'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'delete', 'contracts'
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
DELETE FROM morbac.global_rules
|
||||||
|
WHERE user_id = '30000000-0000-0000-0000-000000000011'
|
||||||
|
AND activity IS NULL AND view IS NULL;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Section 9: Activity/view hierarchy applies when activity/view are set
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
\echo ''
|
||||||
|
\echo '--- 9. Activity/view hierarchy ---'
|
||||||
|
|
||||||
|
-- financial_data is a child of documents (view hierarchy from setup)
|
||||||
|
-- A global permission on 'documents' should cover 'financial_data'
|
||||||
|
INSERT INTO morbac.global_rules (user_id, activity, view, context_id, modality)
|
||||||
|
VALUES (
|
||||||
|
'30000000-0000-0000-0000-000000000011', -- Karl
|
||||||
|
'read', 'documents',
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||||
|
'permission'
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT morbac.t('Karl reads GlobalTech financial_data [global permission on documents covers child view]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'30000000-0000-0000-0000-000000000011'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'read', 'financial_data'
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
DELETE FROM morbac.global_rules
|
||||||
|
WHERE user_id = '30000000-0000-0000-0000-000000000011'
|
||||||
|
AND activity = 'read' AND view = 'documents';
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Section 10: Temporal global rules
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
\echo ''
|
||||||
|
\echo '--- 10. Temporal global rules ---'
|
||||||
|
|
||||||
|
INSERT INTO morbac.global_rules (user_id, activity, view, context_id, modality, valid_from, valid_until)
|
||||||
|
VALUES (
|
||||||
|
'30000000-0000-0000-0000-000000000011', -- Karl
|
||||||
|
'read', 'contracts',
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||||
|
'permission',
|
||||||
|
now() - interval '1 hour',
|
||||||
|
now() + interval '1 day'
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT morbac.t('Karl reads GlobalTech contracts [temporal global rule, active]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'30000000-0000-0000-0000-000000000011'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'read', 'contracts'
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
UPDATE morbac.global_rules
|
||||||
|
SET valid_until = now() - interval '1 second'
|
||||||
|
WHERE user_id = '30000000-0000-0000-0000-000000000011'
|
||||||
|
AND activity = 'read' AND view = 'contracts';
|
||||||
|
|
||||||
|
SELECT morbac.t('Karl reads GlobalTech contracts [temporal global rule, expired]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'30000000-0000-0000-0000-000000000011'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'read', 'contracts'
|
||||||
|
), FALSE);
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== Global Rules Tests Completed ==='
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- System Principals Tests
|
||||||
|
-- =============================================================================
|
||||||
|
-- Tests the morbac.system_principals registry and all associated protections.
|
||||||
|
--
|
||||||
|
-- Scenarios:
|
||||||
|
-- 1. Setup: register a system principal and define its permissions via global_rules
|
||||||
|
-- 2. Permissions work normally (system principal can access what it is granted)
|
||||||
|
-- 3. Prohibitions are ignored (no role-based, user-level, or global prohibition applies)
|
||||||
|
-- 4. Cannot assign roles to a system principal
|
||||||
|
-- 5. Cannot revoke roles from a system principal
|
||||||
|
-- 6. Cannot add negative role assignments for a system principal
|
||||||
|
-- 7. Cannot add user_rules for a system principal
|
||||||
|
-- 8. Cannot create delegations involving a system principal
|
||||||
|
-- 9. Cannot add a targeted global_rules prohibition for a system principal
|
||||||
|
-- 10. Cannot modify or delete existing global_rules for a system principal
|
||||||
|
--
|
||||||
|
-- Prerequisites: 00_setup.sql -> 13_global_rules.sql
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '================================================================'
|
||||||
|
\echo '14 -- SYSTEM PRINCIPALS'
|
||||||
|
\echo '================================================================'
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Section 1: Setup
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
\echo ''
|
||||||
|
\echo '--- 1. Setup ---'
|
||||||
|
|
||||||
|
-- Register a system principal (done as DB owner at deploy time)
|
||||||
|
INSERT INTO morbac.system_principals (user_id, description)
|
||||||
|
VALUES (
|
||||||
|
'40000000-0000-0000-0000-000000000001',
|
||||||
|
'Backend API service account'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Define its permissions via global_rules (immutable once inserted)
|
||||||
|
INSERT INTO morbac.global_rules (user_id, activity, view, context_id, modality)
|
||||||
|
VALUES (
|
||||||
|
'40000000-0000-0000-0000-000000000001',
|
||||||
|
NULL, NULL,
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||||
|
'permission'
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT morbac.t('System principal registered',
|
||||||
|
EXISTS (SELECT 1 FROM morbac.system_principals WHERE user_id = '40000000-0000-0000-0000-000000000001'),
|
||||||
|
TRUE);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Section 2: Permissions work
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
\echo ''
|
||||||
|
\echo '--- 2. System principal permissions are effective ---'
|
||||||
|
|
||||||
|
SELECT morbac.t('System principal reads GlobalTech documents [global permission, activity=NULL view=NULL]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'40000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'read', 'documents'
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
SELECT morbac.t('System principal deletes Engineering contracts [global permission covers all]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'40000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000002'::uuid,
|
||||||
|
'delete', 'contracts'
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Section 3: Prohibitions are ignored
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
\echo ''
|
||||||
|
\echo '--- 3. Prohibitions are ignored for system principals ---'
|
||||||
|
|
||||||
|
-- Insert a blanket global prohibition (user_id=NULL affects everyone normally)
|
||||||
|
INSERT INTO morbac.global_rules (user_id, activity, view, context_id, modality, priority)
|
||||||
|
VALUES (
|
||||||
|
NULL,
|
||||||
|
'delete', 'contracts',
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||||
|
'prohibition', 999
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT morbac.t('System principal deletes contracts [blanket global prohibition ignored]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'40000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'delete', 'contracts'
|
||||||
|
), TRUE);
|
||||||
|
|
||||||
|
-- Regular user is still affected by the prohibition
|
||||||
|
SELECT morbac.t('Dave deletes GlobalTech contracts [blanket prohibition applies to regular users]',
|
||||||
|
morbac.is_allowed_nocache(
|
||||||
|
'30000000-0000-0000-0000-000000000004'::uuid,
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'delete', 'contracts'
|
||||||
|
), FALSE);
|
||||||
|
|
||||||
|
DELETE FROM morbac.global_rules
|
||||||
|
WHERE user_id IS NULL AND activity = 'delete' AND view = 'contracts';
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Section 4: Cannot assign roles to a system principal
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
\echo ''
|
||||||
|
\echo '--- 4. Role assignment blocked ---'
|
||||||
|
|
||||||
|
SELECT morbac.t('Assigning role to system principal raises exception',
|
||||||
|
(SELECT COUNT(*) FROM (
|
||||||
|
SELECT morbac.assign_role(
|
||||||
|
'40000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
(SELECT id FROM morbac.roles WHERE name = 'employee' AND org_id = '10000000-0000-0000-0000-000000000001'),
|
||||||
|
'10000000-0000-0000-0000-000000000001'::uuid
|
||||||
|
)
|
||||||
|
) sub) = 0,
|
||||||
|
FALSE
|
||||||
|
) WHERE FALSE; -- skip, tested via exception below
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO morbac.user_roles (user_id, role_id, org_id)
|
||||||
|
VALUES (
|
||||||
|
'40000000-0000-0000-0000-000000000001',
|
||||||
|
(SELECT id FROM morbac.roles WHERE name = 'employee' AND org_id = '10000000-0000-0000-0000-000000000001'),
|
||||||
|
'10000000-0000-0000-0000-000000000001'
|
||||||
|
);
|
||||||
|
RAISE EXCEPTION 'expected exception not raised';
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
IF SQLERRM LIKE '%system principal%' THEN
|
||||||
|
RAISE NOTICE 'PASS: role assignment to system principal blocked';
|
||||||
|
ELSE
|
||||||
|
RAISE;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Section 5: Cannot add negative role assignments for a system principal
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
\echo ''
|
||||||
|
\echo '--- 5. Negative role assignment blocked ---'
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO morbac.negative_role_assignments (user_id, role_id, org_id)
|
||||||
|
VALUES (
|
||||||
|
'40000000-0000-0000-0000-000000000001',
|
||||||
|
(SELECT id FROM morbac.roles WHERE name = 'employee' AND org_id = '10000000-0000-0000-0000-000000000001'),
|
||||||
|
'10000000-0000-0000-0000-000000000001'
|
||||||
|
);
|
||||||
|
RAISE EXCEPTION 'expected exception not raised';
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
IF SQLERRM LIKE '%system principal%' THEN
|
||||||
|
RAISE NOTICE 'PASS: negative role assignment for system principal blocked';
|
||||||
|
ELSE
|
||||||
|
RAISE;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Section 6: Cannot add user_rules for a system principal
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
\echo ''
|
||||||
|
\echo '--- 6. user_rules blocked ---'
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO morbac.user_rules (user_id, org_id, activity, view, context_id, modality)
|
||||||
|
VALUES (
|
||||||
|
'40000000-0000-0000-0000-000000000001',
|
||||||
|
'10000000-0000-0000-0000-000000000001',
|
||||||
|
'read', 'documents',
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||||
|
'permission'
|
||||||
|
);
|
||||||
|
RAISE EXCEPTION 'expected exception not raised';
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
IF SQLERRM LIKE '%system principal%' THEN
|
||||||
|
RAISE NOTICE 'PASS: user_rules insert for system principal blocked';
|
||||||
|
ELSE
|
||||||
|
RAISE;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Section 7: Cannot create delegations involving a system principal
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
\echo ''
|
||||||
|
\echo '--- 7. Delegations blocked ---'
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO morbac.delegations (delegator_id, delegatee_id, role_id, org_id)
|
||||||
|
VALUES (
|
||||||
|
'30000000-0000-0000-0000-000000000001', -- Alice as delegator
|
||||||
|
'40000000-0000-0000-0000-000000000001', -- system principal as delegatee
|
||||||
|
(SELECT id FROM morbac.roles WHERE name = 'employee' AND org_id = '10000000-0000-0000-0000-000000000001'),
|
||||||
|
'10000000-0000-0000-0000-000000000001'
|
||||||
|
);
|
||||||
|
RAISE EXCEPTION 'expected exception not raised';
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
IF SQLERRM LIKE '%system principal%' THEN
|
||||||
|
RAISE NOTICE 'PASS: delegation to system principal blocked';
|
||||||
|
ELSE
|
||||||
|
RAISE;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Section 8: Cannot add a targeted global_rules prohibition
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
\echo ''
|
||||||
|
\echo '--- 8. Targeted global prohibition blocked ---'
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO morbac.global_rules (user_id, activity, view, context_id, modality)
|
||||||
|
VALUES (
|
||||||
|
'40000000-0000-0000-0000-000000000001',
|
||||||
|
'delete', 'contracts',
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||||
|
'prohibition'
|
||||||
|
);
|
||||||
|
RAISE EXCEPTION 'expected exception not raised';
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
IF SQLERRM LIKE '%system principal%' THEN
|
||||||
|
RAISE NOTICE 'PASS: global prohibition targeting system principal blocked';
|
||||||
|
ELSE
|
||||||
|
RAISE;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Section 9: Cannot add a permission global_rule for a system principal
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
\echo ''
|
||||||
|
\echo '--- 9. New global permission for system principal blocked ---'
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO morbac.global_rules (user_id, activity, view, context_id, modality)
|
||||||
|
VALUES (
|
||||||
|
'40000000-0000-0000-0000-000000000001',
|
||||||
|
'read', 'documents',
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'always'),
|
||||||
|
'permission'
|
||||||
|
);
|
||||||
|
RAISE EXCEPTION 'expected exception not raised';
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
IF SQLERRM LIKE '%system principal%' THEN
|
||||||
|
RAISE NOTICE 'PASS: new global permission for system principal blocked (define at deploy time only)';
|
||||||
|
ELSE
|
||||||
|
RAISE;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Section 10: Cannot delete existing global_rules for a system principal
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
\echo ''
|
||||||
|
\echo '--- 10. Deleting system principal global_rules blocked ---'
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
DELETE FROM morbac.global_rules
|
||||||
|
WHERE user_id = '40000000-0000-0000-0000-000000000001';
|
||||||
|
RAISE EXCEPTION 'expected exception not raised';
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
IF SQLERRM LIKE '%system principal%' THEN
|
||||||
|
RAISE NOTICE 'PASS: deletion of system principal global rules blocked';
|
||||||
|
ELSE
|
||||||
|
RAISE;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== System Principals Tests Completed ==='
|
||||||
@@ -30,6 +30,8 @@ BUILD_FILES=(
|
|||||||
"rules.sql"
|
"rules.sql"
|
||||||
"cross_org_rules.sql"
|
"cross_org_rules.sql"
|
||||||
"user_rules.sql"
|
"user_rules.sql"
|
||||||
|
"global_rules.sql"
|
||||||
|
"system_principals.sql"
|
||||||
"activity_view_bindings.sql"
|
"activity_view_bindings.sql"
|
||||||
"obligations.sql"
|
"obligations.sql"
|
||||||
"audit.sql"
|
"audit.sql"
|
||||||
|
|||||||
Reference in New Issue
Block a user