misc(all): cleanup comments, update documentation, tidy up code

This commit is contained in:
2026-03-27 21:56:00 +01:00
parent 41747a79a0
commit 145f50749b
26 changed files with 131 additions and 451 deletions
+6 -6
View File
@@ -26,9 +26,9 @@ Feature requests are welcome! Please open an issue describing:
2. **Create a feature branch**: `git checkout -b feature/my-feature` 2. **Create a feature branch**: `git checkout -b feature/my-feature`
3. **Make your changes**: 3. **Make your changes**:
- Follow the existing code style (SQL formatting, naming conventions) - Follow the existing code style (SQL formatting, naming conventions)
- Add tests in `test_morbac.sql` for new features - Add tests in `tests/` for new features
- Update `DOCUMENTATION.md` if adding new functionality - Update `docs/DOCUMENTATION.md` if adding new functionality
4. **Test your changes**: `make test` 4. **Test your changes**: `make check`
5. **Commit with clear messages**: `git commit -m "Add feature: description"` 5. **Commit with clear messages**: `git commit -m "Add feature: description"`
6. **Push to your fork**: `git push origin feature/my-feature` 6. **Push to your fork**: `git push origin feature/my-feature`
7. **Open a Pull Request** with a description of your changes 7. **Open a Pull Request** with a description of your changes
@@ -44,8 +44,8 @@ Feature requests are welcome! Please open an issue describing:
### Testing ### Testing
All changes must include tests: All changes must include tests:
- Add test cases to `test_morbac.sql` - Add test cases in `tests/` following the numbered file convention
- Ensure all existing tests still pass - Ensure all existing tests still pass (`make check`)
- Test against PostgreSQL 13+ (mention version in PR) - Test against PostgreSQL 13+ (mention version in PR)
### Documentation ### Documentation
@@ -69,7 +69,7 @@ cd pgmorbac
make install make install
# Run tests # Run tests
make test make check
``` ```
## Questions? ## Questions?
+13 -11
View File
@@ -142,16 +142,17 @@ VALUES (org_id, role_id, 'write', 'sensitive_data', ctx_id, 'permission',
'2024-01-01 00:00:00', '2024-12-31 23:59:59'); '2024-01-01 00:00:00', '2024-12-31 23:59:59');
-- Delegation: Alice delegates role to Bob for 1 week -- 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) INSERT INTO morbac.delegations (delegator_id, delegatee_id, role_id, org_id, valid_until)
VALUES (alice_id, bob_id, role_id, org_id, NOW() + INTERVAL '7 days'); VALUES (alice_id, bob_id, role_id, org_id, NOW() + INTERVAL '7 days');
-- Separation of Duty: Invoice creator and approver roles are mutually exclusive -- Separation of Duty: Invoice creator and approver roles are mutually exclusive
INSERT INTO morbac.sod_conflicts (role1_id, role2_id, org_id) INSERT INTO morbac.sod_conflicts (role_a_id, role_b_id, org_id)
VALUES (creator_role_id, approver_role_id, org_id); VALUES (creator_role_id, approver_role_id, org_id);
-- Cross-Org: Global auditor can access subsidiary -- Cross-Org: Global auditor can access subsidiary
INSERT INTO morbac.cross_org_rules (source_org_id, target_org_id, role_id, activity, view, modality) INSERT INTO morbac.cross_org_rules (source_org_id, target_org_id, role_id, activity, view, context_id, modality)
VALUES (global_org_id, subsidiary_org_id, auditor_role_id, 'read', 'reports', 'permission'); VALUES (global_org_id, subsidiary_org_id, auditor_role_id, 'read', 'reports',
(SELECT id FROM morbac.contexts WHERE name = 'always'), 'permission');
-- Audit logging: Track changes to user roles -- Audit logging: Track changes to user roles
SELECT morbac.enable_audit('user_roles'); SELECT morbac.enable_audit('user_roles');
@@ -180,7 +181,7 @@ psql -d morbac_test -f pgmorbac.sql
dropdb morbac_test dropdb morbac_test
``` ```
See [src/README.md](src/README.md) for source code organization. See [DEVELOPMENT.md](docs/DEVELOPMENT.md) for source code organization.
## Documentation ## Documentation
@@ -198,11 +199,12 @@ Authorization decision flow:
1. Collect all roles for user (direct, delegated, derived, hierarchy) 1. Collect all roles for user (direct, delegated, derived, hierarchy)
2. Filter out negative role assignments 2. Filter out negative role assignments
3. Check for prohibitions: if found, deny access 3. Find the highest-priority applicable prohibition and highest-priority applicable permission
4. Check for permissions: if found, allow access 4. If a permission with strictly higher priority than the prohibition exists, allow
5. Default deny 5. If a prohibition exists (and no higher-priority permission), deny
6. If no prohibition, allow if any permission was found; otherwise deny
Prohibitions always override permissions. Prohibitions win over permissions at equal or unset priority.
## Research ## Research
@@ -223,12 +225,12 @@ cd pgmorbac
vim src/authorization.sql vim src/authorization.sql
# Build and test your changes # Build and test your changes
make test # Run test suite make check # Run test suite
# When ready to release # When ready to release
# 1. Update version in pgmorbac.control # 1. Update version in pgmorbac.control
# 2. Build versioned file # 2. Build versioned file
make build # Creates pgmorbac--X.Y.Z.sql make build # Creates pgmorbac.sql from src/
# 3. Tag in git # 3. Tag in git
git tag v1.0.1 git tag v1.0.1
git push --tags git push --tags
+3 -3
View File
@@ -15,9 +15,9 @@ pgmorbac implements the Multi-OrBAC access control model with the following secu
- Explicit permissions must be granted - Explicit permissions must be granted
### Prohibition Precedence ### Prohibition Precedence
- Prohibitions are checked first - Prohibitions and permissions are both evaluated; the higher-priority rule wins
- Prohibitions always override permissions - At equal or unset priority, prohibition overrides permission
- Early return for performance and security - A permission with strictly higher priority than a prohibition will override it
### Organization Isolation ### Organization Isolation
- All policies are scoped to organizations - All policies are scoped to organizations
+6 -6
View File
@@ -30,7 +30,7 @@ vim src/authorization.sql
### Build and Test ### Build and Test
```bash ```bash
make test make check
``` ```
This builds from `src/`, creates a temporary database, runs all tests, and cleans up. This builds from `src/`, creates a temporary database, runs all tests, and cleans up.
@@ -58,7 +58,7 @@ dropdb mytest
1. Update version in `pgmorbac.control` 1. Update version in `pgmorbac.control`
2. Update `CHANGELOG.md` 2. Update `CHANGELOG.md`
3. Run `make build` and `make test` 3. Run `make build` and `make check`
4. Commit and tag: 4. Commit and tag:
```bash ```bash
@@ -71,7 +71,7 @@ git push origin main --tags
```bash ```bash
make build # Build from src/ files make build # Build from src/ files
make test # Build and run tests make check # Build and run tests
make install # Install to PostgreSQL make install # Install to PostgreSQL
make help # Show all targets make help # Show all targets
``` ```
@@ -87,14 +87,14 @@ Functions use `verb_noun` pattern. Tables use plural nouns.
1. Edit files in `src/` 1. Edit files in `src/`
2. Add tests in `tests/` 2. Add tests in `tests/`
3. Update `docs/DOCUMENTATION.md` 3. Update `docs/DOCUMENTATION.md`
4. Run `make test` 4. Run `make check`
5. Commit source files 5. Commit source files
## Debugging ## Debugging
```sql ```sql
SELECT * FROM morbac.auth_cache ORDER BY last_checked DESC LIMIT 10; SELECT * FROM morbac.auth_cache ORDER BY computed_at DESC LIMIT 10;
SELECT morbac.invalidate_auth_cache(); SELECT morbac.invalidate_cache();
EXPLAIN ANALYZE SELECT morbac.is_allowed(...); EXPLAIN ANALYZE SELECT morbac.is_allowed(...);
SELECT morbac.refresh_hierarchy_cache(); SELECT morbac.refresh_hierarchy_cache();
``` ```
+2 -12
View File
@@ -1,8 +1,4 @@
-- ============================================================================= -- Activities are global abstract actions (not org-scoped)
-- ACTIVITIES
-- =============================================================================
-- Activities represent abstract actions in OrBAC
-- These are global abstractions (not org-scoped)
CREATE TABLE morbac.activities ( CREATE TABLE morbac.activities (
name TEXT PRIMARY KEY, name TEXT PRIMARY KEY,
@@ -11,14 +7,8 @@ CREATE TABLE morbac.activities (
); );
COMMENT ON TABLE morbac.activities IS 'Activities - abstract actions in OrBAC model (global)'; COMMENT ON TABLE morbac.activities IS 'Activities - abstract actions in OrBAC model (global)';
COMMENT ON COLUMN morbac.activities.name IS 'Activity name (unique, global)';
-- =============================================================================
-- ACTIVITY HIERARCHY
-- =============================================================================
-- Activities can inherit from other activities
-- e.g., "write" implies "read", "admin_delete" implies "delete"
-- Senior activities imply junior activities (e.g., write implies read)
CREATE TABLE morbac.activity_hierarchy ( CREATE TABLE morbac.activity_hierarchy (
senior_activity TEXT NOT NULL REFERENCES morbac.activities(name) ON DELETE CASCADE, senior_activity TEXT NOT NULL REFERENCES morbac.activities(name) ON DELETE CASCADE,
junior_activity TEXT NOT NULL REFERENCES morbac.activities(name) ON DELETE CASCADE, junior_activity TEXT NOT NULL REFERENCES morbac.activities(name) ON DELETE CASCADE,
+3 -21
View File
@@ -1,8 +1,3 @@
-- =============================================================================
-- ADMIN HELPER FUNCTIONS
-- =============================================================================
-- Helper: Check if user can assign/revoke roles
CREATE OR REPLACE FUNCTION morbac.can_manage_user_role( CREATE OR REPLACE FUNCTION morbac.can_manage_user_role(
p_admin_user_id UUID, p_admin_user_id UUID,
p_org_id UUID, p_org_id UUID,
@@ -15,7 +10,6 @@ AS $$
DECLARE DECLARE
v_role_name TEXT; v_role_name TEXT;
BEGIN BEGIN
-- Get role name
SELECT name INTO v_role_name SELECT name INTO v_role_name
FROM morbac.roles FROM morbac.roles
WHERE id = p_target_role_id AND org_id = p_org_id; WHERE id = p_target_role_id AND org_id = p_org_id;
@@ -24,7 +18,6 @@ BEGIN
RETURN FALSE; RETURN FALSE;
END IF; END IF;
-- Check if admin has permission to manage this role
RETURN morbac.is_admin_allowed( RETURN morbac.is_admin_allowed(
p_admin_user_id, p_admin_user_id,
p_org_id, p_org_id,
@@ -37,7 +30,6 @@ $$;
COMMENT ON FUNCTION morbac.can_manage_user_role(UUID, UUID, UUID) IS COMMENT ON FUNCTION morbac.can_manage_user_role(UUID, UUID, UUID) IS
'Check if user can assign/revoke a specific role in organization'; 'Check if user can assign/revoke a specific role in organization';
-- Helper: Check if user can create/modify/delete roles
CREATE OR REPLACE FUNCTION morbac.can_manage_roles( CREATE OR REPLACE FUNCTION morbac.can_manage_roles(
p_user_id UUID, p_user_id UUID,
p_org_id UUID p_org_id UUID
@@ -59,7 +51,6 @@ $$;
COMMENT ON FUNCTION morbac.can_manage_roles(UUID, UUID) IS COMMENT ON FUNCTION morbac.can_manage_roles(UUID, UUID) IS
'Check if user can create/modify/delete roles in organization'; 'Check if user can create/modify/delete roles in organization';
-- Helper: Check if user can manage policies
CREATE OR REPLACE FUNCTION morbac.can_manage_policies( CREATE OR REPLACE FUNCTION morbac.can_manage_policies(
p_user_id UUID, p_user_id UUID,
p_org_id UUID p_org_id UUID
@@ -81,7 +72,6 @@ $$;
COMMENT ON FUNCTION morbac.can_manage_policies(UUID, UUID) IS COMMENT ON FUNCTION morbac.can_manage_policies(UUID, UUID) IS
'Check if user can manage policies in organization'; 'Check if user can manage policies in organization';
-- Helper: Assign role to user (with permission check)
CREATE OR REPLACE FUNCTION morbac.admin_assign_role( CREATE OR REPLACE FUNCTION morbac.admin_assign_role(
p_admin_user_id UUID, p_admin_user_id UUID,
p_target_user_id UUID, p_target_user_id UUID,
@@ -92,27 +82,23 @@ RETURNS BOOLEAN
LANGUAGE plpgsql LANGUAGE plpgsql
AS $$ AS $$
BEGIN BEGIN
-- Check if admin has permission
IF NOT morbac.can_manage_user_role(p_admin_user_id, p_org_id, p_role_id) THEN IF NOT morbac.can_manage_user_role(p_admin_user_id, p_org_id, p_role_id) THEN
RAISE EXCEPTION 'User % does not have permission to assign role % in org %', RAISE EXCEPTION 'User % does not have permission to assign role % in org %',
p_admin_user_id, p_role_id, p_org_id; p_admin_user_id, p_role_id, p_org_id;
END IF; END IF;
-- Check SoD violations IF morbac.check_sod_violation(p_target_user_id, p_role_id, p_org_id) THEN
IF array_length(morbac.check_sod_violation(p_target_user_id, p_org_id), 1) > 0 THEN
RAISE EXCEPTION 'Role assignment would violate Separation of Duty constraints'; RAISE EXCEPTION 'Role assignment would violate Separation of Duty constraints';
END IF; END IF;
-- Assign role
INSERT INTO morbac.user_roles (user_id, role_id, org_id) INSERT INTO morbac.user_roles (user_id, role_id, org_id)
VALUES (p_target_user_id, p_role_id, p_org_id) VALUES (p_target_user_id, p_role_id, p_org_id)
ON CONFLICT (user_id, role_id, org_id) DO NOTHING; ON CONFLICT (user_id, role_id, org_id) DO NOTHING;
-- Check cardinality after assignment
DECLARE DECLARE
v_cardinality_error TEXT; v_cardinality_error TEXT;
BEGIN BEGIN
v_cardinality_error := morbac.check_cardinality_violation(p_role_id, p_org_id); v_cardinality_error := morbac.check_cardinality_violation(p_role_id);
IF v_cardinality_error IS NOT NULL THEN IF v_cardinality_error IS NOT NULL THEN
RAISE EXCEPTION 'Role assignment violates cardinality constraint: %', v_cardinality_error; RAISE EXCEPTION 'Role assignment violates cardinality constraint: %', v_cardinality_error;
END IF; END IF;
@@ -125,7 +111,6 @@ $$;
COMMENT ON FUNCTION morbac.admin_assign_role(UUID, UUID, UUID, UUID) IS COMMENT ON FUNCTION morbac.admin_assign_role(UUID, UUID, UUID, UUID) IS
'Assign role to user with admin permission check and constraint validation'; 'Assign role to user with admin permission check and constraint validation';
-- Helper: Revoke role from user (with permission check)
CREATE OR REPLACE FUNCTION morbac.admin_revoke_role( CREATE OR REPLACE FUNCTION morbac.admin_revoke_role(
p_admin_user_id UUID, p_admin_user_id UUID,
p_target_user_id UUID, p_target_user_id UUID,
@@ -136,23 +121,20 @@ RETURNS BOOLEAN
LANGUAGE plpgsql LANGUAGE plpgsql
AS $$ AS $$
BEGIN BEGIN
-- Check if admin has permission
IF NOT morbac.can_manage_user_role(p_admin_user_id, p_org_id, p_role_id) THEN IF NOT morbac.can_manage_user_role(p_admin_user_id, p_org_id, p_role_id) THEN
RAISE EXCEPTION 'User % does not have permission to revoke role % in org %', RAISE EXCEPTION 'User % does not have permission to revoke role % in org %',
p_admin_user_id, p_role_id, p_org_id; p_admin_user_id, p_role_id, p_org_id;
END IF; END IF;
-- Revoke role
DELETE FROM morbac.user_roles DELETE FROM morbac.user_roles
WHERE user_id = p_target_user_id WHERE user_id = p_target_user_id
AND role_id = p_role_id AND role_id = p_role_id
AND org_id = p_org_id; AND org_id = p_org_id;
-- Check cardinality after revocation
DECLARE DECLARE
v_cardinality_error TEXT; v_cardinality_error TEXT;
BEGIN BEGIN
v_cardinality_error := morbac.check_cardinality_violation(p_role_id, p_org_id); v_cardinality_error := morbac.check_cardinality_violation(p_role_id, FALSE);
IF v_cardinality_error IS NOT NULL THEN IF v_cardinality_error IS NOT NULL THEN
RAISE WARNING 'Role revocation may violate cardinality constraint: %', v_cardinality_error; RAISE WARNING 'Role revocation may violate cardinality constraint: %', v_cardinality_error;
END IF; END IF;
-3
View File
@@ -1,6 +1,3 @@
-- =============================================================================
-- ADMINISTRATION RULES
-- =============================================================================
-- Meta-policies defining who can create/modify policies (AdministrationPermission) -- Meta-policies defining who can create/modify policies (AdministrationPermission)
CREATE TABLE morbac.admin_rules ( CREATE TABLE morbac.admin_rules (
+2 -14
View File
@@ -1,8 +1,5 @@
-- ============================================================================= -- Optional audit logging for tracking changes to security-critical tables.
-- AUDIT LOG -- Enable/disable per table with morbac.enable_audit() / morbac.disable_audit().
-- =============================================================================
-- Optional audit logging for tracking changes to security-critical tables
-- Enable/disable per table with triggers
CREATE TABLE morbac.audit_log ( CREATE TABLE morbac.audit_log (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
@@ -35,7 +32,6 @@ COMMENT ON COLUMN morbac.audit_log.changed_fields IS 'Array of field names that
COMMENT ON COLUMN morbac.audit_log.session_username IS 'Database session user'; COMMENT ON COLUMN morbac.audit_log.session_username IS 'Database session user';
COMMENT ON COLUMN morbac.audit_log.client_addr IS 'Client IP address'; COMMENT ON COLUMN morbac.audit_log.client_addr IS 'Client IP address';
-- Generic audit trigger function
CREATE OR REPLACE FUNCTION morbac.audit_trigger() CREATE OR REPLACE FUNCTION morbac.audit_trigger()
RETURNS TRIGGER RETURNS TRIGGER
LANGUAGE plpgsql LANGUAGE plpgsql
@@ -48,7 +44,6 @@ DECLARE
v_org_id UUID; v_org_id UUID;
v_record_id UUID; v_record_id UUID;
BEGIN BEGIN
-- Try to get current user/org context
BEGIN BEGIN
v_user_id := morbac.current_user_id(); v_user_id := morbac.current_user_id();
EXCEPTION WHEN OTHERS THEN EXCEPTION WHEN OTHERS THEN
@@ -61,7 +56,6 @@ BEGIN
v_org_id := NULL; v_org_id := NULL;
END; END;
-- Handle different operations
IF TG_OP = 'DELETE' THEN IF TG_OP = 'DELETE' THEN
v_old_data := row_to_json(OLD)::jsonb; v_old_data := row_to_json(OLD)::jsonb;
v_new_data := NULL; v_new_data := NULL;
@@ -75,14 +69,12 @@ BEGIN
v_new_data := row_to_json(NEW)::jsonb; v_new_data := row_to_json(NEW)::jsonb;
v_record_id := (v_new_data->>'id')::uuid; v_record_id := (v_new_data->>'id')::uuid;
-- Identify changed fields
SELECT array_agg(key) SELECT array_agg(key)
INTO v_changed_fields INTO v_changed_fields
FROM jsonb_each(v_old_data) FROM jsonb_each(v_old_data)
WHERE v_old_data->key IS DISTINCT FROM v_new_data->key; WHERE v_old_data->key IS DISTINCT FROM v_new_data->key;
END IF; END IF;
-- Override org_id from record if available
IF v_org_id IS NULL THEN IF v_org_id IS NULL THEN
IF v_new_data ? 'org_id' THEN IF v_new_data ? 'org_id' THEN
v_org_id := (v_new_data->>'org_id')::uuid; v_org_id := (v_new_data->>'org_id')::uuid;
@@ -91,7 +83,6 @@ BEGIN
END IF; END IF;
END IF; END IF;
-- Insert audit record
INSERT INTO morbac.audit_log ( INSERT INTO morbac.audit_log (
user_id, user_id,
org_id, org_id,
@@ -112,7 +103,6 @@ BEGIN
v_changed_fields v_changed_fields
); );
-- Return appropriate value
IF TG_OP = 'DELETE' THEN IF TG_OP = 'DELETE' THEN
RETURN OLD; RETURN OLD;
ELSE ELSE
@@ -124,7 +114,6 @@ $$;
COMMENT ON FUNCTION morbac.audit_trigger() IS COMMENT ON FUNCTION morbac.audit_trigger() IS
'Generic audit trigger function - captures INSERT/UPDATE/DELETE operations'; '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) CREATE OR REPLACE FUNCTION morbac.enable_audit(p_table_name TEXT)
RETURNS VOID RETURNS VOID
LANGUAGE plpgsql LANGUAGE plpgsql
@@ -148,7 +137,6 @@ $$;
COMMENT ON FUNCTION morbac.enable_audit(TEXT) IS COMMENT ON FUNCTION morbac.enable_audit(TEXT) IS
'Enable audit logging on a morbac table - creates audit trigger'; '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) CREATE OR REPLACE FUNCTION morbac.disable_audit(p_table_name TEXT)
RETURNS VOID RETURNS VOID
LANGUAGE plpgsql LANGUAGE plpgsql
+6 -13
View File
@@ -1,8 +1,5 @@
-- ============================================================================= -- Authorization decision cache to avoid repeated expensive computations.
-- PERFORMANCE: AUTHORIZATION CACHE -- TTL is configurable via morbac.config (cache_ttl_seconds).
-- =============================================================================
-- Cache authorization decisions to avoid repeated expensive computations
-- Cache TTL is configurable via morbac.config table (key: cache_ttl_seconds)
CREATE TABLE morbac.auth_cache ( CREATE TABLE morbac.auth_cache (
user_id UUID NOT NULL, user_id UUID NOT NULL,
@@ -21,7 +18,6 @@ CREATE INDEX idx_auth_cache_user_org ON morbac.auth_cache(user_id, org_id);
COMMENT ON TABLE morbac.auth_cache IS COMMENT ON TABLE morbac.auth_cache IS
'Authorization decision cache - expires after 5 minutes or when policies change'; 'Authorization decision cache - expires after 5 minutes or when policies change';
-- Invalidate cache for user/org
CREATE OR REPLACE FUNCTION morbac.invalidate_cache(p_user_id UUID DEFAULT NULL, p_org_id UUID DEFAULT NULL) CREATE OR REPLACE FUNCTION morbac.invalidate_cache(p_user_id UUID DEFAULT NULL, p_org_id UUID DEFAULT NULL)
RETURNS VOID RETURNS VOID
LANGUAGE plpgsql LANGUAGE plpgsql
@@ -42,7 +38,6 @@ $$;
COMMENT ON FUNCTION morbac.invalidate_cache(UUID, UUID) IS COMMENT ON FUNCTION morbac.invalidate_cache(UUID, UUID) IS
'Invalidate auth cache for specific user/org or all entries'; 'Invalidate auth cache for specific user/org or all entries';
-- Auto cleanup expired cache entries
CREATE OR REPLACE FUNCTION morbac.cleanup_auth_cache() CREATE OR REPLACE FUNCTION morbac.cleanup_auth_cache()
RETURNS INTEGER RETURNS INTEGER
LANGUAGE plpgsql LANGUAGE plpgsql
@@ -59,7 +54,8 @@ $$;
COMMENT ON FUNCTION morbac.cleanup_auth_cache() IS COMMENT ON FUNCTION morbac.cleanup_auth_cache() IS
'Remove expired cache entries - call periodically via cron'; 'Remove expired cache entries - call periodically via cron';
-- Trigger to invalidate cache on rule changes -- Invalidate cache on rule or role changes
CREATE OR REPLACE FUNCTION morbac.invalidate_cache_on_change() CREATE OR REPLACE FUNCTION morbac.invalidate_cache_on_change()
RETURNS TRIGGER RETURNS TRIGGER
LANGUAGE plpgsql LANGUAGE plpgsql
@@ -67,7 +63,6 @@ AS $$
DECLARE DECLARE
v_org_id UUID; v_org_id UUID;
BEGIN BEGIN
-- Only clear cache if org_id exists in NEW or OLD
IF TG_OP = 'DELETE' THEN IF TG_OP = 'DELETE' THEN
BEGIN BEGIN
v_org_id := OLD.org_id; v_org_id := OLD.org_id;
@@ -88,7 +83,6 @@ BEGIN
END; END;
$$; $$;
-- Attach cache invalidation triggers
CREATE TRIGGER trg_invalidate_cache_rules CREATE TRIGGER trg_invalidate_cache_rules
AFTER INSERT OR UPDATE OR DELETE ON morbac.rules AFTER INSERT OR UPDATE OR DELETE ON morbac.rules
FOR EACH ROW EXECUTE FUNCTION morbac.invalidate_cache_on_change(); FOR EACH ROW EXECUTE FUNCTION morbac.invalidate_cache_on_change();
@@ -105,15 +99,14 @@ CREATE TRIGGER trg_invalidate_cache_cross_org
AFTER INSERT OR UPDATE OR DELETE ON morbac.cross_org_rules AFTER INSERT OR UPDATE OR DELETE ON morbac.cross_org_rules
FOR EACH ROW EXECUTE FUNCTION morbac.invalidate_cache_on_change(); FOR EACH ROW EXECUTE FUNCTION morbac.invalidate_cache_on_change();
-- Trigger to refresh hierarchies when they 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()
RETURNS TRIGGER RETURNS TRIGGER
LANGUAGE plpgsql LANGUAGE plpgsql
AS $$ AS $$
BEGIN BEGIN
-- Refresh in background (this will block briefly but necessary)
PERFORM morbac.refresh_hierarchy_cache(); PERFORM morbac.refresh_hierarchy_cache();
-- Also invalidate auth cache since hierarchies affect authorization
DELETE FROM morbac.auth_cache; DELETE FROM morbac.auth_cache;
RETURN COALESCE(NEW, OLD); RETURN COALESCE(NEW, OLD);
END; END;
+6 -16
View File
@@ -1,19 +1,15 @@
-- ============================================================================= -- Authorization decision function implementing canonical OrBAC semantics.
-- AUTHORIZATION DECISION FUNCTION (CRITICAL)
-- =============================================================================
-- Implements the canonical OrBAC authorization decision
-- --
-- Semantics (from Multi-OrBAC paper): -- Access is allowed if and only if:
-- - Access is allowed if and only if:
-- 1. At least one applicable permission exists -- 1. At least one applicable permission exists
-- 2. AND no applicable prohibition exists (or permission has strictly higher priority) -- 2. AND no applicable prohibition exists (or permission has strictly higher priority)
-- - Default deny (no permission = deny) -- Default deny (no permission = deny).
-- - Obligations and recommendations do NOT affect authorization -- Obligations and recommendations do NOT affect authorization.
-- --
-- Priority resolution: -- Priority resolution:
-- - Each rule has an optional integer priority (NULL = 0, lowest) -- - Each rule has an optional integer priority (NULL = 0, lowest)
-- - When both a prohibition and a permission apply, the higher-priority rule wins -- - When both a prohibition and a permission apply, the higher-priority rule wins
-- - Tie goes to prohibition (modality precedence from the paper) -- - Tie goes to prohibition (modality precedence from the Multi-OrBAC paper)
CREATE OR REPLACE FUNCTION morbac.is_allowed_nocache( CREATE OR REPLACE FUNCTION morbac.is_allowed_nocache(
p_user_id UUID, p_user_id UUID,
@@ -44,7 +40,7 @@ BEGIN
LOOP LOOP
IF morbac.eval_context(v_rule.context_id) THEN IF morbac.eval_context(v_rule.context_id) THEN
v_max_prohibition_priority := v_rule.prio; v_max_prohibition_priority := v_rule.prio;
EXIT; -- Highest-priority prohibition found; lower ones can't change the outcome EXIT;
END IF; END IF;
END LOOP; END LOOP;
@@ -107,18 +103,15 @@ BEGIN
END LOOP; END LOOP;
-- STEP 5: Priority resolution -- STEP 5: Priority resolution
-- No prohibition at all: allow if any permission was found
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;
END IF; END IF;
-- Prohibition exists: a permission with strictly higher priority overrides it
IF v_max_permission_priority IS NOT NULL IF v_max_permission_priority IS NOT NULL
AND v_max_permission_priority > v_max_prohibition_priority THEN AND v_max_permission_priority > v_max_prohibition_priority THEN
RETURN TRUE; RETURN TRUE;
END IF; END IF;
-- Prohibition wins (no permission, equal priority, or lower-priority permission)
RETURN FALSE; RETURN FALSE;
END; END;
$$; $$;
@@ -142,7 +135,6 @@ DECLARE
v_computed_result BOOLEAN; v_computed_result BOOLEAN;
v_expires_at TIMESTAMPTZ; v_expires_at TIMESTAMPTZ;
BEGIN BEGIN
-- Try cache first
SELECT allowed, expires_at INTO v_cached_result, v_expires_at SELECT allowed, expires_at INTO v_cached_result, v_expires_at
FROM morbac.auth_cache FROM morbac.auth_cache
WHERE user_id = p_user_id WHERE user_id = p_user_id
@@ -155,10 +147,8 @@ BEGIN
RETURN v_cached_result; RETURN v_cached_result;
END IF; END IF;
-- Cache miss - compute authorization
v_computed_result := morbac.is_allowed_nocache(p_user_id, p_org_id, p_activity, p_view); v_computed_result := morbac.is_allowed_nocache(p_user_id, p_org_id, p_activity, p_view);
-- Store in cache (TTL from config)
INSERT INTO morbac.auth_cache (user_id, org_id, activity, view, allowed, expires_at) INSERT INTO morbac.auth_cache (user_id, org_id, activity, view, allowed, expires_at)
VALUES ( VALUES (
p_user_id, p_user_id,
-9
View File
@@ -1,9 +1,3 @@
-- =============================================================================
-- CONFIGURATION
-- =============================================================================
-- Centralized configuration for pgmorbac extension
-- Edit these values to customize behavior
CREATE TABLE morbac.config ( CREATE TABLE morbac.config (
key TEXT PRIMARY KEY, key TEXT PRIMARY KEY,
value TEXT NOT NULL, value TEXT NOT NULL,
@@ -13,13 +7,11 @@ CREATE TABLE morbac.config (
COMMENT ON TABLE morbac.config IS 'Extension configuration - edit values to customize behavior'; COMMENT ON TABLE morbac.config IS 'Extension configuration - edit values to customize behavior';
-- Insert default configuration values
INSERT INTO morbac.config (key, value, description) VALUES INSERT INTO morbac.config (key, value, description) VALUES
('cache_ttl_seconds', '300', 'Authorization cache time-to-live in seconds (default: 5 minutes)'), ('cache_ttl_seconds', '300', 'Authorization cache time-to-live in seconds (default: 5 minutes)'),
('hierarchy_max_depth', '10', 'Maximum depth for hierarchy traversal to prevent infinite loops'), ('hierarchy_max_depth', '10', 'Maximum depth for hierarchy traversal to prevent infinite loops'),
('enable_audit_by_default', 'false', 'Whether to enable audit logging by default on installation'); ('enable_audit_by_default', 'false', 'Whether to enable audit logging by default on installation');
-- Helper function to get configuration values
CREATE OR REPLACE FUNCTION morbac.get_config(p_key TEXT) CREATE OR REPLACE FUNCTION morbac.get_config(p_key TEXT)
RETURNS TEXT RETURNS TEXT
LANGUAGE plpgsql LANGUAGE plpgsql
@@ -39,7 +31,6 @@ $$;
COMMENT ON FUNCTION morbac.get_config(TEXT) IS COMMENT ON FUNCTION morbac.get_config(TEXT) IS
'Get configuration value by key'; 'Get configuration value by key';
-- Helper function to set configuration values
CREATE OR REPLACE FUNCTION morbac.set_config(p_key TEXT, p_value TEXT) CREATE OR REPLACE FUNCTION morbac.set_config(p_key TEXT, p_value TEXT)
RETURNS VOID RETURNS VOID
LANGUAGE plpgsql LANGUAGE plpgsql
+2 -17
View File
@@ -1,8 +1,4 @@
-- ============================================================================= -- Contexts represent conditions under which rules apply, implemented as callable predicates
-- CONTEXTS
-- =============================================================================
-- Contexts represent conditions under which rules apply
-- Implemented as callable predicates (functions)
CREATE TABLE morbac.contexts ( CREATE TABLE morbac.contexts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -15,13 +11,9 @@ CREATE TABLE morbac.contexts (
CREATE INDEX idx_contexts_name ON morbac.contexts(name); CREATE INDEX idx_contexts_name ON morbac.contexts(name);
COMMENT ON TABLE morbac.contexts IS 'Contexts - conditions under which rules apply (callable predicates)'; COMMENT ON TABLE morbac.contexts IS 'Contexts - conditions under which rules apply (callable predicates)';
COMMENT ON COLUMN morbac.contexts.name IS 'Context name (unique)';
COMMENT ON COLUMN morbac.contexts.evaluator IS 'Function that evaluates this context (returns boolean)'; COMMENT ON COLUMN morbac.contexts.evaluator IS 'Function that evaluates this context (returns boolean)';
-- ============================================================================= -- Default context that always evaluates to true
-- DEFAULT CONTEXT: ALWAYS
-- =============================================================================
-- Create a default context that always evaluates to true
CREATE OR REPLACE FUNCTION morbac.context_always() CREATE OR REPLACE FUNCTION morbac.context_always()
RETURNS BOOLEAN RETURNS BOOLEAN
@@ -35,17 +27,12 @@ $$;
COMMENT ON FUNCTION morbac.context_always() IS 'Default context evaluator - always returns true'; COMMENT ON FUNCTION morbac.context_always() IS 'Default context evaluator - always returns true';
-- Insert the default 'always' context
INSERT INTO morbac.contexts (name, description, evaluator) INSERT INTO morbac.contexts (name, description, evaluator)
VALUES ( VALUES (
'always', 'always',
'Default context - always evaluates to true', 'Default context - always evaluates to true',
'morbac.context_always'::regproc 'morbac.context_always'::regproc
); );
-- =============================================================================
-- CONTEXT EVALUATION HELPER
-- =============================================================================
-- Evaluates a context by calling its evaluator function
CREATE OR REPLACE FUNCTION morbac.eval_context(p_context_id UUID) CREATE OR REPLACE FUNCTION morbac.eval_context(p_context_id UUID)
RETURNS BOOLEAN RETURNS BOOLEAN
@@ -56,7 +43,6 @@ DECLARE
v_evaluator REGPROC; v_evaluator REGPROC;
v_result BOOLEAN; v_result BOOLEAN;
BEGIN BEGIN
-- Get the evaluator function for this context
SELECT evaluator INTO v_evaluator SELECT evaluator INTO v_evaluator
FROM morbac.contexts FROM morbac.contexts
WHERE id = p_context_id; WHERE id = p_context_id;
@@ -65,7 +51,6 @@ BEGIN
RAISE EXCEPTION 'Context % not found', p_context_id; RAISE EXCEPTION 'Context % not found', p_context_id;
END IF; END IF;
-- Execute the evaluator function
EXECUTE format('SELECT %s()', v_evaluator::text) INTO v_result; EXECUTE format('SELECT %s()', v_evaluator::text) INTO v_result;
RETURN COALESCE(v_result, FALSE); RETURN COALESCE(v_result, FALSE);
+1 -7
View File
@@ -1,8 +1,4 @@
-- ============================================================================= -- Rules that grant access across organization boundaries
-- INTER-ORGANIZATIONAL RULES
-- =============================================================================
-- Rules that apply across organizations (cross-org access)
-- Allows users from one org to access resources in another org
CREATE TABLE morbac.cross_org_rules ( CREATE TABLE morbac.cross_org_rules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -30,5 +26,3 @@ CREATE INDEX idx_cross_org_rules_temporal ON morbac.cross_org_rules(valid_from,
COMMENT ON TABLE morbac.cross_org_rules IS 'Inter-organizational rules for cross-org access'; 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.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.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';
+4 -22
View File
@@ -1,28 +1,18 @@
-- ============================================================================= -- Multi-org resource pattern (guidance):
-- UTILITY RESOURCE PATTERN (GUIDANCE)
-- =============================================================================
-- --
-- For resources that belong to multiple organizations: -- For resources that belong to multiple organizations:
-- --
-- Create resource table (org-neutral): -- CREATE TABLE app.documents (id UUID PRIMARY KEY, content TEXT, ...);
-- CREATE TABLE app.documents (
-- id UUID PRIMARY KEY,
-- content TEXT,
-- ...
-- );
-- --
-- Create organization membership table:
-- CREATE TABLE app.document_orgs ( -- CREATE TABLE app.document_orgs (
-- document_id UUID REFERENCES app.documents(id), -- document_id UUID REFERENCES app.documents(id),
-- org_id UUID REFERENCES morbac.orgs(id), -- org_id UUID REFERENCES morbac.orgs(id),
-- PRIMARY KEY (document_id, org_id) -- PRIMARY KEY (document_id, org_id)
-- ); -- );
-- --
-- Apply RLS with multi-org support:
-- ALTER TABLE app.documents ENABLE ROW LEVEL SECURITY; -- ALTER TABLE app.documents ENABLE ROW LEVEL SECURITY;
-- --
-- CREATE POLICY document_select ON app.documents -- CREATE POLICY document_select ON app.documents FOR SELECT
-- FOR SELECT
-- USING ( -- USING (
-- EXISTS ( -- EXISTS (
-- SELECT 1 FROM app.document_orgs do -- SELECT 1 FROM app.document_orgs do
@@ -32,18 +22,10 @@
-- AND morbac.rls_check('read', 'documents') -- AND morbac.rls_check('read', 'documents')
-- ); -- );
-- --
-- This pattern allows a resource to be visible in multiple organizations -- This allows a resource to be visible in multiple organizations
-- while enforcing OrBAC policy within each organization context. -- while enforcing OrBAC policy within each organization context.
--
-- =============================================================================
-- =============================================================================
-- INSTALLATION COMPLETE
-- =============================================================================
-- Grant usage on schema to public (adjust based on your security requirements) -- Grant usage on schema to public (adjust based on your security requirements)
-- GRANT USAGE ON SCHEMA morbac TO public; -- GRANT USAGE ON SCHEMA morbac TO public;
-- GRANT SELECT ON ALL TABLES IN SCHEMA morbac TO public; -- GRANT SELECT ON ALL TABLES IN SCHEMA morbac TO public;
-- GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA morbac TO public; -- GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA morbac TO public;
-- For production, create specific roles and grant appropriate privileges
+5 -14
View File
@@ -1,14 +1,5 @@
-- ============================================================================= -- pgmorbac: Multi-OrBAC for PostgreSQL
-- pgmorbac Extension -- Organization-Based Access Control with multi-organization support,
-- ============================================================================= -- role/activity/view hierarchies, deontic modalities, and temporal constraints.
-- Multi-OrBAC: Organization-Based Access Control with Multi-Organization Support -- Based on the CNRS Multi-OrBAC research model.
-- Based on the CNRS research paper on Multi-OrBAC model
--
-- This extension implements:
-- - Role-Based Access Control (RBAC) with organizational scoping
-- - Hierarchical organizations, roles, activities, and views
-- - Permission, Prohibition, Obligation, and Recommendation deontic modalities
-- - Temporal constraints on rules
-- - Contextual access control
-- - Role delegation with time bounds
-- =============================================================================
+2 -56
View File
@@ -1,9 +1,5 @@
-- ============================================================================= -- Transitive closure and effective role/activity/view lookup functions
-- HIERARCHY FUNCTIONS
-- =============================================================================
-- Functions to compute transitive closures for organization and role hierarchies
-- Get all ancestor organizations (including self)
CREATE OR REPLACE FUNCTION morbac.get_org_ancestors(p_org_id UUID) CREATE OR REPLACE FUNCTION morbac.get_org_ancestors(p_org_id UUID)
RETURNS TABLE(org_id UUID, depth INTEGER) RETURNS TABLE(org_id UUID, depth INTEGER)
LANGUAGE plpgsql LANGUAGE plpgsql
@@ -12,10 +8,8 @@ AS $$
BEGIN BEGIN
RETURN QUERY RETURN QUERY
WITH RECURSIVE org_ancestors AS ( WITH RECURSIVE org_ancestors AS (
-- Base case: the organization itself
SELECT p_org_id AS org_id, 0 AS depth SELECT p_org_id AS org_id, 0 AS depth
UNION UNION
-- Recursive case: parent organizations
SELECT o.parent_id, oa.depth + 1 SELECT o.parent_id, oa.depth + 1
FROM org_ancestors oa FROM org_ancestors oa
INNER JOIN morbac.orgs o ON o.id = oa.org_id INNER JOIN morbac.orgs o ON o.id = oa.org_id
@@ -28,7 +22,6 @@ $$;
COMMENT ON FUNCTION morbac.get_org_ancestors(UUID) IS COMMENT ON FUNCTION morbac.get_org_ancestors(UUID) IS
'Returns all ancestor organizations (including self) with depth in hierarchy'; 'Returns all ancestor organizations (including self) with depth in hierarchy';
-- Get all descendant organizations (including self)
CREATE OR REPLACE FUNCTION morbac.get_org_descendants(p_org_id UUID) CREATE OR REPLACE FUNCTION morbac.get_org_descendants(p_org_id UUID)
RETURNS TABLE(org_id UUID, depth INTEGER) RETURNS TABLE(org_id UUID, depth INTEGER)
LANGUAGE plpgsql LANGUAGE plpgsql
@@ -37,10 +30,8 @@ AS $$
BEGIN BEGIN
RETURN QUERY RETURN QUERY
WITH RECURSIVE org_descendants AS ( WITH RECURSIVE org_descendants AS (
-- Base case: the organization itself
SELECT p_org_id AS org_id, 0 AS depth SELECT p_org_id AS org_id, 0 AS depth
UNION UNION
-- Recursive case: child organizations
SELECT o.id, od.depth + 1 SELECT o.id, od.depth + 1
FROM org_descendants od FROM org_descendants od
INNER JOIN morbac.orgs o ON o.parent_id = od.org_id INNER JOIN morbac.orgs o ON o.parent_id = od.org_id
@@ -52,7 +43,7 @@ $$;
COMMENT ON FUNCTION morbac.get_org_descendants(UUID) IS COMMENT ON FUNCTION morbac.get_org_descendants(UUID) IS
'Returns all descendant organizations (including self) with depth in hierarchy'; 'Returns all descendant organizations (including self) with depth in hierarchy';
-- Get a named scope of organizations relative to a given org -- Get a named scope of organizations relative to a given org.
-- --
-- Supported scopes: -- Supported scopes:
-- 'self' — the org itself only (depth = 0) -- 'self' — the org itself only (depth = 0)
@@ -65,8 +56,6 @@ COMMENT ON FUNCTION morbac.get_org_descendants(UUID) IS
-- 'root' — topmost ancestor only (max depth ancestor) -- 'root' — topmost ancestor only (max depth ancestor)
-- --
-- Optional p_max_depth limits how many levels are traversed (NULL = unlimited). -- Optional p_max_depth limits how many levels are traversed (NULL = unlimited).
-- For upward scopes (parent/ancestors/lineage/root) depth counts steps toward root.
-- For downward scopes (children/descendants/subtree) depth counts steps toward leaves.
CREATE OR REPLACE FUNCTION morbac.get_org_scope( CREATE OR REPLACE FUNCTION morbac.get_org_scope(
p_org_id UUID, p_org_id UUID,
p_scope TEXT, p_scope TEXT,
@@ -138,7 +127,6 @@ $$;
COMMENT ON FUNCTION morbac.get_org_scope(UUID, TEXT, INTEGER) IS COMMENT ON FUNCTION morbac.get_org_scope(UUID, TEXT, INTEGER) IS
'Returns a named set of organizations relative to p_org_id. Scopes: self, children, descendants, subtree, parent, ancestors, lineage, root. Optional p_max_depth limits traversal depth.'; 'Returns a named set of organizations relative to p_org_id. Scopes: self, children, descendants, subtree, parent, ancestors, lineage, root. Optional p_max_depth limits traversal depth.';
-- Get all roles a user effectively has (direct + inherited via role hierarchy)
CREATE OR REPLACE FUNCTION morbac.get_effective_roles(p_user_id UUID, p_org_id UUID) CREATE OR REPLACE FUNCTION morbac.get_effective_roles(p_user_id UUID, p_org_id UUID)
RETURNS TABLE(role_id UUID, depth INTEGER) RETURNS TABLE(role_id UUID, depth INTEGER)
LANGUAGE plpgsql LANGUAGE plpgsql
@@ -147,13 +135,11 @@ AS $$
BEGIN BEGIN
RETURN QUERY RETURN QUERY
WITH RECURSIVE effective_roles AS ( WITH RECURSIVE effective_roles AS (
-- Base case: directly assigned roles
SELECT ur.role_id, 0 AS depth SELECT ur.role_id, 0 AS depth
FROM morbac.user_roles ur FROM morbac.user_roles ur
WHERE ur.user_id = p_user_id WHERE ur.user_id = p_user_id
AND ur.org_id = p_org_id AND ur.org_id = p_org_id
UNION UNION
-- Recursive case: junior roles (inherited by the user's assigned roles)
SELECT rh.junior_role_id, er.depth + 1 SELECT rh.junior_role_id, er.depth + 1
FROM effective_roles er FROM effective_roles er
INNER JOIN morbac.role_hierarchy rh ON rh.senior_role_id = er.role_id INNER JOIN morbac.role_hierarchy rh ON rh.senior_role_id = er.role_id
@@ -167,7 +153,6 @@ $$;
COMMENT ON FUNCTION morbac.get_effective_roles(UUID, UUID) IS COMMENT ON FUNCTION morbac.get_effective_roles(UUID, UUID) IS
'Returns all effective roles for a user in an organization (direct + inherited via role hierarchy)'; 'Returns all effective roles for a user in an organization (direct + inherited via role hierarchy)';
-- Get all roles inherited by a role (transitive closure)
CREATE OR REPLACE FUNCTION morbac.get_inherited_roles(p_role_id UUID) CREATE OR REPLACE FUNCTION morbac.get_inherited_roles(p_role_id UUID)
RETURNS TABLE(role_id UUID, depth INTEGER) RETURNS TABLE(role_id UUID, depth INTEGER)
LANGUAGE plpgsql LANGUAGE plpgsql
@@ -176,10 +161,8 @@ AS $$
BEGIN BEGIN
RETURN QUERY RETURN QUERY
WITH RECURSIVE inherited_roles AS ( WITH RECURSIVE inherited_roles AS (
-- Base case: the role itself
SELECT p_role_id AS role_id, 0 AS depth SELECT p_role_id AS role_id, 0 AS depth
UNION UNION
-- Recursive case: junior roles
SELECT rh.junior_role_id, ir.depth + 1 SELECT rh.junior_role_id, ir.depth + 1
FROM inherited_roles ir FROM inherited_roles ir
INNER JOIN morbac.role_hierarchy rh ON rh.senior_role_id = ir.role_id INNER JOIN morbac.role_hierarchy rh ON rh.senior_role_id = ir.role_id
@@ -193,7 +176,6 @@ $$;
COMMENT ON FUNCTION morbac.get_inherited_roles(UUID) IS COMMENT ON FUNCTION morbac.get_inherited_roles(UUID) IS
'Returns all roles inherited by a role (transitive closure via role hierarchy)'; 'Returns all roles inherited by a role (transitive closure via role hierarchy)';
-- Get all effective activities (including inherited via activity hierarchy)
CREATE OR REPLACE FUNCTION morbac.get_effective_activities(p_activity TEXT) CREATE OR REPLACE FUNCTION morbac.get_effective_activities(p_activity TEXT)
RETURNS TABLE(activity TEXT, depth INTEGER) RETURNS TABLE(activity TEXT, depth INTEGER)
LANGUAGE plpgsql LANGUAGE plpgsql
@@ -202,10 +184,8 @@ AS $$
BEGIN BEGIN
RETURN QUERY RETURN QUERY
WITH RECURSIVE effective_activities AS ( WITH RECURSIVE effective_activities AS (
-- Base case: the activity itself
SELECT p_activity AS activity, 0 AS depth SELECT p_activity AS activity, 0 AS depth
UNION UNION
-- Recursive case: junior activities (implied activities)
SELECT ah.junior_activity, ea.depth + 1 SELECT ah.junior_activity, ea.depth + 1
FROM effective_activities ea FROM effective_activities ea
INNER JOIN morbac.activity_hierarchy ah ON ah.senior_activity = ea.activity INNER JOIN morbac.activity_hierarchy ah ON ah.senior_activity = ea.activity
@@ -219,7 +199,6 @@ $$;
COMMENT ON FUNCTION morbac.get_effective_activities(TEXT) IS COMMENT ON FUNCTION morbac.get_effective_activities(TEXT) IS
'Returns all activities including those implied via activity hierarchy (e.g., write implies read)'; 'Returns all activities including those implied via activity hierarchy (e.g., write implies read)';
-- Get all effective views (including inherited via view hierarchy)
CREATE OR REPLACE FUNCTION morbac.get_effective_views(p_view TEXT) CREATE OR REPLACE FUNCTION morbac.get_effective_views(p_view TEXT)
RETURNS TABLE(view TEXT, depth INTEGER) RETURNS TABLE(view TEXT, depth INTEGER)
LANGUAGE plpgsql LANGUAGE plpgsql
@@ -228,10 +207,8 @@ AS $$
BEGIN BEGIN
RETURN QUERY RETURN QUERY
WITH RECURSIVE effective_views AS ( WITH RECURSIVE effective_views AS (
-- Base case: the view itself
SELECT p_view AS view, 0 AS depth SELECT p_view AS view, 0 AS depth
UNION UNION
-- Recursive case: junior views (more general categories)
SELECT vh.junior_view, ev.depth + 1 SELECT vh.junior_view, ev.depth + 1
FROM effective_views ev FROM effective_views ev
INNER JOIN morbac.view_hierarchy vh ON vh.senior_view = ev.view INNER JOIN morbac.view_hierarchy vh ON vh.senior_view = ev.view
@@ -245,7 +222,6 @@ $$;
COMMENT ON FUNCTION morbac.get_effective_views(TEXT) IS COMMENT ON FUNCTION morbac.get_effective_views(TEXT) IS
'Returns all views including parent categories via view hierarchy'; 'Returns all views including parent categories via view hierarchy';
-- Get comprehensive effective roles including delegation and derived roles
CREATE OR REPLACE FUNCTION morbac.get_comprehensive_roles(p_user_id UUID, p_org_id UUID) CREATE OR REPLACE FUNCTION morbac.get_comprehensive_roles(p_user_id UUID, p_org_id UUID)
RETURNS TABLE(role_id UUID, source TEXT, depth INTEGER) RETURNS TABLE(role_id UUID, source TEXT, depth INTEGER)
LANGUAGE plpgsql LANGUAGE plpgsql
@@ -259,7 +235,6 @@ BEGIN
FROM morbac.user_roles ur FROM morbac.user_roles ur
WHERE ur.user_id = p_user_id WHERE ur.user_id = p_user_id
AND ur.org_id = p_org_id AND ur.org_id = p_org_id
-- Check not negatively assigned
AND NOT EXISTS ( AND NOT EXISTS (
SELECT 1 FROM morbac.negative_role_assignments nra SELECT 1 FROM morbac.negative_role_assignments nra
WHERE nra.user_id = p_user_id WHERE nra.user_id = p_user_id
@@ -276,14 +251,12 @@ BEGIN
AND d.org_id = p_org_id AND d.org_id = p_org_id
AND NOT d.revoked AND NOT d.revoked
AND now() BETWEEN d.valid_from AND d.valid_until AND now() BETWEEN d.valid_from AND d.valid_until
-- Check delegator has the role
AND EXISTS ( AND EXISTS (
SELECT 1 FROM morbac.user_roles ur SELECT 1 FROM morbac.user_roles ur
WHERE ur.user_id = d.delegator_id WHERE ur.user_id = d.delegator_id
AND ur.role_id = d.role_id AND ur.role_id = d.role_id
AND ur.org_id = d.org_id AND ur.org_id = d.org_id
) )
-- Check not negatively assigned to delegatee
AND NOT EXISTS ( AND NOT EXISTS (
SELECT 1 FROM morbac.negative_role_assignments nra SELECT 1 FROM morbac.negative_role_assignments nra
WHERE nra.user_id = p_user_id WHERE nra.user_id = p_user_id
@@ -322,7 +295,6 @@ $$;
COMMENT ON FUNCTION morbac.get_comprehensive_roles(UUID, UUID) IS COMMENT ON FUNCTION morbac.get_comprehensive_roles(UUID, UUID) IS
'Returns all effective roles including direct, delegated, derived, and inherited via hierarchy'; 'Returns all effective roles including direct, delegated, derived, and inherited via hierarchy';
-- Helper to evaluate derived role conditions
CREATE OR REPLACE FUNCTION morbac.eval_derived_role( CREATE OR REPLACE FUNCTION morbac.eval_derived_role(
p_evaluator REGPROC, p_evaluator REGPROC,
p_user_id UUID, p_user_id UUID,
@@ -345,29 +317,3 @@ $$;
COMMENT ON FUNCTION morbac.eval_derived_role(REGPROC, UUID, UUID) IS COMMENT ON FUNCTION morbac.eval_derived_role(REGPROC, UUID, UUID) IS
'Evaluates a derived role condition function'; 'Evaluates a derived role condition function';
-- Get all roles inherited by a role (transitive closure)
CREATE OR REPLACE FUNCTION morbac.get_inherited_roles(p_role_id UUID)
RETURNS TABLE(role_id UUID, depth INTEGER)
LANGUAGE plpgsql
STABLE
AS $$
BEGIN
RETURN QUERY
WITH RECURSIVE inherited_roles AS (
-- Base case: the role itself
SELECT p_role_id AS role_id, 0 AS depth
UNION
-- Recursive case: junior roles
SELECT rh.junior_role_id, ir.depth + 1
FROM inherited_roles ir
INNER JOIN morbac.role_hierarchy rh ON rh.senior_role_id = ir.role_id
)
SELECT DISTINCT ON (inherited_roles.role_id) inherited_roles.role_id, inherited_roles.depth
FROM inherited_roles
ORDER BY inherited_roles.role_id, inherited_roles.depth;
END;
$$;
COMMENT ON FUNCTION morbac.get_inherited_roles(UUID) IS
'Returns all roles inherited by a role (transitive closure via role hierarchy)';
+1 -9
View File
@@ -1,9 +1,5 @@
-- ============================================================================= -- Precomputed transitive closures for all hierarchies to avoid recursive CTEs on each request
-- PERFORMANCE: MATERIALIZED HIERARCHY VIEWS
-- =============================================================================
-- Precomputed transitive closures for hierarchies to avoid recursive CTEs on every request
-- Precomputed role hierarchy transitive closure
CREATE MATERIALIZED VIEW morbac.mv_role_closure AS CREATE MATERIALIZED VIEW morbac.mv_role_closure AS
WITH RECURSIVE role_closure AS ( WITH RECURSIVE role_closure AS (
SELECT id as senior_role_id, id as junior_role_id, 0 as depth SELECT id as senior_role_id, id as junior_role_id, 0 as depth
@@ -27,7 +23,6 @@ CREATE INDEX idx_mv_role_closure_junior ON morbac.mv_role_closure(junior_role_id
COMMENT ON MATERIALIZED VIEW morbac.mv_role_closure IS COMMENT ON MATERIALIZED VIEW morbac.mv_role_closure IS
'Precomputed role hierarchy transitive closure - refresh after role hierarchy changes'; 'Precomputed role hierarchy transitive closure - refresh after role hierarchy changes';
-- Precomputed org hierarchy transitive closure
CREATE MATERIALIZED VIEW morbac.mv_org_closure AS CREATE MATERIALIZED VIEW morbac.mv_org_closure AS
WITH RECURSIVE org_closure AS ( WITH RECURSIVE org_closure AS (
SELECT id as descendant_id, id as ancestor_id, 0 as depth SELECT id as descendant_id, id as ancestor_id, 0 as depth
@@ -48,7 +43,6 @@ CREATE INDEX idx_mv_org_closure_ancestor ON morbac.mv_org_closure(ancestor_id);
COMMENT ON MATERIALIZED VIEW morbac.mv_org_closure IS COMMENT ON MATERIALIZED VIEW morbac.mv_org_closure IS
'Precomputed organization hierarchy transitive closure - refresh after org hierarchy changes'; 'Precomputed organization hierarchy transitive closure - refresh after org hierarchy changes';
-- Precomputed activity hierarchy
CREATE MATERIALIZED VIEW morbac.mv_activity_closure AS CREATE MATERIALIZED VIEW morbac.mv_activity_closure AS
WITH RECURSIVE activity_closure AS ( WITH RECURSIVE activity_closure AS (
SELECT name as senior_activity, name as junior_activity, 0 as depth SELECT name as senior_activity, name as junior_activity, 0 as depth
@@ -72,7 +66,6 @@ CREATE INDEX idx_mv_activity_closure_junior ON morbac.mv_activity_closure(junior
COMMENT ON MATERIALIZED VIEW morbac.mv_activity_closure IS COMMENT ON MATERIALIZED VIEW morbac.mv_activity_closure IS
'Precomputed activity hierarchy transitive closure - refresh after activity hierarchy changes'; 'Precomputed activity hierarchy transitive closure - refresh after activity hierarchy changes';
-- Precomputed view hierarchy
CREATE MATERIALIZED VIEW morbac.mv_view_closure AS CREATE MATERIALIZED VIEW morbac.mv_view_closure AS
WITH RECURSIVE view_closure AS ( WITH RECURSIVE view_closure AS (
SELECT name as senior_view, name as junior_view, 0 as depth SELECT name as senior_view, name as junior_view, 0 as depth
@@ -96,7 +89,6 @@ CREATE INDEX idx_mv_view_closure_junior ON morbac.mv_view_closure(junior_view);
COMMENT ON MATERIALIZED VIEW morbac.mv_view_closure IS COMMENT ON MATERIALIZED VIEW morbac.mv_view_closure IS
'Precomputed view hierarchy transitive closure - refresh after view hierarchy changes'; 'Precomputed view hierarchy transitive closure - refresh after view hierarchy changes';
-- Helper function to refresh all materialized views
CREATE OR REPLACE FUNCTION morbac.refresh_hierarchy_cache() CREATE OR REPLACE FUNCTION morbac.refresh_hierarchy_cache()
RETURNS VOID RETURNS VOID
LANGUAGE plpgsql LANGUAGE plpgsql
+4 -12
View File
@@ -1,16 +1,11 @@
-- ============================================================================= -- Obligations and recommendations do NOT affect authorization decisions.
-- OBLIGATIONS AND RECOMMENDATIONS -- They are queryable for informational purposes (task lists, compliance prompts, etc.).
-- =============================================================================
-- Obligations and recommendations do NOT affect authorization
-- They are queryable for informational purposes
-- --
-- Conflict resolution (from Multi-OrBAC paper): -- Conflict resolution (from Multi-OrBAC paper):
-- - A prohibition voids any applicable obligation for the same (activity, view) -- - A prohibition voids any applicable obligation for the same (activity, view)
-- - A prohibition or obligation voids any applicable recommendation for the same (activity, view) -- - A prohibition or obligation voids any applicable recommendation for the same (activity, view)
-- - Both functions use comprehensive roles (direct, delegated, derived, inherited)
-- Pending obligations for a user in an organization -- Returns pending obligations not voided by an applicable prohibition
-- Returns only obligations not voided by an applicable prohibition
CREATE OR REPLACE FUNCTION morbac.pending_obligations( CREATE OR REPLACE FUNCTION morbac.pending_obligations(
p_user_id UUID, p_user_id UUID,
p_org_id UUID p_org_id UUID
@@ -67,8 +62,7 @@ $$;
COMMENT ON FUNCTION morbac.pending_obligations(UUID, UUID) IS COMMENT ON FUNCTION morbac.pending_obligations(UUID, UUID) IS
'Returns pending obligations for a user in an organization (informational only). Prohibitions void applicable obligations per Multi-OrBAC conflict resolution.'; 'Returns pending obligations for a user in an organization (informational only). Prohibitions void applicable obligations per Multi-OrBAC conflict resolution.';
-- Recommendations for a user in an organization -- Returns recommendations not voided by an applicable prohibition or obligation
-- Returns only recommendations not voided by an applicable prohibition or obligation
CREATE OR REPLACE FUNCTION morbac.pending_recommendations( CREATE OR REPLACE FUNCTION morbac.pending_recommendations(
p_user_id UUID, p_user_id UUID,
p_org_id UUID p_org_id UUID
@@ -125,7 +119,6 @@ $$;
COMMENT ON FUNCTION morbac.pending_recommendations(UUID, UUID) IS COMMENT ON FUNCTION morbac.pending_recommendations(UUID, UUID) IS
'Returns recommendations for a user in an organization (informational only). Prohibitions and obligations void applicable recommendations per Multi-OrBAC conflict resolution.'; 'Returns recommendations for a user in an organization (informational only). Prohibitions and obligations void applicable recommendations per Multi-OrBAC conflict resolution.';
-- Get all roles for a user in an organization
CREATE OR REPLACE FUNCTION morbac.user_roles_in_org( CREATE OR REPLACE FUNCTION morbac.user_roles_in_org(
p_user_id UUID, p_user_id UUID,
p_org_id UUID p_org_id UUID
@@ -151,7 +144,6 @@ $$;
COMMENT ON FUNCTION morbac.user_roles_in_org(UUID, UUID) IS COMMENT ON FUNCTION morbac.user_roles_in_org(UUID, UUID) IS
'Returns all roles for a user in an organization'; 'Returns all roles for a user in an organization';
-- Check if user has specific role in organization
CREATE OR REPLACE FUNCTION morbac.user_has_role( CREATE OR REPLACE FUNCTION morbac.user_has_role(
p_user_id UUID, p_user_id UUID,
p_org_id UUID, p_org_id UUID,
-10
View File
@@ -1,9 +1,3 @@
-- =============================================================================
-- ORGANIZATIONS
-- =============================================================================
-- Organizations are first-class entities in Multi-OrBAC
-- Each organization has its own policy space
CREATE TABLE morbac.orgs ( CREATE TABLE morbac.orgs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL UNIQUE, name TEXT NOT NULL UNIQUE,
@@ -16,7 +10,3 @@ CREATE INDEX idx_orgs_name ON morbac.orgs(name);
CREATE INDEX idx_orgs_parent ON morbac.orgs(parent_id); CREATE INDEX idx_orgs_parent ON morbac.orgs(parent_id);
COMMENT ON TABLE morbac.orgs IS 'Organizations - first-class entities in Multi-OrBAC with hierarchy support'; COMMENT ON TABLE morbac.orgs IS 'Organizations - first-class entities in Multi-OrBAC with hierarchy support';
COMMENT ON COLUMN morbac.orgs.id IS 'Unique organization identifier';
COMMENT ON COLUMN morbac.orgs.name IS 'Organization name (unique)';
COMMENT ON COLUMN morbac.orgs.parent_id IS 'Parent organization for hierarchical organizations';
COMMENT ON COLUMN morbac.orgs.metadata IS 'Optional metadata for organization';
+6 -31
View File
@@ -1,8 +1,4 @@
-- ============================================================================= -- Simplified policy declaration using names instead of UUIDs
-- POLICY DSL TABLE
-- =============================================================================
-- Simplified table for developers to declare policy
-- Uses friendly names instead of UUIDs
CREATE TABLE morbac.policy ( CREATE TABLE morbac.policy (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -17,23 +13,16 @@ CREATE TABLE morbac.policy (
compiled BOOLEAN NOT NULL DEFAULT FALSE compiled BOOLEAN NOT NULL DEFAULT FALSE
); );
-- UNIQUE including priority: NULL treated as -1 so two NULL-priority rows for the same tuple conflict -- NULL priority treated as -1 so two NULL-priority rows for the same tuple conflict
CREATE UNIQUE INDEX idx_policy_unique CREATE UNIQUE INDEX idx_policy_unique
ON morbac.policy(org_name, role_name, activity, view, modality, context_name, COALESCE(priority, -1)); ON morbac.policy(org_name, role_name, activity, view, modality, context_name, COALESCE(priority, -1));
CREATE INDEX idx_policy_not_compiled ON morbac.policy(compiled) WHERE NOT compiled; CREATE INDEX idx_policy_not_compiled ON morbac.policy(compiled) WHERE NOT compiled;
COMMENT ON TABLE morbac.policy IS 'Policy DSL - simplified policy declaration using names'; COMMENT ON TABLE morbac.policy IS 'Policy DSL - simplified policy declaration using names';
COMMENT ON COLUMN morbac.policy.org_name IS 'Organization name (resolved during compilation)';
COMMENT ON COLUMN morbac.policy.role_name IS 'Role name (resolved during compilation)';
COMMENT ON COLUMN morbac.policy.activity IS 'Activity name';
COMMENT ON COLUMN morbac.policy.view IS 'View name';
COMMENT ON COLUMN morbac.policy.modality IS 'Deontic modality';
COMMENT ON COLUMN morbac.policy.context_name IS 'Context name (default: always)';
COMMENT ON COLUMN morbac.policy.priority IS 'Optional rule priority (higher wins over lower; NULL = 0)';
COMMENT ON COLUMN morbac.policy.compiled IS 'Whether this policy entry has been compiled into rules'; COMMENT ON COLUMN morbac.policy.compiled IS 'Whether this policy entry has been compiled into rules';
COMMENT ON COLUMN morbac.policy.priority IS 'Optional rule priority (higher wins over lower; NULL = 0)';
-- Helper function to check administration permissions
CREATE OR REPLACE FUNCTION morbac.is_admin_allowed( CREATE OR REPLACE FUNCTION morbac.is_admin_allowed(
p_user_id UUID, p_user_id UUID,
p_org_id UUID, p_org_id UUID,
@@ -47,7 +36,6 @@ AS $$
DECLARE DECLARE
v_rule RECORD; v_rule RECORD;
BEGIN BEGIN
-- Check for admin prohibitions first
FOR v_rule IN FOR v_rule IN
SELECT ar.context_id SELECT ar.context_id
FROM morbac.admin_rules ar FROM morbac.admin_rules ar
@@ -64,7 +52,6 @@ BEGIN
END IF; END IF;
END LOOP; END LOOP;
-- Check for admin permissions
FOR v_rule IN FOR v_rule IN
SELECT ar.context_id SELECT ar.context_id
FROM morbac.admin_rules ar FROM morbac.admin_rules ar
@@ -81,19 +68,15 @@ BEGIN
END IF; END IF;
END LOOP; END LOOP;
RETURN FALSE; -- Default deny RETURN FALSE;
END; END;
$$; $$;
COMMENT ON FUNCTION morbac.is_admin_allowed(UUID, UUID, TEXT, TEXT) IS COMMENT ON FUNCTION morbac.is_admin_allowed(UUID, UUID, TEXT, TEXT) IS
'Checks administration permissions for policy management operations'; 'Checks administration permissions for policy management operations';
-- ============================================================================= -- Translates policy DSL entries into concrete rules by resolving names to IDs.
-- POLICY COMPILER -- Idempotent - safe to run multiple times.
-- =============================================================================
-- Translates policy DSL entries into concrete rules
-- Resolves names to IDs
-- Idempotent - safe to run multiple times
CREATE OR REPLACE FUNCTION morbac.compile_policy() CREATE OR REPLACE FUNCTION morbac.compile_policy()
RETURNS TABLE( RETURNS TABLE(
@@ -112,42 +95,34 @@ DECLARE
v_errors TEXT[] := ARRAY[]::TEXT[]; v_errors TEXT[] := ARRAY[]::TEXT[];
v_error_count INTEGER := 0; v_error_count INTEGER := 0;
BEGIN BEGIN
-- Process all uncompiled policy entries
FOR v_policy IN FOR v_policy IN
SELECT * FROM morbac.policy WHERE NOT compiled SELECT * FROM morbac.policy WHERE NOT compiled
LOOP LOOP
BEGIN BEGIN
-- Resolve organization
SELECT id INTO STRICT v_org_id SELECT id INTO STRICT v_org_id
FROM morbac.orgs FROM morbac.orgs
WHERE name = v_policy.org_name; WHERE name = v_policy.org_name;
-- Resolve role within organization
SELECT id INTO STRICT v_role_id SELECT id INTO STRICT v_role_id
FROM morbac.roles FROM morbac.roles
WHERE org_id = v_org_id AND name = v_policy.role_name; WHERE org_id = v_org_id AND name = v_policy.role_name;
-- Resolve context
SELECT id INTO STRICT v_context_id SELECT id INTO STRICT v_context_id
FROM morbac.contexts FROM morbac.contexts
WHERE name = v_policy.context_name; WHERE name = v_policy.context_name;
-- Ensure activity exists
INSERT INTO morbac.activities (name) INSERT INTO morbac.activities (name)
VALUES (v_policy.activity) VALUES (v_policy.activity)
ON CONFLICT (name) DO NOTHING; ON CONFLICT (name) DO NOTHING;
-- Ensure view exists
INSERT INTO morbac.views (name) INSERT INTO morbac.views (name)
VALUES (v_policy.view) VALUES (v_policy.view)
ON CONFLICT (name) DO NOTHING; ON CONFLICT (name) DO NOTHING;
-- Insert rule (ignore if already exists)
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, priority) INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, priority)
VALUES (v_org_id, v_role_id, v_policy.activity, v_policy.view, v_context_id, v_policy.modality, v_policy.priority) VALUES (v_org_id, v_role_id, v_policy.activity, v_policy.view, v_context_id, v_policy.modality, v_policy.priority)
ON CONFLICT (org_id, role_id, activity, view, context_id, modality) DO NOTHING; ON CONFLICT (org_id, role_id, activity, view, context_id, modality) DO NOTHING;
-- Mark as compiled
UPDATE morbac.policy SET compiled = TRUE WHERE id = v_policy.id; UPDATE morbac.policy SET compiled = TRUE WHERE id = v_policy.id;
v_compiled := v_compiled + 1; v_compiled := v_compiled + 1;
+1 -12
View File
@@ -1,10 +1,5 @@
-- ============================================================================= -- Row-Level Security helpers, compatible with PostgREST
-- RLS HELPER FUNCTIONS
-- =============================================================================
-- Helper functions for Row-Level Security policies
-- Compatible with PostgREST
-- Get current user ID from session variable
CREATE OR REPLACE FUNCTION morbac.current_user_id() CREATE OR REPLACE FUNCTION morbac.current_user_id()
RETURNS UUID RETURNS UUID
LANGUAGE plpgsql LANGUAGE plpgsql
@@ -13,7 +8,6 @@ AS $$
DECLARE DECLARE
v_user_id TEXT; v_user_id TEXT;
BEGIN BEGIN
-- Read from morbac.user_id GUC (set via SET morbac.user_id = '...' or PostgREST header mapping)
v_user_id := current_setting('morbac.user_id', TRUE); v_user_id := current_setting('morbac.user_id', TRUE);
IF v_user_id IS NULL OR v_user_id = '' THEN IF v_user_id IS NULL OR v_user_id = '' THEN
@@ -30,7 +24,6 @@ $$;
COMMENT ON FUNCTION morbac.current_user_id() IS COMMENT ON FUNCTION morbac.current_user_id() IS
'Returns current user ID from morbac.user_id session variable'; 'Returns current user ID from morbac.user_id session variable';
-- Get current organization ID from session variable
CREATE OR REPLACE FUNCTION morbac.current_org_id() CREATE OR REPLACE FUNCTION morbac.current_org_id()
RETURNS UUID RETURNS UUID
LANGUAGE plpgsql LANGUAGE plpgsql
@@ -39,7 +32,6 @@ AS $$
DECLARE DECLARE
v_org_id TEXT; v_org_id TEXT;
BEGIN BEGIN
-- Read from morbac.org_id GUC (set via SET morbac.org_id = '...' or PostgREST header mapping)
v_org_id := current_setting('morbac.org_id', TRUE); v_org_id := current_setting('morbac.org_id', TRUE);
IF v_org_id IS NULL OR v_org_id = '' THEN IF v_org_id IS NULL OR v_org_id = '' THEN
@@ -56,7 +48,6 @@ $$;
COMMENT ON FUNCTION morbac.current_org_id() IS COMMENT ON FUNCTION morbac.current_org_id() IS
'Returns current organization ID from morbac.org_id session variable'; 'Returns current organization ID from morbac.org_id session variable';
-- RLS check function
CREATE OR REPLACE FUNCTION morbac.rls_check( CREATE OR REPLACE FUNCTION morbac.rls_check(
p_activity TEXT, p_activity TEXT,
p_view TEXT p_view TEXT
@@ -72,12 +63,10 @@ BEGIN
v_user_id := morbac.current_user_id(); v_user_id := morbac.current_user_id();
v_org_id := morbac.current_org_id(); v_org_id := morbac.current_org_id();
-- If no user or org context, deny
IF v_user_id IS NULL OR v_org_id IS NULL THEN IF v_user_id IS NULL OR v_org_id IS NULL THEN
RETURN FALSE; RETURN FALSE;
END IF; END IF;
-- Call cached authorization decision function (default behavior)
RETURN morbac.is_allowed(v_user_id, v_org_id, p_activity, p_view); RETURN morbac.is_allowed(v_user_id, v_org_id, p_activity, p_view);
END; END;
$$; $$;
+6 -49
View File
@@ -1,8 +1,4 @@
-- ============================================================================= -- Roles are scoped to organizations and abstract sets of subjects
-- ROLES
-- =============================================================================
-- Roles are scoped to organizations
-- A role abstracts a set of subjects within an organization
CREATE TABLE morbac.roles ( CREATE TABLE morbac.roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -17,13 +13,7 @@ CREATE INDEX idx_roles_org_id ON morbac.roles(org_id);
CREATE INDEX idx_roles_org_name ON morbac.roles(org_id, name); CREATE INDEX idx_roles_org_name ON morbac.roles(org_id, name);
COMMENT ON TABLE morbac.roles IS 'Roles scoped to organizations - abstract sets of subjects'; COMMENT ON TABLE morbac.roles IS 'Roles scoped to organizations - abstract sets of subjects';
COMMENT ON COLUMN morbac.roles.org_id IS 'Organization this role belongs to';
COMMENT ON COLUMN morbac.roles.name IS 'Role name (unique within organization)';
-- =============================================================================
-- ROLE HIERARCHY
-- =============================================================================
-- Roles can inherit from other roles (role hierarchy)
-- Senior roles inherit permissions from junior roles -- Senior roles inherit permissions from junior roles
CREATE TABLE morbac.role_hierarchy ( CREATE TABLE morbac.role_hierarchy (
@@ -41,11 +31,7 @@ COMMENT ON TABLE morbac.role_hierarchy IS 'Role hierarchy - senior roles inherit
COMMENT ON COLUMN morbac.role_hierarchy.senior_role_id IS 'Senior role (inherits permissions)'; COMMENT ON COLUMN morbac.role_hierarchy.senior_role_id IS 'Senior role (inherits permissions)';
COMMENT ON COLUMN morbac.role_hierarchy.junior_role_id IS 'Junior role (provides permissions)'; COMMENT ON COLUMN morbac.role_hierarchy.junior_role_id IS 'Junior role (provides permissions)';
-- ============================================================================= -- Maps users to roles within organizations; user_id is external (e.g., from auth system)
-- USER-ROLE ASSIGNMENTS
-- =============================================================================
-- Maps users to roles within organizations
-- user_id is external (e.g., from authentication system)
CREATE TABLE morbac.user_roles ( CREATE TABLE morbac.user_roles (
user_id UUID NOT NULL, user_id UUID NOT NULL,
@@ -57,19 +43,12 @@ CREATE TABLE morbac.user_roles (
CREATE INDEX idx_user_roles_user_org ON morbac.user_roles(user_id, org_id); CREATE INDEX idx_user_roles_user_org ON morbac.user_roles(user_id, org_id);
CREATE INDEX idx_user_roles_role ON morbac.user_roles(role_id); CREATE INDEX idx_user_roles_role ON morbac.user_roles(role_id);
-- Performance: Fast role lookup with included role_id
CREATE INDEX idx_user_roles_fast ON morbac.user_roles(user_id, org_id) INCLUDE (role_id); CREATE INDEX idx_user_roles_fast ON morbac.user_roles(user_id, org_id) INCLUDE (role_id);
COMMENT ON TABLE morbac.user_roles IS 'Maps users to roles within organizations'; COMMENT ON TABLE morbac.user_roles IS 'Maps users to roles within organizations';
COMMENT ON COLUMN morbac.user_roles.user_id IS 'External user identifier'; COMMENT ON COLUMN morbac.user_roles.user_id IS 'External user identifier';
COMMENT ON COLUMN morbac.user_roles.role_id IS 'Role assigned to the user';
COMMENT ON COLUMN morbac.user_roles.org_id IS 'Organization context for this assignment';
-- ============================================================================= -- Temporary delegation of a role from one user to another, time-bounded
-- DELEGATION
-- =============================================================================
-- Users can delegate their permissions to other users temporarily
-- Delegation is time-bounded and role-scoped
CREATE TABLE morbac.delegations ( CREATE TABLE morbac.delegations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -89,18 +68,11 @@ CREATE INDEX idx_delegations_delegatee ON morbac.delegations(delegatee_id, org_i
CREATE INDEX idx_delegations_validity ON morbac.delegations(valid_from, valid_until) WHERE NOT revoked; CREATE INDEX idx_delegations_validity ON morbac.delegations(valid_from, valid_until) WHERE NOT revoked;
COMMENT ON TABLE morbac.delegations IS 'Temporary delegation of roles from one user to another'; COMMENT ON TABLE morbac.delegations IS 'Temporary delegation of roles from one user to another';
COMMENT ON COLUMN morbac.delegations.delegator_id IS 'User delegating the role';
COMMENT ON COLUMN morbac.delegations.delegatee_id IS 'User receiving the delegated role';
COMMENT ON COLUMN morbac.delegations.role_id IS 'Role being delegated';
COMMENT ON COLUMN morbac.delegations.valid_from IS 'Delegation start time'; COMMENT ON COLUMN morbac.delegations.valid_from IS 'Delegation start time';
COMMENT ON COLUMN morbac.delegations.valid_until IS 'Delegation end time'; COMMENT ON COLUMN morbac.delegations.valid_until IS 'Delegation end time';
COMMENT ON COLUMN morbac.delegations.revoked IS 'Whether delegation has been revoked'; COMMENT ON COLUMN morbac.delegations.revoked IS 'Whether delegation has been revoked';
-- ============================================================================= -- Explicitly prevent users from ever getting certain roles; takes precedence over positive assignments
-- NEGATIVE ROLE ASSIGNMENTS
-- =============================================================================
-- Explicitly prevent users from ever getting certain roles
-- Takes precedence over positive assignments
CREATE TABLE morbac.negative_role_assignments ( CREATE TABLE morbac.negative_role_assignments (
user_id UUID NOT NULL, user_id UUID NOT NULL,
@@ -114,14 +86,9 @@ CREATE TABLE morbac.negative_role_assignments (
CREATE INDEX idx_negative_assignments ON morbac.negative_role_assignments(user_id, org_id); CREATE INDEX idx_negative_assignments ON morbac.negative_role_assignments(user_id, org_id);
COMMENT ON TABLE morbac.negative_role_assignments IS 'Explicit prohibition of role assignments'; COMMENT ON TABLE morbac.negative_role_assignments IS 'Explicit prohibition of role assignments';
COMMENT ON COLUMN morbac.negative_role_assignments.user_id IS 'User prohibited from having role';
COMMENT ON COLUMN morbac.negative_role_assignments.role_id IS 'Role that is prohibited';
COMMENT ON COLUMN morbac.negative_role_assignments.reason IS 'Reason for prohibition'; COMMENT ON COLUMN morbac.negative_role_assignments.reason IS 'Reason for prohibition';
-- ============================================================================= -- Mutually exclusive roles that cannot be held simultaneously
-- SEPARATION OF DUTY (SoD)
-- =============================================================================
-- Define mutually exclusive roles that cannot be held simultaneously
CREATE TABLE morbac.sod_conflicts ( CREATE TABLE morbac.sod_conflicts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -138,14 +105,8 @@ CREATE INDEX idx_sod_conflicts_org ON morbac.sod_conflicts(org_id);
CREATE INDEX idx_sod_conflicts_roles ON morbac.sod_conflicts(role_a_id, role_b_id); CREATE INDEX idx_sod_conflicts_roles ON morbac.sod_conflicts(role_a_id, role_b_id);
COMMENT ON TABLE morbac.sod_conflicts IS 'Separation of Duty: mutually exclusive roles'; COMMENT ON TABLE morbac.sod_conflicts IS 'Separation of Duty: mutually exclusive roles';
COMMENT ON COLUMN morbac.sod_conflicts.role_a_id IS 'First conflicting role';
COMMENT ON COLUMN morbac.sod_conflicts.role_b_id IS 'Second conflicting role';
COMMENT ON COLUMN morbac.sod_conflicts.description IS 'Description of the conflict';
-- ============================================================================= -- Min/max user count constraints per role
-- CARDINALITY CONSTRAINTS
-- =============================================================================
-- Limit the number of users that can have a specific role
CREATE TABLE morbac.role_cardinality ( CREATE TABLE morbac.role_cardinality (
role_id UUID PRIMARY KEY REFERENCES morbac.roles(id) ON DELETE CASCADE, role_id UUID PRIMARY KEY REFERENCES morbac.roles(id) ON DELETE CASCADE,
@@ -161,9 +122,6 @@ COMMENT ON TABLE morbac.role_cardinality IS 'Cardinality constraints for roles (
COMMENT ON COLUMN morbac.role_cardinality.min_users IS 'Minimum number of users required for this role'; COMMENT ON COLUMN morbac.role_cardinality.min_users IS 'Minimum number of users required for this role';
COMMENT ON COLUMN morbac.role_cardinality.max_users IS 'Maximum number of users allowed for this role'; COMMENT ON COLUMN morbac.role_cardinality.max_users IS 'Maximum number of users allowed for this role';
-- =============================================================================
-- DERIVED ROLES
-- =============================================================================
-- Roles computed dynamically based on conditions rather than explicit assignment -- Roles computed dynamically based on conditions rather than explicit assignment
CREATE TABLE morbac.derived_roles ( CREATE TABLE morbac.derived_roles (
@@ -174,5 +132,4 @@ CREATE TABLE morbac.derived_roles (
); );
COMMENT ON TABLE morbac.derived_roles IS 'Roles computed dynamically based on conditions'; COMMENT ON TABLE morbac.derived_roles IS 'Roles computed dynamically based on conditions';
COMMENT ON COLUMN morbac.derived_roles.role_id IS 'Role that is derived';
COMMENT ON COLUMN morbac.derived_roles.condition_evaluator IS 'Function(user_id, org_id) returning boolean'; COMMENT ON COLUMN morbac.derived_roles.condition_evaluator IS 'Function(user_id, org_id) returning boolean';
+8 -31
View File
@@ -1,11 +1,4 @@
-- ============================================================================= -- Core OrBAC rule relation: Rule(org, role, activity, view, context, modality)
-- RULES (Core OrBAC Policy)
-- =============================================================================
-- Implements the OrBAC rule relation:
-- Rule(org, role, activity, view, context, modality)
--
-- Represents: Permission, Prohibition, Obligation, or Recommendation
CREATE TABLE morbac.rules ( CREATE TABLE morbac.rules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -25,19 +18,20 @@ CREATE TABLE morbac.rules (
CHECK (valid_until IS NULL OR valid_from IS NULL OR valid_until > valid_from) CHECK (valid_until IS NULL OR valid_from IS NULL OR valid_until > valid_from)
); );
-- Indexes for fast lookup and validity
CREATE INDEX idx_rules_org_role ON morbac.rules(org_id, role_id); 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_activity_view ON morbac.rules(activity, view);
CREATE INDEX idx_rules_modality ON morbac.rules(modality); 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_lookup ON morbac.rules(org_id, role_id, activity, view, modality);
-- Fast lookup index for active rules
CREATE INDEX idx_rules_fast_lookup ON morbac.rules(org_id, activity, modality, view) CREATE INDEX idx_rules_fast_lookup ON morbac.rules(org_id, activity, modality, view)
INCLUDE (role_id, context_id) INCLUDE (role_id, context_id)
WHERE is_active = true; WHERE is_active = true;
-- Placeholder trigger to maintain is_active (update as needed) COMMENT ON TABLE morbac.rules IS 'Core OrBAC rules - Permission, Prohibition, Obligation, Recommendation';
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.';
-- Trigger to maintain is_active based on temporal validity
CREATE OR REPLACE FUNCTION morbac.rules_set_is_active() CREATE OR REPLACE FUNCTION morbac.rules_set_is_active()
RETURNS TRIGGER AS $$ RETURNS TRIGGER AS $$
BEGIN BEGIN
@@ -54,18 +48,8 @@ CREATE TRIGGER trg_rules_set_is_active
BEFORE INSERT OR UPDATE ON morbac.rules BEFORE INSERT OR UPDATE ON morbac.rules
FOR EACH ROW EXECUTE FUNCTION morbac.rules_set_is_active(); FOR EACH ROW EXECUTE FUNCTION morbac.rules_set_is_active();
COMMENT ON TABLE morbac.rules IS 'Core OrBAC rules - Permission, Prohibition, Obligation, Recommendation'; -- Check if a rule is currently valid based on temporal constraints
COMMENT ON COLUMN morbac.rules.org_id IS 'Organization scope';
COMMENT ON COLUMN morbac.rules.role_id IS 'Role this rule applies to';
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.priority IS 'Optional: Rule priority (higher wins). NULL = 0. A permission with higher priority than a prohibition overrides it.';
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( CREATE OR REPLACE FUNCTION morbac.is_rule_valid(
p_valid_from TIMESTAMPTZ, p_valid_from TIMESTAMPTZ,
p_valid_until TIMESTAMPTZ p_valid_until TIMESTAMPTZ
@@ -77,17 +61,10 @@ AS $$
DECLARE DECLARE
v_now TIMESTAMPTZ := CURRENT_TIMESTAMP; v_now TIMESTAMPTZ := CURRENT_TIMESTAMP;
BEGIN 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 IF p_valid_from IS NOT NULL AND v_now < p_valid_from THEN
RETURN FALSE; RETURN FALSE;
END IF; END IF;
-- Check valid_until
IF p_valid_until IS NOT NULL AND v_now >= p_valid_until THEN IF p_valid_until IS NOT NULL AND v_now >= p_valid_until THEN
RETURN FALSE; RETURN FALSE;
END IF; END IF;
-5
View File
@@ -1,8 +1,3 @@
-- =============================================================================
-- DEONTIC MODALITY TYPE
-- =============================================================================
-- Represents the four deontic modalities of OrBAC model
CREATE TYPE morbac.modality AS ENUM ( CREATE TYPE morbac.modality AS ENUM (
'permission', 'permission',
'prohibition', 'prohibition',
+4 -12
View File
@@ -1,6 +1,4 @@
-- ============================================================================= -- Validation functions for SoD, cardinality, and rule conflict detection.
-- VALIDATION FUNCTIONS
-- =============================================================================
-- --
-- Rule conflict detection (from Multi-OrBAC paper): -- Rule conflict detection (from Multi-OrBAC paper):
-- Conflicts exist when two rules share the same (org, role, activity, view, context) -- Conflicts exist when two rules share the same (org, role, activity, view, context)
@@ -12,7 +10,6 @@
-- --
-- Note: hierarchy-based overlaps are intentional design, not flagged here. -- Note: hierarchy-based overlaps are intentional design, not flagged here.
-- Check if role assignment would violate Separation of Duty
CREATE OR REPLACE FUNCTION morbac.check_sod_violation( CREATE OR REPLACE FUNCTION morbac.check_sod_violation(
p_user_id UUID, p_user_id UUID,
p_role_id UUID, p_role_id UUID,
@@ -25,7 +22,6 @@ AS $$
DECLARE DECLARE
v_conflict_exists BOOLEAN; v_conflict_exists BOOLEAN;
BEGIN BEGIN
-- Check if user already has a conflicting role
SELECT EXISTS ( SELECT EXISTS (
SELECT 1 SELECT 1
FROM morbac.user_roles ur FROM morbac.user_roles ur
@@ -45,7 +41,6 @@ $$;
COMMENT ON FUNCTION morbac.check_sod_violation(UUID, UUID, UUID) IS COMMENT ON FUNCTION morbac.check_sod_violation(UUID, UUID, UUID) IS
'Returns true if assigning role would violate Separation of Duty constraints'; 'Returns true if assigning role would violate Separation of Duty constraints';
-- Check if role assignment would violate cardinality constraints
CREATE OR REPLACE FUNCTION morbac.check_cardinality_violation( CREATE OR REPLACE FUNCTION morbac.check_cardinality_violation(
p_role_id UUID, p_role_id UUID,
p_adding BOOLEAN DEFAULT TRUE p_adding BOOLEAN DEFAULT TRUE
@@ -59,7 +54,6 @@ DECLARE
v_min_users INTEGER; v_min_users INTEGER;
v_max_users INTEGER; v_max_users INTEGER;
BEGIN BEGIN
-- Get current user count and constraints
SELECT SELECT
COUNT(DISTINCT ur.user_id), COUNT(DISTINCT ur.user_id),
rc.min_users, rc.min_users,
@@ -70,28 +64,25 @@ BEGIN
WHERE ur.role_id = p_role_id WHERE ur.role_id = p_role_id
GROUP BY rc.min_users, rc.max_users; GROUP BY rc.min_users, rc.max_users;
-- Check max constraint when adding
IF p_adding AND v_max_users IS NOT NULL THEN IF p_adding AND v_max_users IS NOT NULL THEN
IF v_current_count >= v_max_users THEN IF v_current_count >= v_max_users THEN
RETURN format('Maximum users (%s) reached for role', v_max_users); RETURN format('Maximum users (%s) reached for role', v_max_users);
END IF; END IF;
END IF; END IF;
-- Check min constraint when removing
IF NOT p_adding AND v_min_users IS NOT NULL THEN IF NOT p_adding AND v_min_users IS NOT NULL THEN
IF v_current_count <= v_min_users THEN IF v_current_count <= v_min_users THEN
RETURN format('Minimum users (%s) required for role', v_min_users); RETURN format('Minimum users (%s) required for role', v_min_users);
END IF; END IF;
END IF; END IF;
RETURN NULL; -- No violation RETURN NULL;
END; END;
$$; $$;
COMMENT ON FUNCTION morbac.check_cardinality_violation(UUID, BOOLEAN) IS COMMENT ON FUNCTION morbac.check_cardinality_violation(UUID, BOOLEAN) IS
'Returns error message if cardinality constraint would be violated, NULL otherwise'; 'Returns error message if cardinality constraint would be violated, NULL otherwise';
-- Detect direct modality conflicts for a candidate rule
CREATE OR REPLACE FUNCTION morbac.detect_rule_conflicts( CREATE OR REPLACE FUNCTION morbac.detect_rule_conflicts(
p_org_id UUID, p_org_id UUID,
p_role_id UUID, p_role_id UUID,
@@ -99,7 +90,7 @@ CREATE OR REPLACE FUNCTION morbac.detect_rule_conflicts(
p_view TEXT, p_view TEXT,
p_context_id UUID, p_context_id UUID,
p_modality morbac.modality, p_modality morbac.modality,
p_exclude_id UUID DEFAULT NULL -- exclude self on UPDATE p_exclude_id UUID DEFAULT NULL
) )
RETURNS TABLE( RETURNS TABLE(
conflicting_rule_id UUID, conflicting_rule_id UUID,
@@ -145,6 +136,7 @@ COMMENT ON FUNCTION morbac.detect_rule_conflicts(UUID, UUID, TEXT, TEXT, UUID, m
'Returns rules that directly conflict with the given tuple due to modality precedence (prohibition > obligation > recommendation > permission).'; 'Returns rules that directly conflict with the given tuple due to modality precedence (prohibition > obligation > recommendation > permission).';
-- Trigger: warn (non-blocking) when a new/updated rule conflicts with an existing one -- Trigger: warn (non-blocking) when a new/updated rule conflicts with an existing one
CREATE OR REPLACE FUNCTION morbac.trg_warn_rule_conflicts() CREATE OR REPLACE FUNCTION morbac.trg_warn_rule_conflicts()
RETURNS TRIGGER RETURNS TRIGGER
LANGUAGE plpgsql LANGUAGE plpgsql
+2 -12
View File
@@ -1,8 +1,4 @@
-- ============================================================================= -- Views are global abstract object categories (not org-scoped)
-- VIEWS
-- =============================================================================
-- Views represent abstract object categories in OrBAC
-- These are global abstractions (not org-scoped)
CREATE TABLE morbac.views ( CREATE TABLE morbac.views (
name TEXT PRIMARY KEY, name TEXT PRIMARY KEY,
@@ -11,14 +7,8 @@ CREATE TABLE morbac.views (
); );
COMMENT ON TABLE morbac.views IS 'Views - abstract object categories in OrBAC model (global)'; COMMENT ON TABLE morbac.views IS 'Views - abstract object categories in OrBAC model (global)';
COMMENT ON COLUMN morbac.views.name IS 'View name (unique, global)';
-- =============================================================================
-- VIEW HIERARCHY
-- =============================================================================
-- Views can inherit from other views
-- e.g., "confidential_documents" is a "documents", "admin_reports" is a "reports"
-- Senior views inherit from junior views (e.g., confidential_documents is a documents)
CREATE TABLE morbac.view_hierarchy ( CREATE TABLE morbac.view_hierarchy (
senior_view TEXT NOT NULL REFERENCES morbac.views(name) ON DELETE CASCADE, senior_view TEXT NOT NULL REFERENCES morbac.views(name) ON DELETE CASCADE,
junior_view TEXT NOT NULL REFERENCES morbac.views(name) ON DELETE CASCADE, junior_view TEXT NOT NULL REFERENCES morbac.views(name) ON DELETE CASCADE,