misc(all): cleanup comments, update documentation, tidy up code
This commit is contained in:
+6
-6
@@ -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?
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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,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';
|
|
||||||
|
|||||||
+18
-36
@@ -1,49 +1,31 @@
|
|||||||
-- =============================================================================
|
-- 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
|
-- WHERE do.document_id = app.documents.id
|
||||||
-- WHERE do.document_id = app.documents.id
|
-- AND do.org_id = morbac.current_org_id()
|
||||||
-- AND do.org_id = morbac.current_org_id()
|
-- )
|
||||||
-- )
|
-- 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
@@ -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
|
|
||||||
-- =============================================================================
|
|
||||||
|
|||||||
+26
-80
@@ -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
|
||||||
@@ -269,34 +244,32 @@ BEGIN
|
|||||||
|
|
||||||
UNION
|
UNION
|
||||||
|
|
||||||
-- Delegated roles (active and not revoked)
|
-- Delegated roles (active and not revoked)
|
||||||
SELECT d.role_id, 'delegation'::TEXT, 0 AS depth
|
SELECT d.role_id, 'delegation'::TEXT, 0 AS depth
|
||||||
FROM morbac.delegations d
|
FROM morbac.delegations d
|
||||||
WHERE d.delegatee_id = p_user_id
|
WHERE d.delegatee_id = p_user_id
|
||||||
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
|
)
|
||||||
)
|
AND NOT EXISTS (
|
||||||
-- Check not negatively assigned to delegatee
|
SELECT 1 FROM morbac.negative_role_assignments nra
|
||||||
AND NOT EXISTS (
|
WHERE nra.user_id = p_user_id
|
||||||
SELECT 1 FROM morbac.negative_role_assignments nra
|
AND nra.role_id = d.role_id
|
||||||
WHERE nra.user_id = p_user_id
|
AND nra.org_id = p_org_id
|
||||||
AND nra.role_id = d.role_id
|
)
|
||||||
AND nra.org_id = p_org_id
|
|
||||||
)
|
|
||||||
|
|
||||||
UNION
|
UNION
|
||||||
|
|
||||||
-- Role hierarchy (junior roles inherited by user's roles)
|
-- Role hierarchy (junior roles inherited by user's roles)
|
||||||
SELECT rh.junior_role_id, er.source || '_inherited', er.depth + 1
|
SELECT rh.junior_role_id, er.source || '_inherited', 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
|
||||||
)
|
)
|
||||||
SELECT DISTINCT ON (effective_roles.role_id) effective_roles.role_id, effective_roles.source, effective_roles.depth
|
SELECT DISTINCT ON (effective_roles.role_id) effective_roles.role_id, effective_roles.source, effective_roles.depth
|
||||||
FROM effective_roles
|
FROM effective_roles
|
||||||
@@ -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 +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
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user