diff --git a/ADMIN_GUIDE.md b/ADMIN_GUIDE.md index 4a4d963..f45bcba 100644 --- a/ADMIN_GUIDE.md +++ b/ADMIN_GUIDE.md @@ -41,7 +41,7 @@ FROM morbac.orgs WHERE name = 'Acme Corp'; -- Grant ability to manage policies INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, modality, context_id) -SELECT +SELECT o.id, r.id, 'manage', @@ -54,7 +54,7 @@ WHERE o.name = 'Acme Corp' AND r.name = 'org_admin'; -- Grant ability to manage roles INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, modality, context_id) -SELECT +SELECT o.id, r.id, 'manage', @@ -67,7 +67,7 @@ WHERE o.name = 'Acme Corp' AND r.name = 'org_admin'; -- Grant ability to assign ALL roles (wildcard) INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, modality, context_id) -SELECT +SELECT o.id, r.id, 'assign_role', @@ -80,7 +80,7 @@ WHERE o.name = 'Acme Corp' AND r.name = 'org_admin'; -- Assign someone as org admin INSERT INTO morbac.user_roles (user_id, role_id, org_id) -SELECT +SELECT 'alice-uuid'::uuid, r.id, o.id @@ -101,7 +101,7 @@ FROM morbac.orgs WHERE name = 'Acme Corp'; -- Grant ability to assign specific roles INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, modality, context_id) -SELECT +SELECT o.id, r.id, 'assign_role', @@ -115,7 +115,7 @@ WHERE o.name = 'Acme Corp' AND r.name = 'hr_manager'; -- Prohibit assigning admin roles INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, modality, context_id) -SELECT +SELECT o.id, r.id, 'assign_role', @@ -139,7 +139,7 @@ FROM morbac.orgs WHERE name = 'Acme Corp'; -- Grant policy management INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, modality, context_id) -SELECT +SELECT o.id, r.id, 'manage', @@ -223,7 +223,7 @@ BEGIN p_role_id, p_org_id ); - + v_result := json_build_object( 'success', true, 'message', 'Role assigned successfully' @@ -234,7 +234,7 @@ BEGIN 'error', SQLERRM ); END; - + RETURN v_result; END; $$; @@ -290,7 +290,7 @@ INSERT INTO morbac.contexts (name, description, evaluator) VALUES -- HR can only assign roles during business hours INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, modality, context_id) -SELECT +SELECT o.id, r.id, 'assign_role', @@ -318,8 +318,8 @@ WHERE o.name = 'Acme Corp' AND r.name = 'hr_manager'; INSERT INTO morbac.orgs (name) VALUES ('Acme Corp'); -- 2. Create roles -INSERT INTO morbac.roles (org_id, name) -SELECT id, name FROM morbac.orgs, +INSERT INTO morbac.roles (org_id, name) +SELECT id, name FROM morbac.orgs, (VALUES ('org_admin'), ('hr_manager'), ('employee'), ('manager')) AS roles(name) WHERE morbac.orgs.name = 'Acme Corp'; @@ -328,7 +328,7 @@ INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, m SELECT o.id, r.id, activity, target, 'permission', c.id FROM morbac.orgs o JOIN morbac.roles r ON r.org_id = o.id -CROSS JOIN (VALUES +CROSS JOIN (VALUES ('manage', 'policies'), ('manage', 'roles'), ('assign_role', '*') @@ -356,7 +356,7 @@ WHERE o.name = 'Acme Corp' AND r.name = 'org_admin'; SELECT morbac.admin_assign_role( 'alice-uuid'::uuid, 'bob-uuid'::uuid, - (SELECT id FROM morbac.roles WHERE name = 'hr_manager' AND org_id = + (SELECT id FROM morbac.roles WHERE name = 'hr_manager' AND org_id = (SELECT id FROM morbac.orgs WHERE name = 'Acme Corp')), (SELECT id FROM morbac.orgs WHERE name = 'Acme Corp') ); @@ -365,7 +365,7 @@ SELECT morbac.admin_assign_role( SELECT morbac.admin_assign_role( 'bob-uuid'::uuid, 'charlie-uuid'::uuid, - (SELECT id FROM morbac.roles WHERE name = 'employee' AND org_id = + (SELECT id FROM morbac.roles WHERE name = 'employee' AND org_id = (SELECT id FROM morbac.orgs WHERE name = 'Acme Corp')), (SELECT id FROM morbac.orgs WHERE name = 'Acme Corp') ); diff --git a/CHANGELOG.md b/CHANGELOG.md index 87a0bf7..18d1ea0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,12 +21,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Advanced features: - Organization, role, activity, and view hierarchies with transitive closure - Temporal delegation with time bounds + - Temporal constraints on rules with validity periods (valid_from, valid_until) - Negative role assignments - Separation of Duty (SoD) constraints - Role cardinality constraints (min/max users) - Derived roles (computed via functions) - - Cross-organizational rules + - Cross-organizational rules with temporal support - Administration rules (meta-policies) + - Audit logging for security-critical operations +- Audit logging system: + - Generic audit trigger for tracking INSERT/UPDATE/DELETE operations + - Comprehensive audit log with JSONB support for before/after states + - Helper functions: `enable_audit()`, `disable_audit()` + - Field-level change tracking + - Client connection metadata capture (IP, application name, session user) - Policy DSL with idempotent compiler - RLS helper functions for PostgREST integration - Comprehensive test suite with 20 test scenarios diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 09a0981..837f422 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -446,6 +446,147 @@ VALUES (org_id, sec_admin_role_id, true); SELECT morbac.is_admin_allowed(user_uuid, org_id, 'manage_policies'); ``` +### Temporal Constraints + +Rules and cross-organizational rules support optional temporal validity periods. Rules are only active within their specified time window. + +**Adding temporal constraints to rules:** + +```sql +-- Rule active only in 2024 +INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, valid_from, valid_until) +VALUES (org_id, contractor_role_id, 'read', 'projects', ctx_id, 'permission', + '2024-01-01 00:00:00+00', '2024-12-31 23:59:59+00'); + +-- Rule active starting from a specific date (no end date) +INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, valid_from) +VALUES (org_id, employee_role_id, 'read', 'handbook', ctx_id, 'permission', + '2024-06-01 00:00:00+00'); + +-- Rule expires at a specific date +INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, valid_until) +VALUES (org_id, temp_role_id, 'write', 'reports', ctx_id, 'permission', + '2024-12-31 23:59:59+00'); +``` + +**Temporal constraints on cross-org rules:** + +```sql +INSERT INTO morbac.cross_org_rules ( + source_org_id, target_org_id, role_id, activity, view, modality, + valid_from, valid_until +) VALUES ( + parent_org_id, subsidiary_org_id, auditor_role_id, + 'read', 'financial_data', 'permission', + '2024-01-01 00:00:00+00', '2024-03-31 23:59:59+00' +); +``` + +**Checking rule validity manually:** + +```sql +SELECT morbac.is_rule_valid(valid_from, valid_until); +-- Returns: TRUE if current time is within bounds (or no constraints set) +-- FALSE if outside validity period +``` + +**Behavior:** +- Both `valid_from` and `valid_until` are optional (can be NULL) +- If both are NULL, rule is always active +- If only `valid_from` is set, rule is active from that time onwards +- If only `valid_until` is set, rule is active until that time +- If both are set, rule is active only during that window +- `is_allowed()` automatically checks temporal validity for all rules +- Expired rules are effectively inactive but remain in database for audit purposes + +### Audit Logging + +Comprehensive audit trail for security-critical operations. The audit system tracks all INSERT, UPDATE, and DELETE operations on specified tables. + +**Audit log table structure:** + +```sql +morbac.audit_log ( + id BIGSERIAL PRIMARY KEY, + timestamp TIMESTAMPTZ, -- When change occurred + user_id UUID, -- User making change (if applicable) + org_id UUID, -- Organization context + table_name TEXT, -- Table being modified + operation TEXT, -- INSERT, UPDATE, DELETE + record_id UUID, -- ID of affected record + old_data JSONB, -- Record state before change (UPDATE/DELETE) + new_data JSONB, -- Record state after change (INSERT/UPDATE) + changed_fields TEXT[], -- List of modified columns (UPDATE) + session_user TEXT, -- PostgreSQL session user + client_addr INET, -- Client IP address + application_name TEXT, -- Application name from connection + metadata JSONB -- Additional custom metadata +) +``` + +**Enabling audit logging:** + +```sql +-- Enable auditing on user_roles table +SELECT morbac.enable_audit('user_roles'); + +-- Enable auditing on multiple tables +SELECT morbac.enable_audit('rules'); +SELECT morbac.enable_audit('delegations'); +SELECT morbac.enable_audit('admin_rules'); +``` + +**Disabling audit logging:** + +```sql +SELECT morbac.disable_audit('user_roles'); +``` + +**Querying audit log:** + +```sql +-- Recent changes to user_roles +SELECT + timestamp, + operation, + session_user, + old_data->>'role_id' AS old_role, + new_data->>'role_id' AS new_role +FROM morbac.audit_log +WHERE table_name = 'user_roles' +ORDER BY timestamp DESC +LIMIT 20; + +-- All changes by specific user +SELECT * FROM morbac.audit_log +WHERE user_id = '123e4567-e89b-12d3-a456-426614174000' +ORDER BY timestamp DESC; + +-- Changes to specific record +SELECT * FROM morbac.audit_log +WHERE table_name = 'rules' AND record_id = rule_uuid +ORDER BY timestamp; + +-- Detect field-level changes +SELECT + timestamp, + record_id, + changed_fields, + old_data, + new_data +FROM morbac.audit_log +WHERE table_name = 'rules' + AND operation = 'UPDATE' + AND 'modality' = ANY(changed_fields); +``` + +**Best practices:** +- Enable auditing on security-critical tables: `user_roles`, `rules`, `delegations`, `admin_rules`, `sod_conflicts` +- Create indexes on frequently queried columns: `CREATE INDEX ON morbac.audit_log(table_name, timestamp DESC)` +- Implement retention policies to archive old audit logs +- Use JSONB operators to query `old_data` and `new_data` efficiently +- Consider partitioning `audit_log` by timestamp for large installations + ## API Reference ### Authorization Functions diff --git a/README.md b/README.md index c31ab48..baa03a9 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,12 @@ A PostgreSQL extension implementing the Multi-OrBAC access control model - enabl - Activity and view hierarchies with transitive permission inheritance - Prohibition precedence over permissions - Temporal delegation with time bounds +- Temporal constraints on rules with validity periods - Separation of duty constraints - Derived roles computed from application logic - Cross-organizational policies - Context-aware rules +- Audit logging for security-critical operations - Row-level security integration ### Prerequisites @@ -109,6 +111,11 @@ CREATE POLICY doc_access ON app.documents ### Advanced Features ```sql +-- Temporal rules: Rule active only during business hours or specific period +INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, valid_from, valid_until) +VALUES (org_id, role_id, 'write', 'sensitive_data', ctx_id, 'permission', + '2024-01-01 00:00:00', '2024-12-31 23:59:59'); + -- Delegation: Alice delegates role to Bob for 1 week INSERT INTO morbac.delegations (delegator_user_id, delegate_user_id, role_id, org_id, valid_until) VALUES (alice_id, bob_id, role_id, org_id, NOW() + INTERVAL '7 days'); @@ -120,6 +127,15 @@ VALUES (preparer_role_id, approver_role_id, org_id); -- Cross-Org: Global auditor can access subsidiary INSERT INTO morbac.cross_org_rules (source_org_id, target_org_id, role_id, activity, view, modality) VALUES (global_org_id, subsidiary_org_id, auditor_role_id, 'read', 'reports', 'permission'); + +-- Audit logging: Track changes to user roles +SELECT morbac.enable_audit('user_roles'); + +-- Query audit log +SELECT * FROM morbac.audit_log +WHERE table_name = 'user_roles' +ORDER BY timestamp DESC +LIMIT 10; ``` ## Testing diff --git a/SECURITY.md b/SECURITY.md index 275af40..a8a0ac4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -98,8 +98,24 @@ AS $$ ... $$; ### 4. Audit Critical Operations ```sql --- Log policy changes and authorization decisions --- (morbac_pg does not include audit logging by default) +-- Enable audit logging on security-critical tables +SELECT morbac.enable_audit('user_roles'); +SELECT morbac.enable_audit('rules'); +SELECT morbac.enable_audit('delegations'); + +-- Query audit log for security review +SELECT + timestamp, + operation, + table_name, + session_user, + client_addr, + old_data, + new_data +FROM morbac.audit_log +WHERE table_name = 'user_roles' +ORDER BY timestamp DESC +LIMIT 100; ``` ### 5. Regular Updates diff --git a/morbac_pg--1.0.sql b/morbac_pg--1.0.sql index 6c15818..55d8503 100644 --- a/morbac_pg--1.0.sql +++ b/morbac_pg--1.0.sql @@ -369,15 +369,19 @@ CREATE TABLE morbac.rules ( view TEXT NOT NULL REFERENCES morbac.views(name) ON DELETE CASCADE, context_id UUID NOT NULL REFERENCES morbac.contexts(id) ON DELETE CASCADE, modality morbac.modality NOT NULL, + valid_from TIMESTAMPTZ, + valid_until TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), metadata JSONB DEFAULT '{}'::jsonb, - UNIQUE(org_id, role_id, activity, view, context_id, modality) + UNIQUE(org_id, role_id, activity, view, context_id, modality), + CHECK (valid_until IS NULL OR valid_from IS NULL OR valid_until > valid_from) ); CREATE INDEX idx_rules_org_role ON morbac.rules(org_id, role_id); CREATE INDEX idx_rules_activity_view ON morbac.rules(activity, view); CREATE INDEX idx_rules_modality ON morbac.rules(modality); CREATE INDEX idx_rules_lookup ON morbac.rules(org_id, role_id, activity, view, modality); +CREATE INDEX idx_rules_validity ON morbac.rules(valid_from, valid_until); COMMENT ON TABLE morbac.rules IS 'Core OrBAC rules - Permission, Prohibition, Obligation, Recommendation'; COMMENT ON COLUMN morbac.rules.org_id IS 'Organization scope'; @@ -386,6 +390,42 @@ COMMENT ON COLUMN morbac.rules.activity IS 'Activity (abstract action)'; COMMENT ON COLUMN morbac.rules.view IS 'View (abstract object category)'; COMMENT ON COLUMN morbac.rules.context_id IS 'Context condition'; COMMENT ON COLUMN morbac.rules.modality IS 'Deontic modality: permission, prohibition, obligation, recommendation'; +COMMENT ON COLUMN morbac.rules.valid_from IS 'Optional: Rule valid from this timestamp'; +COMMENT ON COLUMN morbac.rules.valid_until IS 'Optional: Rule valid until this timestamp'; + +-- Helper function to check if a rule is currently valid +CREATE OR REPLACE FUNCTION morbac.is_rule_valid( + p_valid_from TIMESTAMPTZ, + p_valid_until TIMESTAMPTZ +) +RETURNS BOOLEAN +LANGUAGE plpgsql +STABLE +AS $$ +DECLARE + v_now TIMESTAMPTZ := CURRENT_TIMESTAMP; +BEGIN + -- If no temporal constraints, rule is valid + IF p_valid_from IS NULL AND p_valid_until IS NULL THEN + RETURN TRUE; + END IF; + + -- Check valid_from + IF p_valid_from IS NOT NULL AND v_now < p_valid_from THEN + RETURN FALSE; + END IF; + + -- Check valid_until + IF p_valid_until IS NOT NULL AND v_now >= p_valid_until THEN + RETURN FALSE; + END IF; + + RETURN TRUE; +END; +$$; + +COMMENT ON FUNCTION morbac.is_rule_valid(TIMESTAMPTZ, TIMESTAMPTZ) IS +'Check if a rule is currently valid based on temporal constraints'; -- ============================================================================= -- 10a. INTER-ORGANIZATIONAL RULES @@ -403,17 +443,23 @@ CREATE TABLE morbac.cross_org_rules ( context_id UUID NOT NULL REFERENCES morbac.contexts(id) ON DELETE CASCADE, modality morbac.modality NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + valid_from TIMESTAMPTZ, + valid_until TIMESTAMPTZ, metadata JSONB DEFAULT '{}'::jsonb, CHECK (source_org_id != target_org_id), + CHECK (valid_until IS NULL OR valid_from IS NULL OR valid_until > valid_from), UNIQUE(source_org_id, target_org_id, role_id, activity, view, context_id, modality) ); CREATE INDEX idx_cross_org_rules_source ON morbac.cross_org_rules(source_org_id, role_id); CREATE INDEX idx_cross_org_rules_target ON morbac.cross_org_rules(target_org_id); +CREATE INDEX idx_cross_org_rules_temporal ON morbac.cross_org_rules(valid_from, valid_until); COMMENT ON TABLE morbac.cross_org_rules IS 'Inter-organizational rules for cross-org access'; COMMENT ON COLUMN morbac.cross_org_rules.source_org_id IS 'Organization where user has role'; COMMENT ON COLUMN morbac.cross_org_rules.target_org_id IS 'Organization where resource resides'; +COMMENT ON COLUMN morbac.cross_org_rules.valid_from IS 'Optional start time for rule validity'; +COMMENT ON COLUMN morbac.cross_org_rules.valid_until IS 'Optional end time for rule validity'; -- ============================================================================= -- 10b. ADMINISTRATION RULES @@ -439,6 +485,179 @@ COMMENT ON TABLE morbac.admin_rules IS 'Administration rules - meta-policies for COMMENT ON COLUMN morbac.admin_rules.admin_activity IS 'Admin action: create_rule, modify_rule, delete_rule, assign_role, etc.'; COMMENT ON COLUMN morbac.admin_rules.admin_target IS 'What can be administered: rules, roles, users, orgs, etc.'; +-- ============================================================================= +-- 10c. AUDIT LOG +-- ============================================================================= +-- Optional audit logging for tracking changes to security-critical tables +-- Enable/disable per table with triggers + +CREATE TABLE morbac.audit_log ( + id BIGSERIAL PRIMARY KEY, + timestamp TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + user_id UUID, + org_id UUID, + table_name TEXT NOT NULL, + operation TEXT NOT NULL, + record_id UUID, + old_data JSONB, + new_data JSONB, + changed_fields TEXT[], + session_user TEXT DEFAULT SESSION_USER, + client_addr INET DEFAULT INET_CLIENT_ADDR(), + application_name TEXT DEFAULT CURRENT_SETTING('application_name', true), + metadata JSONB DEFAULT '{}'::jsonb +); + +CREATE INDEX idx_audit_log_timestamp ON morbac.audit_log(timestamp DESC); +CREATE INDEX idx_audit_log_user_id ON morbac.audit_log(user_id); +CREATE INDEX idx_audit_log_org_id ON morbac.audit_log(org_id); +CREATE INDEX idx_audit_log_table_operation ON morbac.audit_log(table_name, operation); +CREATE INDEX idx_audit_log_record_id ON morbac.audit_log(record_id); + +COMMENT ON TABLE morbac.audit_log IS 'Audit trail for security-critical operations'; +COMMENT ON COLUMN morbac.audit_log.user_id IS 'Application user (from morbac.current_user_id() if available)'; +COMMENT ON COLUMN morbac.audit_log.org_id IS 'Organization context (from morbac.current_org_id() if available)'; +COMMENT ON COLUMN morbac.audit_log.operation IS 'INSERT, UPDATE, DELETE, or custom operation name'; +COMMENT ON COLUMN morbac.audit_log.changed_fields IS 'Array of field names that changed (for UPDATE operations)'; +COMMENT ON COLUMN morbac.audit_log.session_user IS 'Database session user'; +COMMENT ON COLUMN morbac.audit_log.client_addr IS 'Client IP address'; + +-- Generic audit trigger function +CREATE OR REPLACE FUNCTION morbac.audit_trigger() +RETURNS TRIGGER +LANGUAGE plpgsql +AS $$ +DECLARE + v_old_data JSONB; + v_new_data JSONB; + v_changed_fields TEXT[]; + v_user_id UUID; + v_org_id UUID; + v_record_id UUID; +BEGIN + -- Try to get current user/org context + BEGIN + v_user_id := morbac.current_user_id(); + EXCEPTION WHEN OTHERS THEN + v_user_id := NULL; + END; + + BEGIN + v_org_id := morbac.current_org_id(); + EXCEPTION WHEN OTHERS THEN + v_org_id := NULL; + END; + + -- Handle different operations + IF TG_OP = 'DELETE' THEN + v_old_data := row_to_json(OLD)::jsonb; + v_new_data := NULL; + v_record_id := (v_old_data->>'id')::uuid; + ELSIF TG_OP = 'INSERT' THEN + v_old_data := NULL; + v_new_data := row_to_json(NEW)::jsonb; + v_record_id := (v_new_data->>'id')::uuid; + ELSIF TG_OP = 'UPDATE' THEN + v_old_data := row_to_json(OLD)::jsonb; + v_new_data := row_to_json(NEW)::jsonb; + v_record_id := (v_new_data->>'id')::uuid; + + -- Identify changed fields + SELECT array_agg(key) + INTO v_changed_fields + FROM jsonb_each(v_old_data) + WHERE v_old_data->key IS DISTINCT FROM v_new_data->key; + END IF; + + -- Override org_id from record if available + IF v_org_id IS NULL THEN + IF v_new_data ? 'org_id' THEN + v_org_id := (v_new_data->>'org_id')::uuid; + ELSIF v_old_data ? 'org_id' THEN + v_org_id := (v_old_data->>'org_id')::uuid; + END IF; + END IF; + + -- Insert audit record + INSERT INTO morbac.audit_log ( + user_id, + org_id, + table_name, + operation, + record_id, + old_data, + new_data, + changed_fields + ) VALUES ( + v_user_id, + v_org_id, + TG_TABLE_NAME, + TG_OP, + v_record_id, + v_old_data, + v_new_data, + v_changed_fields + ); + + -- Return appropriate value + IF TG_OP = 'DELETE' THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; +END; +$$; + +COMMENT ON FUNCTION morbac.audit_trigger() IS +'Generic audit trigger function - captures INSERT/UPDATE/DELETE operations'; + +-- Helper function to enable audit logging on a table +CREATE OR REPLACE FUNCTION morbac.enable_audit(p_table_name TEXT) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +DECLARE + v_trigger_name TEXT; +BEGIN + v_trigger_name := 'audit_' || p_table_name; + + EXECUTE format( + 'CREATE TRIGGER %I AFTER INSERT OR UPDATE OR DELETE ON morbac.%I ' || + 'FOR EACH ROW EXECUTE FUNCTION morbac.audit_trigger()', + v_trigger_name, + p_table_name + ); + + RAISE NOTICE 'Audit logging enabled for morbac.%', p_table_name; +END; +$$; + +COMMENT ON FUNCTION morbac.enable_audit(TEXT) IS +'Enable audit logging on a morbac table - creates audit trigger'; + +-- Helper function to disable audit logging on a table +CREATE OR REPLACE FUNCTION morbac.disable_audit(p_table_name TEXT) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +DECLARE + v_trigger_name TEXT; +BEGIN + v_trigger_name := 'audit_' || p_table_name; + + EXECUTE format( + 'DROP TRIGGER IF EXISTS %I ON morbac.%I', + v_trigger_name, + p_table_name + ); + + RAISE NOTICE 'Audit logging disabled for morbac.%', p_table_name; +END; +$$; + +COMMENT ON FUNCTION morbac.disable_audit(TEXT) IS +'Disable audit logging on a morbac table - removes audit trigger'; + -- ============================================================================= -- 11. HIERARCHY FUNCTIONS -- ============================================================================= @@ -882,6 +1101,7 @@ BEGIN -- - Activity hierarchy (senior activities imply junior) -- - View hierarchy (senior views imply junior) -- - Comprehensive roles (direct, delegated, derived, inherited) + -- - Temporal validity (if rule has time constraints) FOR v_rule IN SELECT r.context_id @@ -900,6 +1120,8 @@ BEGIN AND r.role_id IN ( SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, p_org_id) ) + -- Check temporal validity + AND morbac.is_rule_valid(r.valid_from, r.valid_until) LOOP -- Evaluate context IF morbac.eval_context(v_rule.context_id) THEN @@ -920,6 +1142,8 @@ BEGIN AND cr.role_id IN ( SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, cr.source_org_id) ) + -- Check temporal validity + AND morbac.is_rule_valid(cr.valid_from, cr.valid_until) LOOP IF morbac.eval_context(v_rule.context_id) THEN RETURN FALSE; @@ -944,6 +1168,8 @@ BEGIN AND r.role_id IN ( SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, p_org_id) ) + -- Check temporal validity + AND morbac.is_rule_valid(r.valid_from, r.valid_until) LOOP -- Evaluate context IF morbac.eval_context(v_rule.context_id) THEN @@ -963,6 +1189,8 @@ BEGIN AND cr.role_id IN ( SELECT role_id FROM morbac.get_comprehensive_roles(p_user_id, cr.source_org_id) ) + -- Check temporal validity + AND morbac.is_rule_valid(cr.valid_from, cr.valid_until) LOOP IF morbac.eval_context(v_rule.context_id) THEN RETURN TRUE; diff --git a/test_morbac.sql b/test_morbac.sql index 0e30649..d5b36c6 100644 --- a/test_morbac.sql +++ b/test_morbac.sql @@ -127,7 +127,10 @@ INSERT INTO morbac.activities (name, description) VALUES INSERT INTO morbac.views (name, description) VALUES ('documents', 'Document resources'), ('reports', 'Report resources'), - ('sensitive_data', 'Sensitive/confidential data'); + ('sensitive_data', 'Sensitive/confidential data'), + ('temp_documents', 'Temporary documents'), + ('future_documents', 'Future documents'), + ('active_documents', 'Active documents'); SELECT * FROM morbac.activities ORDER BY name; SELECT * FROM morbac.views ORDER BY name; @@ -607,6 +610,122 @@ SELECT 'documents' ) as allowed; +\echo '' +\echo '=== Test 21: Temporal Constraints on Rules ===' + +-- Create a time-limited rule (expires in the past) +INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, valid_until) +VALUES ( + '11111111-1111-1111-1111-111111111111', + 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', -- employee role + 'write', + 'temp_documents', + (SELECT id FROM morbac.contexts WHERE name = 'always'), + 'permission', + '2020-01-01 00:00:00+00' -- Expired rule +); + +-- Create a future rule (not yet active) +INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, valid_from) +VALUES ( + '11111111-1111-1111-1111-111111111111', + 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + 'write', + 'future_documents', + (SELECT id FROM morbac.contexts WHERE name = 'always'), + 'permission', + '2030-01-01 00:00:00+00' -- Future rule +); + +-- Create an active time-bounded rule +INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, valid_from, valid_until) +VALUES ( + '11111111-1111-1111-1111-111111111111', + 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + 'write', + 'active_documents', + (SELECT id FROM morbac.contexts WHERE name = 'always'), + 'permission', + '2020-01-01 00:00:00+00', -- Started in the past + '2030-12-31 23:59:59+00' -- Ends in the future +); + +\echo 'Alice (employee) access to temporal-constrained documents:' +SELECT + 'write to temp_documents (expired rule)' as action, + morbac.is_allowed( + 'aaaaaaaa-0000-0000-0000-000000000001'::uuid, + '11111111-1111-1111-1111-111111111111'::uuid, + 'write', + 'temp_documents' + ) as allowed +UNION ALL +SELECT + 'write to future_documents (not yet active)' as action, + morbac.is_allowed( + 'aaaaaaaa-0000-0000-0000-000000000001'::uuid, + '11111111-1111-1111-1111-111111111111'::uuid, + 'write', + 'future_documents' + ) as allowed +UNION ALL +SELECT + 'write to active_documents (currently valid)' as action, + morbac.is_allowed( + 'aaaaaaaa-0000-0000-0000-000000000001'::uuid, + '11111111-1111-1111-1111-111111111111'::uuid, + 'write', + 'active_documents' + ) as allowed; + +\echo '' +\echo '=== Test 22: Audit Logging ===' + +-- Enable audit on user_roles table +SELECT morbac.enable_audit('user_roles'); + +\echo 'Audit trigger created for user_roles table' + +-- Perform some auditable operations +INSERT INTO morbac.user_roles (user_id, role_id, org_id) +VALUES ( + 'ffffffff-0000-0000-0000-000000000001'::uuid, + 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', -- employee role + '11111111-1111-1111-1111-111111111111' +); + +-- Update an existing user_role +UPDATE morbac.user_roles +SET active = false +WHERE user_id = 'aaaaaaaa-0000-0000-0000-000000000001'::uuid + AND role_id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'; + +-- Delete a user role +DELETE FROM morbac.user_roles +WHERE user_id = 'ffffffff-0000-0000-0000-000000000001'::uuid + AND role_id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'; + +\echo 'Audit log entries for user_roles operations:' +SELECT + operation, + record_id, + CASE + WHEN old_data IS NOT NULL THEN old_data->>'active' + ELSE 'N/A' + END as old_active, + CASE + WHEN new_data IS NOT NULL THEN new_data->>'active' + ELSE 'N/A' + END as new_active, + session_user, + timestamp +FROM morbac.audit_log +WHERE table_name = 'user_roles' +ORDER BY timestamp; + +\echo 'Disable audit on user_roles:' +SELECT morbac.disable_audit('user_roles'); + \echo '' \echo '=== ALL TESTS COMPLETED SUCCESSFULLY ===' \echo '' @@ -621,7 +740,5 @@ SELECT \echo '- Cardinality constraints' \echo '- Administration rules' \echo '- Cross-organizational rules' -\echo '- Derived roles (framework ready)' -\echo '- Complete authorization with all features' -\echo '- Prohibition precedence verified' -\echo '- Multi-organization support verified' +\echo '- Temporal constraints on rules (valid_from, valid_until)' +\echo '- Audit logging for security-critical operations'