init: first commit
This commit is contained in:
+383
@@ -0,0 +1,383 @@
|
|||||||
|
# Administration Guide
|
||||||
|
|
||||||
|
This guide explains how to set up organization-scoped administrators without relying on database superadmins.
|
||||||
|
|
||||||
|
## Concept
|
||||||
|
|
||||||
|
Multi-OrBAC allows you to delegate administrative capabilities to specific roles within each organization. This means:
|
||||||
|
|
||||||
|
- Organization administrators can manage users, roles, and policies within their own organization
|
||||||
|
- No need for database-level superadmin access for day-to-day administration
|
||||||
|
- Fine-grained control over what each admin role can do
|
||||||
|
- Admins cannot affect other organizations
|
||||||
|
|
||||||
|
## Administration Rules
|
||||||
|
|
||||||
|
The `morbac.admin_rules` table defines what administrative actions specific roles can perform:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE morbac.admin_rules (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
org_id UUID REFERENCES morbac.orgs(id),
|
||||||
|
role_id UUID REFERENCES morbac.roles(id),
|
||||||
|
admin_activity TEXT, -- 'manage', 'assign_role', etc.
|
||||||
|
admin_target TEXT, -- 'policies', 'roles', role name, etc.
|
||||||
|
modality morbac.modality, -- 'permission' or 'prohibition'
|
||||||
|
context_id UUID REFERENCES morbac.contexts(id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Admin Patterns
|
||||||
|
|
||||||
|
### 1. Organization Administrator
|
||||||
|
|
||||||
|
Full admin within their organization:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Create admin role
|
||||||
|
INSERT INTO morbac.roles (org_id, name, description)
|
||||||
|
SELECT id, 'org_admin', 'Organization administrator'
|
||||||
|
FROM morbac.orgs WHERE name = 'Acme Corp';
|
||||||
|
|
||||||
|
-- Grant ability to manage policies
|
||||||
|
INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, modality, context_id)
|
||||||
|
SELECT
|
||||||
|
o.id,
|
||||||
|
r.id,
|
||||||
|
'manage',
|
||||||
|
'policies',
|
||||||
|
'permission',
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'always')
|
||||||
|
FROM morbac.orgs o
|
||||||
|
JOIN morbac.roles r ON r.org_id = o.id
|
||||||
|
WHERE o.name = 'Acme Corp' AND r.name = 'org_admin';
|
||||||
|
|
||||||
|
-- Grant ability to manage roles
|
||||||
|
INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, modality, context_id)
|
||||||
|
SELECT
|
||||||
|
o.id,
|
||||||
|
r.id,
|
||||||
|
'manage',
|
||||||
|
'roles',
|
||||||
|
'permission',
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'always')
|
||||||
|
FROM morbac.orgs o
|
||||||
|
JOIN morbac.roles r ON r.org_id = o.id
|
||||||
|
WHERE o.name = 'Acme Corp' AND r.name = 'org_admin';
|
||||||
|
|
||||||
|
-- Grant ability to assign ALL roles (wildcard)
|
||||||
|
INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, modality, context_id)
|
||||||
|
SELECT
|
||||||
|
o.id,
|
||||||
|
r.id,
|
||||||
|
'assign_role',
|
||||||
|
'*',
|
||||||
|
'permission',
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'always')
|
||||||
|
FROM morbac.orgs o
|
||||||
|
JOIN morbac.roles r ON r.org_id = o.id
|
||||||
|
WHERE o.name = 'Acme Corp' AND r.name = 'org_admin';
|
||||||
|
|
||||||
|
-- Assign someone as org admin
|
||||||
|
INSERT INTO morbac.user_roles (user_id, role_id, org_id)
|
||||||
|
SELECT
|
||||||
|
'alice-uuid'::uuid,
|
||||||
|
r.id,
|
||||||
|
o.id
|
||||||
|
FROM morbac.orgs o
|
||||||
|
JOIN morbac.roles r ON r.org_id = o.id
|
||||||
|
WHERE o.name = 'Acme Corp' AND r.name = 'org_admin';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. HR Manager (User/Role Assignment Only)
|
||||||
|
|
||||||
|
Can assign users to roles but cannot modify policies:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Create HR manager role
|
||||||
|
INSERT INTO morbac.roles (org_id, name, description)
|
||||||
|
SELECT id, 'hr_manager', 'HR manager - can assign users to roles'
|
||||||
|
FROM morbac.orgs WHERE name = 'Acme Corp';
|
||||||
|
|
||||||
|
-- Grant ability to assign specific roles
|
||||||
|
INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, modality, context_id)
|
||||||
|
SELECT
|
||||||
|
o.id,
|
||||||
|
r.id,
|
||||||
|
'assign_role',
|
||||||
|
target_role,
|
||||||
|
'permission',
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'always')
|
||||||
|
FROM morbac.orgs o
|
||||||
|
CROSS JOIN (VALUES ('employee'), ('manager'), ('contractor')) AS roles(target_role)
|
||||||
|
JOIN morbac.roles r ON r.org_id = o.id
|
||||||
|
WHERE o.name = 'Acme Corp' AND r.name = 'hr_manager';
|
||||||
|
|
||||||
|
-- Prohibit assigning admin roles
|
||||||
|
INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, modality, context_id)
|
||||||
|
SELECT
|
||||||
|
o.id,
|
||||||
|
r.id,
|
||||||
|
'assign_role',
|
||||||
|
'org_admin',
|
||||||
|
'prohibition',
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'always')
|
||||||
|
FROM morbac.orgs o
|
||||||
|
JOIN morbac.roles r ON r.org_id = o.id
|
||||||
|
WHERE o.name = 'Acme Corp' AND r.name = 'hr_manager';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Security Manager (Policy Management Only)
|
||||||
|
|
||||||
|
Can define policies but cannot assign users:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Create security manager role
|
||||||
|
INSERT INTO morbac.roles (org_id, name, description)
|
||||||
|
SELECT id, 'security_manager', 'Security manager - can manage policies'
|
||||||
|
FROM morbac.orgs WHERE name = 'Acme Corp';
|
||||||
|
|
||||||
|
-- Grant policy management
|
||||||
|
INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, modality, context_id)
|
||||||
|
SELECT
|
||||||
|
o.id,
|
||||||
|
r.id,
|
||||||
|
'manage',
|
||||||
|
'policies',
|
||||||
|
'permission',
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'always')
|
||||||
|
FROM morbac.orgs o
|
||||||
|
JOIN morbac.roles r ON r.org_id = o.id
|
||||||
|
WHERE o.name = 'Acme Corp' AND r.name = 'security_manager';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using Admin Functions
|
||||||
|
|
||||||
|
### Check Permissions
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Check if Alice can manage policies
|
||||||
|
SELECT morbac.can_manage_policies('alice-uuid'::uuid, org_id);
|
||||||
|
|
||||||
|
-- Check if Alice can manage roles
|
||||||
|
SELECT morbac.can_manage_roles('alice-uuid'::uuid, org_id);
|
||||||
|
|
||||||
|
-- Check if Alice can assign a specific role
|
||||||
|
SELECT morbac.can_manage_user_role('alice-uuid'::uuid, org_id, employee_role_id);
|
||||||
|
|
||||||
|
-- General admin check
|
||||||
|
SELECT morbac.is_admin_allowed('alice-uuid'::uuid, org_id, 'assign_role', 'manager');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Assign/Revoke Roles Safely
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Alice (HR manager) assigns Bob to employee role
|
||||||
|
SELECT morbac.admin_assign_role(
|
||||||
|
'alice-uuid'::uuid, -- Admin user
|
||||||
|
'bob-uuid'::uuid, -- Target user
|
||||||
|
employee_role_id, -- Role to assign
|
||||||
|
org_id -- Organization
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Alice revokes Bob's employee role
|
||||||
|
SELECT morbac.admin_revoke_role(
|
||||||
|
'alice-uuid'::uuid,
|
||||||
|
'bob-uuid'::uuid,
|
||||||
|
employee_role_id,
|
||||||
|
org_id
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
These functions automatically:
|
||||||
|
- Check if Alice has permission to manage the role
|
||||||
|
- Validate SoD constraints
|
||||||
|
- Validate cardinality constraints
|
||||||
|
- Raise exceptions if constraints are violated
|
||||||
|
|
||||||
|
## Application Integration
|
||||||
|
|
||||||
|
### REST API Example
|
||||||
|
|
||||||
|
Your application can expose admin endpoints that use these functions:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Endpoint: POST /api/orgs/:orgId/users/:userId/roles/:roleId
|
||||||
|
-- Handler checks:
|
||||||
|
CREATE OR REPLACE FUNCTION app.assign_role_endpoint(
|
||||||
|
p_requesting_user_id UUID,
|
||||||
|
p_org_id UUID,
|
||||||
|
p_target_user_id UUID,
|
||||||
|
p_role_id UUID
|
||||||
|
)
|
||||||
|
RETURNS JSON
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_result JSON;
|
||||||
|
BEGIN
|
||||||
|
BEGIN
|
||||||
|
PERFORM morbac.admin_assign_role(
|
||||||
|
p_requesting_user_id,
|
||||||
|
p_target_user_id,
|
||||||
|
p_role_id,
|
||||||
|
p_org_id
|
||||||
|
);
|
||||||
|
|
||||||
|
v_result := json_build_object(
|
||||||
|
'success', true,
|
||||||
|
'message', 'Role assigned successfully'
|
||||||
|
);
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
v_result := json_build_object(
|
||||||
|
'success', false,
|
||||||
|
'error', SQLERRM
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
RETURN v_result;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Row-Level Security for Admin Tables
|
||||||
|
|
||||||
|
Protect admin configuration with RLS:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Only org admins can see admin rules
|
||||||
|
ALTER TABLE morbac.admin_rules ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
CREATE POLICY admin_rules_access ON morbac.admin_rules
|
||||||
|
FOR SELECT
|
||||||
|
USING (
|
||||||
|
org_id = morbac.current_org_id()
|
||||||
|
AND (
|
||||||
|
-- User is org admin
|
||||||
|
morbac.can_manage_policies(morbac.current_user_id(), org_id)
|
||||||
|
OR morbac.can_manage_roles(morbac.current_user_id(), org_id)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Only org admins can modify admin rules
|
||||||
|
CREATE POLICY admin_rules_modify ON morbac.admin_rules
|
||||||
|
FOR ALL
|
||||||
|
USING (
|
||||||
|
org_id = morbac.current_org_id()
|
||||||
|
AND morbac.can_manage_policies(morbac.current_user_id(), org_id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Context-Based Admin Rules
|
||||||
|
|
||||||
|
You can limit admin actions to specific contexts (e.g., business hours):
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Create context for business hours
|
||||||
|
CREATE OR REPLACE FUNCTION morbac.context_business_hours()
|
||||||
|
RETURNS BOOLEAN
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
STABLE
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN EXTRACT(DOW FROM CURRENT_DATE) BETWEEN 1 AND 5
|
||||||
|
AND EXTRACT(HOUR FROM CURRENT_TIME) BETWEEN 9 AND 17;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
INSERT INTO morbac.contexts (name, description, evaluator) VALUES
|
||||||
|
('business_hours', 'Monday-Friday 9am-5pm', 'morbac.context_business_hours');
|
||||||
|
|
||||||
|
-- HR can only assign roles during business hours
|
||||||
|
INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, modality, context_id)
|
||||||
|
SELECT
|
||||||
|
o.id,
|
||||||
|
r.id,
|
||||||
|
'assign_role',
|
||||||
|
'*',
|
||||||
|
'permission',
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'business_hours')
|
||||||
|
FROM morbac.orgs o
|
||||||
|
JOIN morbac.roles r ON r.org_id = o.id
|
||||||
|
WHERE o.name = 'Acme Corp' AND r.name = 'hr_manager';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Principle of Least Privilege**: Grant only necessary admin capabilities to each role
|
||||||
|
2. **Separation of Duties**: Separate policy management from user assignment
|
||||||
|
3. **Audit Trail**: Log all admin actions (consider triggers on user_roles, rules tables)
|
||||||
|
4. **Context-Based**: Use contexts to limit when admin actions can occur
|
||||||
|
5. **Multiple Admins**: Use role cardinality to ensure multiple admins exist
|
||||||
|
6. **Prohibitions**: Use prohibition admin rules to explicitly deny certain actions
|
||||||
|
|
||||||
|
## Example: Complete Setup
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 1. Create organization
|
||||||
|
INSERT INTO morbac.orgs (name) VALUES ('Acme Corp');
|
||||||
|
|
||||||
|
-- 2. Create roles
|
||||||
|
INSERT INTO morbac.roles (org_id, name)
|
||||||
|
SELECT id, name FROM morbac.orgs,
|
||||||
|
(VALUES ('org_admin'), ('hr_manager'), ('employee'), ('manager')) AS roles(name)
|
||||||
|
WHERE morbac.orgs.name = 'Acme Corp';
|
||||||
|
|
||||||
|
-- 3. Set up org_admin with full permissions
|
||||||
|
INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, modality, context_id)
|
||||||
|
SELECT o.id, r.id, activity, target, 'permission', c.id
|
||||||
|
FROM morbac.orgs o
|
||||||
|
JOIN morbac.roles r ON r.org_id = o.id
|
||||||
|
CROSS JOIN (VALUES
|
||||||
|
('manage', 'policies'),
|
||||||
|
('manage', 'roles'),
|
||||||
|
('assign_role', '*')
|
||||||
|
) AS perms(activity, target)
|
||||||
|
CROSS JOIN morbac.contexts c
|
||||||
|
WHERE o.name = 'Acme Corp' AND r.name = 'org_admin' AND c.name = 'always';
|
||||||
|
|
||||||
|
-- 4. Set up hr_manager with limited permissions
|
||||||
|
INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, modality, context_id)
|
||||||
|
SELECT o.id, r.id, 'assign_role', role_name, 'permission', c.id
|
||||||
|
FROM morbac.orgs o
|
||||||
|
JOIN morbac.roles r ON r.org_id = o.id
|
||||||
|
CROSS JOIN (VALUES ('employee'), ('manager')) AS assignable(role_name)
|
||||||
|
CROSS JOIN morbac.contexts c
|
||||||
|
WHERE o.name = 'Acme Corp' AND r.name = 'hr_manager' AND c.name = 'always';
|
||||||
|
|
||||||
|
-- 5. Assign Alice as org_admin
|
||||||
|
INSERT INTO morbac.user_roles (user_id, role_id, org_id)
|
||||||
|
SELECT 'alice-uuid'::uuid, r.id, o.id
|
||||||
|
FROM morbac.orgs o
|
||||||
|
JOIN morbac.roles r ON r.org_id = o.id
|
||||||
|
WHERE o.name = 'Acme Corp' AND r.name = 'org_admin';
|
||||||
|
|
||||||
|
-- 6. Now Alice can assign Bob as hr_manager
|
||||||
|
SELECT morbac.admin_assign_role(
|
||||||
|
'alice-uuid'::uuid,
|
||||||
|
'bob-uuid'::uuid,
|
||||||
|
(SELECT id FROM morbac.roles WHERE name = 'hr_manager' AND org_id =
|
||||||
|
(SELECT id FROM morbac.orgs WHERE name = 'Acme Corp')),
|
||||||
|
(SELECT id FROM morbac.orgs WHERE name = 'Acme Corp')
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 7. Now Bob can assign users to employee/manager roles
|
||||||
|
SELECT morbac.admin_assign_role(
|
||||||
|
'bob-uuid'::uuid,
|
||||||
|
'charlie-uuid'::uuid,
|
||||||
|
(SELECT id FROM morbac.roles WHERE name = 'employee' AND org_id =
|
||||||
|
(SELECT id FROM morbac.orgs WHERE name = 'Acme Corp')),
|
||||||
|
(SELECT id FROM morbac.orgs WHERE name = 'Acme Corp')
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Multi-OrBAC provides complete delegation of administrative capabilities:
|
||||||
|
|
||||||
|
- **No superadmins needed** for day-to-day operations
|
||||||
|
- **Organization-scoped** - admins only affect their own org
|
||||||
|
- **Fine-grained** - control exactly what each admin role can do
|
||||||
|
- **Safe** - automatic constraint validation (SoD, cardinality)
|
||||||
|
- **Auditable** - all actions go through tracked functions
|
||||||
|
- **Context-aware** - limit when admin actions can occur
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.0.0] - 2026-02-19
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial release of Multi-OrBAC PostgreSQL extension
|
||||||
|
- Complete Multi-OrBAC implementation based on CNRS research paper
|
||||||
|
- Core features:
|
||||||
|
- Organization-centric access control
|
||||||
|
- Role-based abstraction with organizational scope
|
||||||
|
- Activity and view abstractions
|
||||||
|
- Context-based rule evaluation
|
||||||
|
- Four deontic modalities (permission, prohibition, obligation, recommendation)
|
||||||
|
- Prohibition precedence over permissions
|
||||||
|
- Multi-organization support
|
||||||
|
- Advanced features:
|
||||||
|
- Organization, role, activity, and view hierarchies with transitive closure
|
||||||
|
- Temporal delegation with time bounds
|
||||||
|
- Negative role assignments
|
||||||
|
- Separation of Duty (SoD) constraints
|
||||||
|
- Role cardinality constraints (min/max users)
|
||||||
|
- Derived roles (computed via functions)
|
||||||
|
- Cross-organizational rules
|
||||||
|
- Administration rules (meta-policies)
|
||||||
|
- Policy DSL with idempotent compiler
|
||||||
|
- RLS helper functions for PostgREST integration
|
||||||
|
- Comprehensive test suite with 20 test scenarios
|
||||||
|
- Complete documentation
|
||||||
|
- Build and installation automation (Makefile, install.sh)
|
||||||
|
|
||||||
|
[1.0.0]: https://github.com/yourusername/morbac_pg/releases/tag/v1.0.0
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# Contributing to morbac_pg
|
||||||
|
|
||||||
|
Thank you for your interest in contributing to morbac_pg!
|
||||||
|
|
||||||
|
## How to Contribute
|
||||||
|
|
||||||
|
### Reporting Bugs
|
||||||
|
|
||||||
|
If you find a bug, please open an issue with:
|
||||||
|
- A clear description of the bug
|
||||||
|
- Steps to reproduce
|
||||||
|
- Expected vs actual behavior
|
||||||
|
- PostgreSQL version
|
||||||
|
- Relevant logs or error messages
|
||||||
|
|
||||||
|
### Suggesting Features
|
||||||
|
|
||||||
|
Feature requests are welcome! Please open an issue describing:
|
||||||
|
- The use case or problem you're trying to solve
|
||||||
|
- How the feature would work
|
||||||
|
- Whether it aligns with Multi-OrBAC model principles
|
||||||
|
|
||||||
|
### Code Contributions
|
||||||
|
|
||||||
|
1. **Fork the repository**
|
||||||
|
2. **Create a feature branch**: `git checkout -b feature/my-feature`
|
||||||
|
3. **Make your changes**:
|
||||||
|
- Follow the existing code style (SQL formatting, naming conventions)
|
||||||
|
- Add tests in `test_morbac.sql` for new features
|
||||||
|
- Update `DOCUMENTATION.md` if adding new functionality
|
||||||
|
4. **Test your changes**: `make test`
|
||||||
|
5. **Commit with clear messages**: `git commit -m "Add feature: description"`
|
||||||
|
6. **Push to your fork**: `git push origin feature/my-feature`
|
||||||
|
7. **Open a Pull Request** with a description of your changes
|
||||||
|
|
||||||
|
### Code Style Guidelines
|
||||||
|
|
||||||
|
- **SQL**: Use uppercase for keywords, lowercase for identifiers
|
||||||
|
- **Naming**: Use snake_case for tables, columns, functions
|
||||||
|
- **Schema**: All objects must be in the `morbac` schema
|
||||||
|
- **Comments**: Add comments for complex logic
|
||||||
|
- **Functions**: Include COMMENT ON statements for public functions
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
All changes must include tests:
|
||||||
|
- Add test cases to `test_morbac.sql`
|
||||||
|
- Ensure all existing tests still pass
|
||||||
|
- Test against PostgreSQL 12+ (mention version in PR)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Update `DOCUMENTATION.md` for API changes
|
||||||
|
- Update `CHANGELOG.md` following [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format
|
||||||
|
- Keep `README.md` simple (link to docs instead of expanding it)
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install PostgreSQL (if not already installed)
|
||||||
|
brew install postgresql # macOS
|
||||||
|
# or use your package manager
|
||||||
|
|
||||||
|
# Clone your fork
|
||||||
|
git clone https://github.com/your-username/morbac_pg.git
|
||||||
|
cd morbac_pg
|
||||||
|
|
||||||
|
# Install the extension
|
||||||
|
make install
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
make test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Questions?
|
||||||
|
|
||||||
|
Feel free to open an issue for questions or discussion.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
By contributing, you agree that your contributions will be licensed under the MIT License.
|
||||||
@@ -0,0 +1,770 @@
|
|||||||
|
# morbac_pg Documentation
|
||||||
|
|
||||||
|
Complete technical documentation for the Multi-OrBAC PostgreSQL extension.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Architecture](#architecture)
|
||||||
|
- [Schema Reference](#schema-reference)
|
||||||
|
- [Core Concepts](#core-concepts)
|
||||||
|
- [Advanced Features](#advanced-features)
|
||||||
|
- [API Reference](#api-reference)
|
||||||
|
- [Integration Guides](#integration-guides)
|
||||||
|
- [Performance](#performance)
|
||||||
|
- [Security Model](#security-model)
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Design Principles
|
||||||
|
|
||||||
|
morbac_pg is built on these principles:
|
||||||
|
|
||||||
|
1. **Pure PostgreSQL** - No external dependencies (except pgcrypto)
|
||||||
|
2. **Schema Isolation** - All objects in `morbac` schema
|
||||||
|
3. **Default Deny** - No permission = access denied
|
||||||
|
4. **Prohibition Precedence** - Prohibitions checked first, always override permissions
|
||||||
|
5. **Organization-Centric** - All policies scoped to organizations
|
||||||
|
6. **Multi-Tenant Native** - Users and resources can span organizations
|
||||||
|
|
||||||
|
### Authorization Flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[Authorization Request] --> B[Get Comprehensive Roles]
|
||||||
|
B --> C[Direct Assignments]
|
||||||
|
B --> D[Delegations]
|
||||||
|
B --> E[Derived Roles]
|
||||||
|
B --> F[Role Hierarchy]
|
||||||
|
C --> G[Filter Negative Assignments]
|
||||||
|
D --> G
|
||||||
|
E --> G
|
||||||
|
F --> G
|
||||||
|
G --> H{Check Prohibitions}
|
||||||
|
H -->|Found| I[DENY]
|
||||||
|
H -->|Not Found| J{Check Permissions}
|
||||||
|
J -->|Found| K[ALLOW]
|
||||||
|
J -->|Not Found| L[DENY - Default]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance Optimizations
|
||||||
|
|
||||||
|
**Prohibition-first checking:** Prohibitions are checked before permissions for early exit on denial.
|
||||||
|
|
||||||
|
**Indexed foreign keys:** All foreign key relationships have indexes for fast joins.
|
||||||
|
|
||||||
|
**Composite indexes:** Optimized indexes for common query patterns (org_id, role_id, activity, view).
|
||||||
|
|
||||||
|
**STABLE functions:** RLS functions are marked STABLE for transaction-level caching.
|
||||||
|
|
||||||
|
**Recursive CTEs:** Efficient hierarchy traversal using PostgreSQL's recursive common table expressions.
|
||||||
|
|
||||||
|
## Schema Reference
|
||||||
|
|
||||||
|
### Entity Relationship Diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
erDiagram
|
||||||
|
orgs ||--o{ orgs : "parent_id"
|
||||||
|
orgs ||--o{ roles : "has"
|
||||||
|
orgs ||--o{ user_roles : "contains"
|
||||||
|
orgs ||--o{ rules : "defines"
|
||||||
|
|
||||||
|
roles ||--o{ user_roles : "assigned_to"
|
||||||
|
roles ||--o{ role_hierarchy : "senior"
|
||||||
|
roles ||--o{ role_hierarchy : "junior"
|
||||||
|
roles ||--o{ rules : "applies_to"
|
||||||
|
|
||||||
|
activities ||--o{ rules : "action"
|
||||||
|
activities ||--o{ activity_hierarchy : "parent"
|
||||||
|
activities ||--o{ activity_hierarchy : "child"
|
||||||
|
|
||||||
|
views ||--o{ rules : "target"
|
||||||
|
views ||--o{ view_hierarchy : "parent"
|
||||||
|
views ||--o{ view_hierarchy : "child"
|
||||||
|
|
||||||
|
contexts ||--o{ rules : "condition"
|
||||||
|
|
||||||
|
roles ||--o{ delegations : "delegated"
|
||||||
|
roles ||--o{ negative_role_assignments : "prohibited"
|
||||||
|
roles ||--o{ sod_conflicts : "conflicts_with"
|
||||||
|
roles ||--o{ derived_roles : "computed"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Core Tables
|
||||||
|
|
||||||
|
**morbac.orgs** - Organizations with hierarchy support (`parent_id` self-reference). Unique name required. Supports metadata as JSONB.
|
||||||
|
|
||||||
|
**morbac.roles** - Roles scoped to organizations. Unique `(org_id, name)` constraint ensures role names are unique within each org.
|
||||||
|
|
||||||
|
**morbac.role_hierarchy** - Role inheritance via `(senior_role_id, junior_role_id)`. Senior roles inherit all junior permissions. Supports transitive closure.
|
||||||
|
|
||||||
|
**morbac.user_roles** - Direct user-to-role assignments. Primary key on `(user_id, role_id, org_id)`. Note: `user_id` is external (application manages users).
|
||||||
|
|
||||||
|
**morbac.activities** - Abstract actions (global scope). Text primary key. Examples: `read`, `write`, `delete`, `approve`.
|
||||||
|
|
||||||
|
**morbac.activity_hierarchy** - Activity inheritance via `(parent_activity, child_activity)`. Permission to parent grants child activities.
|
||||||
|
|
||||||
|
**morbac.views** - Abstract object categories (global scope). Text primary key. Examples: `documents`, `reports`, `financial_data`.
|
||||||
|
|
||||||
|
**morbac.view_hierarchy** - View inheritance via `(parent_view, child_view)`. Access to parent grants child views.
|
||||||
|
|
||||||
|
**morbac.contexts** - Contextual conditions as callable predicates. Column `evaluator` (REGPROC) references a function returning BOOLEAN (preferably STABLE). Built-in context `always` returns true.
|
||||||
|
|
||||||
|
**morbac.rules** - Core policy rules linking org, role, activity, view, context, and modality. Indexed on `(org_id, role_id, activity, view, modality)` for fast lookups.
|
||||||
|
|
||||||
|
**morbac.policy** - Policy DSL using names instead of UUIDs. Insert here, then call `compile_policy()` to generate rules.
|
||||||
|
|
||||||
|
### Advanced Feature Tables
|
||||||
|
|
||||||
|
#### morbac.delegations
|
||||||
|
|
||||||
|
Temporal role delegation.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE morbac.delegations (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
delegator_user_id UUID NOT NULL,
|
||||||
|
delegate_user_id UUID NOT NULL,
|
||||||
|
role_id UUID NOT NULL REFERENCES morbac.roles(id) ON DELETE CASCADE,
|
||||||
|
org_id UUID NOT NULL REFERENCES morbac.orgs(id) ON DELETE CASCADE,
|
||||||
|
valid_from TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
valid_until TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
CHECK (valid_until IS NULL OR valid_until > valid_from)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Columns:**
|
||||||
|
- `valid_from` - Delegation start time
|
||||||
|
- `valid_until` - Optional end time (NULL = indefinite)
|
||||||
|
|
||||||
|
**Automatic handling:** Included in `get_comprehensive_roles()` when active.
|
||||||
|
|
||||||
|
#### morbac.negative_role_assignments
|
||||||
|
|
||||||
|
Explicit role prohibitions.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE morbac.negative_role_assignments (
|
||||||
|
user_id UUID NOT NULL,
|
||||||
|
role_id UUID NOT NULL REFERENCES morbac.roles(id) ON DELETE CASCADE,
|
||||||
|
org_id UUID NOT NULL REFERENCES morbac.orgs(id) ON DELETE CASCADE,
|
||||||
|
reason TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY (user_id, role_id, org_id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Precedence:** Overrides direct assignments, delegations, and derived roles.
|
||||||
|
|
||||||
|
#### morbac.sod_conflicts
|
||||||
|
|
||||||
|
Separation of Duty constraints.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE morbac.sod_conflicts (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
role1_id UUID NOT NULL REFERENCES morbac.roles(id) ON DELETE CASCADE,
|
||||||
|
role2_id UUID NOT NULL REFERENCES morbac.roles(id) ON DELETE CASCADE,
|
||||||
|
org_id UUID NOT NULL REFERENCES morbac.orgs(id) ON DELETE CASCADE,
|
||||||
|
description TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE(role1_id, role2_id, org_id),
|
||||||
|
CHECK (role1_id < role2_id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage:** Validated via `check_sod_violation()` before role assignment.
|
||||||
|
|
||||||
|
#### morbac.role_cardinality
|
||||||
|
|
||||||
|
Min/max users per role.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE morbac.role_cardinality (
|
||||||
|
role_id UUID PRIMARY KEY REFERENCES morbac.roles(id) ON DELETE CASCADE,
|
||||||
|
org_id UUID NOT NULL REFERENCES morbac.orgs(id) ON DELETE CASCADE,
|
||||||
|
min_users INTEGER NOT NULL DEFAULT 0,
|
||||||
|
max_users INTEGER,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
CHECK (min_users >= 0),
|
||||||
|
CHECK (max_users IS NULL OR max_users >= min_users)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage:** Validated via `check_cardinality_violation()`.
|
||||||
|
|
||||||
|
#### morbac.derived_roles
|
||||||
|
|
||||||
|
Dynamically computed roles.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE morbac.derived_roles (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
role_id UUID NOT NULL REFERENCES morbac.roles(id) ON DELETE CASCADE,
|
||||||
|
org_id UUID NOT NULL REFERENCES morbac.orgs(id) ON DELETE CASCADE,
|
||||||
|
evaluator REGPROC NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Columns:**
|
||||||
|
- `evaluator` - Function returning `TABLE(user_id UUID, org_id UUID)`
|
||||||
|
|
||||||
|
#### morbac.cross_org_rules
|
||||||
|
|
||||||
|
Inter-organizational rules.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE morbac.cross_org_rules (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
source_org_id UUID NOT NULL REFERENCES morbac.orgs(id) ON DELETE CASCADE,
|
||||||
|
target_org_id UUID NOT NULL REFERENCES morbac.orgs(id) ON DELETE CASCADE,
|
||||||
|
role_id UUID NOT NULL REFERENCES morbac.roles(id) ON DELETE CASCADE,
|
||||||
|
activity TEXT NOT NULL REFERENCES morbac.activities(name) ON DELETE CASCADE,
|
||||||
|
view TEXT NOT NULL REFERENCES morbac.views(name) ON DELETE CASCADE,
|
||||||
|
modality morbac.modality NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage:** Allows roles in `source_org` to access resources in `target_org`.
|
||||||
|
|
||||||
|
#### morbac.admin_rules
|
||||||
|
|
||||||
|
Administration meta-policies.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE morbac.admin_rules (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
org_id UUID NOT NULL REFERENCES morbac.orgs(id) ON DELETE CASCADE,
|
||||||
|
role_id UUID NOT NULL REFERENCES morbac.roles(id) ON DELETE CASCADE,
|
||||||
|
can_manage_policies BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
can_manage_roles BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
can_manage_users BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE(org_id, role_id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### Deontic Modalities
|
||||||
|
|
||||||
|
Multi-OrBAC implements four modalities:
|
||||||
|
|
||||||
|
1. **Permission** - Allows action (if no prohibition)
|
||||||
|
2. **Prohibition** - Denies action (always wins)
|
||||||
|
3. **Obligation** - Must be done (informational only)
|
||||||
|
4. **Recommendation** - Should be done (informational only)
|
||||||
|
|
||||||
|
Only permissions and prohibitions affect `is_allowed()` decisions.
|
||||||
|
|
||||||
|
### Context Evaluation
|
||||||
|
|
||||||
|
Contexts are boolean predicates that determine if a rule applies:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Context function signature
|
||||||
|
CREATE OR REPLACE FUNCTION morbac.my_context()
|
||||||
|
RETURNS BOOLEAN
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
STABLE -- Important for RLS performance
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN <condition>;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Register context
|
||||||
|
INSERT INTO morbac.contexts (name, evaluator) VALUES
|
||||||
|
('my_context', 'morbac.my_context'::regproc);
|
||||||
|
```
|
||||||
|
|
||||||
|
Context functions should:
|
||||||
|
|
||||||
|
- Use STABLE (not VOLATILE) for RLS compatibility
|
||||||
|
- Keep logic simple and fast
|
||||||
|
- Avoid external dependencies
|
||||||
|
- Consider caching if computation is expensive
|
||||||
|
|
||||||
|
### Policy DSL
|
||||||
|
|
||||||
|
Simplified policy declaration:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Insert policies using names
|
||||||
|
INSERT INTO morbac.policy (org_name, role_name, activity, view, modality) VALUES
|
||||||
|
('Acme Corp', 'manager', 'approve', 'requests', 'permission');
|
||||||
|
|
||||||
|
-- Compile (idempotent)
|
||||||
|
SELECT * FROM morbac.compile_policy();
|
||||||
|
```
|
||||||
|
|
||||||
|
Compiler behavior:
|
||||||
|
- Creates missing activities/views
|
||||||
|
- Resolves names to UUIDs
|
||||||
|
- Creates entries in `morbac.rules`
|
||||||
|
- Reports errors for missing orgs/roles/contexts
|
||||||
|
- Safe to run multiple times
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
### Hierarchies
|
||||||
|
|
||||||
|
Organizations, roles, activities, and views all support hierarchical relationships with transitive closure.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Organization hierarchy (via parent_id)
|
||||||
|
INSERT INTO morbac.orgs (name, parent_id) VALUES ('EMEA', parent_org_id);
|
||||||
|
SELECT * FROM morbac.get_org_ancestors(org_id);
|
||||||
|
SELECT * FROM morbac.get_org_descendants(org_id);
|
||||||
|
|
||||||
|
-- Role hierarchy (senior inherits from junior)
|
||||||
|
INSERT INTO morbac.role_hierarchy (senior_role_id, junior_role_id)
|
||||||
|
VALUES (manager_role_id, employee_role_id);
|
||||||
|
SELECT * FROM morbac.get_inherited_roles(role_id);
|
||||||
|
|
||||||
|
-- Activity hierarchy (parent includes children)
|
||||||
|
INSERT INTO morbac.activity_hierarchy (parent_activity, child_activity)
|
||||||
|
VALUES ('write', 'create'), ('write', 'update');
|
||||||
|
SELECT * FROM morbac.get_effective_activities('write');
|
||||||
|
|
||||||
|
-- View hierarchy (parent includes children)
|
||||||
|
INSERT INTO morbac.view_hierarchy (parent_view, child_view)
|
||||||
|
VALUES ('documents', 'invoices'), ('documents', 'contracts');
|
||||||
|
SELECT * FROM morbac.get_effective_views('documents');
|
||||||
|
```
|
||||||
|
|
||||||
|
Example hierarchy diagram:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[Director] -->|inherits from| B[Manager]
|
||||||
|
B -->|inherits from| C[Employee]
|
||||||
|
A -.->|transitive| C
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delegation
|
||||||
|
|
||||||
|
Temporary role delegation with time bounds:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO morbac.delegations (
|
||||||
|
delegator_user_id, delegate_user_id, role_id, org_id,
|
||||||
|
valid_from, valid_until
|
||||||
|
) VALUES (
|
||||||
|
alice_uuid, bob_uuid, approver_role_id, org_id,
|
||||||
|
NOW(), NOW() + INTERVAL '7 days'
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Automatically included in `get_comprehensive_roles()` when active. Set `valid_until` to NULL for indefinite delegation.
|
||||||
|
|
||||||
|
### Negative Role Assignments
|
||||||
|
|
||||||
|
Explicit prohibitions with highest precedence:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO morbac.negative_role_assignments (user_id, role_id, org_id, reason)
|
||||||
|
VALUES (user_uuid, admin_role_id, org_id, 'Security audit requirement');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Separation of Duty
|
||||||
|
|
||||||
|
Mutually exclusive role constraints:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO morbac.sod_conflicts (role1_id, role2_id, org_id, description)
|
||||||
|
VALUES (preparer_role_id, approver_role_id, org_id, 'Cannot prepare and approve same transaction');
|
||||||
|
|
||||||
|
SELECT morbac.check_sod_violation(user_uuid, org_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cardinality Constraints
|
||||||
|
|
||||||
|
Min/max users per role:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO morbac.role_cardinality (role_id, org_id, min_users, max_users)
|
||||||
|
VALUES (admin_role_id, org_id, 1, 3);
|
||||||
|
|
||||||
|
SELECT morbac.check_cardinality_violation(admin_role_id, org_id);
|
||||||
|
```
|
||||||
|
Check before INSERT INTO user_roles
|
||||||
|
```sql
|
||||||
|
-- Require 1-3 admins
|
||||||
|
INSERT INTO morbac.role_cardinality (role_id, org_id, min_users, max_users)
|
||||||
|
VALUES (admin_role_id, org_id, 1, 3);
|
||||||
|
|
||||||
|
-- Validate
|
||||||
|
SELECT morbac.check_cardinality_violation(admin_role_id, org_id);
|
||||||
|
-- Returns: TEXT (error message) or NULL (valid)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Derived Roles
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Define evaluator
|
||||||
|
### Derived Roles
|
||||||
|
|
||||||
|
Dynamically computed roles via custom functions:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE OR REPLACE FUNCTION morbac.eval_project_leads()
|
||||||
|
RETURNS TABLE(user_id UUID, org_id UUID)
|
||||||
|
LANGUAGE sql STABLE AS $$
|
||||||
|
SELECT user_id, org_id FROM app.projects WHERE lead_user_id IS NOT NULL;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
INSERT INTO morbac.derived_roles (role_id, org_id, evaluator)
|
||||||
|
VALUES (project_lead_role_id, org_id, 'morbac.eval_project_leads'::regproc);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cross-Organizational Rules
|
||||||
|
|
||||||
|
Inter-organizational access policies:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO morbac.cross_org_rules (
|
||||||
|
source_org_id, target_org_id, role_id, activity, view, modality
|
||||||
|
) VALUES (
|
||||||
|
global_org_id, subsidiary_org_id, auditor_role_id,
|
||||||
|
'read', 'financial_reports', 'permission'
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Administration Rules
|
||||||
|
|
||||||
|
Meta-policies for policy management:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO morbac.admin_rules (org_id, role_id, can_manage_policies)
|
||||||
|
VALUES (org_id, sec_admin_role_id, true);
|
||||||
|
|
||||||
|
SELECT morbac.is_admin_allowed(user_uuid, org_id, 'manage_policies');
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Authorization Functions
|
||||||
|
|
||||||
|
**`is_allowed(user_id, org_id, activity, view)`** - Main authorization decision. Returns BOOLEAN. Checks prohibitions first, then permissions, defaults to deny.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT morbac.is_allowed(user_uuid, org_uuid, 'read', 'documents');
|
||||||
|
```
|
||||||
|
|
||||||
|
**`get_comprehensive_roles(user_id, org_id)`** - Returns all roles for user (direct, delegated, derived, hierarchy, minus negative assignments).
|
||||||
|
|
||||||
|
**`get_effective_roles(user_id, org_id)`** - Alias for `get_comprehensive_roles()`.
|
||||||
|
|
||||||
|
### Hierarchy Functions
|
||||||
|
|
||||||
|
**`get_org_ancestors(org_id)`** - Returns all parent organizations with depth.
|
||||||
|
|
||||||
|
**`get_org_descendants(org_id)`** - Returns all child organizations with depth.
|
||||||
|
|
||||||
|
**`get_inherited_roles(role_id)`** - Returns all junior roles (transitive).
|
||||||
|
|
||||||
|
**`get_effective_activities(activity)`** - Returns activity plus all child activities.
|
||||||
|
|
||||||
|
**`get_effective_views(view)`** - Returns view plus all child views.
|
||||||
|
|
||||||
|
### Validation Functions
|
||||||
|
|
||||||
|
**`check_sod_violation(user_id, org_id)`** - Returns TEXT[] of conflicting role pairs or empty array.
|
||||||
|
|
||||||
|
**`check_cardinality_violation(role_id, org_id)`** - Returns error message or NULL if valid.
|
||||||
|
|
||||||
|
### Administration Functions
|
||||||
|
|
||||||
|
**`is_admin_allowed(user_id, org_id, capability)`** - Check administrative permissions. Capabilities: `manage_policies`, `manage_roles`, `manage_users`.
|
||||||
|
|
||||||
|
**`eval_derived_role(user_id, org_id, derived_role_id)`** - Evaluate if user has derived role.
|
||||||
|
|
||||||
|
### Context Functions
|
||||||
|
|
||||||
|
**`eval_context(context_id)`** - Evaluate a context predicate.
|
||||||
|
|
||||||
|
### Policy DSL Functions
|
||||||
|
|
||||||
|
**`compile_policy()`** - Compile policy DSL into rules. Returns TABLE with success status and messages. Idempotent.
|
||||||
|
|
||||||
|
### RLS Helper Functions
|
||||||
|
|
||||||
|
**`current_user_id()`** - Get user ID from `request.header.x-user-id` (PostgREST) or `current_setting('morbac.user_id')`.
|
||||||
|
|
||||||
|
**`current_org_id()`** - Get org ID from `request.header.x-org-id` (PostgREST) or `current_setting('morbac.org_id')`.
|
||||||
|
|
||||||
|
**`rls_check(activity, view)`** - Authorization check for RLS policies using current user/org context.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE POLICY my_policy ON app.table
|
||||||
|
FOR SELECT USING (morbac.rls_check('read', 'documents'));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Informational Functions
|
||||||
|
|
||||||
|
**`pending_obligations(user_id, org_id)`** - Returns obligations for user (informational only).
|
||||||
|
|
||||||
|
**`pending_recommendations(user_id, org_id)`** - Returns recommendations for user (informational only).
|
||||||
|
|
||||||
|
**`user_has_role(user_id, org_id, role_name)`** - Check if user has specific role by name.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
morbac.user_has_role(
|
||||||
|
p_user_id UUID,
|
||||||
|
p_org_id UUID,
|
||||||
|
p_role_name TEXT
|
||||||
|
) RETURNS BOOLEAN
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration Guides
|
||||||
|
|
||||||
|
### PostgREST Integration
|
||||||
|
|
||||||
|
#### 1. Setup Headers
|
||||||
|
|
||||||
|
Configure PostgREST to pass user/org context:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# Nginx config
|
||||||
|
proxy_set_header X-User-Id $user_id;
|
||||||
|
proxy_set_header X-Org-Id $org_id;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Enable RLS
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE app.documents ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Grant usage to PostgREST role
|
||||||
|
GRANT USAGE ON SCHEMA morbac TO postgrest_role;
|
||||||
|
GRANT SELECT ON ALL TABLES IN SCHEMA morbac TO postgrest_role;
|
||||||
|
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA morbac TO postgrest_role;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Create RLS Policies
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE POLICY document_read ON app.documents
|
||||||
|
FOR SELECT
|
||||||
|
USING (morbac.rls_check('read', 'documents'));
|
||||||
|
|
||||||
|
CREATE POLICY document_write ON app.documents
|
||||||
|
FOR INSERT
|
||||||
|
WITH CHECK (morbac.rls_check('write', 'documents'));
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Set Context (alternative to headers)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- In application connection
|
||||||
|
SET morbac.user_id = '123e4567-e89b-12d3-a456-426614174000';
|
||||||
|
SET morbac.org_id = '987fcdeb-51a2-43d7-9c6e-5a8b7c9d0e1f';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration Guides
|
||||||
|
|
||||||
|
### PostgREST Integration
|
||||||
|
|
||||||
|
Configure PostgREST to pass headers, enable RLS, and create policies:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Enable RLS
|
||||||
|
ALTER TABLE app.documents ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
GRANT USAGE ON SCHEMA morbac TO postgrest_role;
|
||||||
|
GRANT SELECT ON ALL TABLES IN SCHEMA morbac TO postgrest_role;
|
||||||
|
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA morbac TO postgrest_role;
|
||||||
|
|
||||||
|
-- Create RLS policies
|
||||||
|
CREATE POLICY document_read ON app.documents
|
||||||
|
FOR SELECT USING (morbac.rls_check('read', 'documents'));
|
||||||
|
|
||||||
|
-- Alternative: Set context directly
|
||||||
|
SET morbac.user_id = '123e4567...';
|
||||||
|
SET morbac.org_id = '987fcdeb...';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Application Integration
|
||||||
|
|
||||||
|
Python example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import psycopg2
|
||||||
|
conn = psycopg2.connect("dbname=mydb")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SET morbac.user_id = %s", [user_id])
|
||||||
|
cursor.execute("SET morbac.org_id = %s", [org_id])
|
||||||
|
cursor.execute("SELECT * FROM app.documents") # RLS enforced
|
||||||
|
```
|
||||||
|
|
||||||
|
Node.js example:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const client = await pool.connect();
|
||||||
|
await client.query('SET morbac.user_id = $1', [userId]);
|
||||||
|
await client.query('SET morbac.org_id = $1', [orgId]);
|
||||||
|
const res = await client.query('SELECT * FROM app.documents');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-Organization Resources
|
||||||
|
|
||||||
|
Resources can belong to multiple organizations:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE app.document_orgs (
|
||||||
|
document_id UUID,
|
||||||
|
org_id UUID,
|
||||||
|
PRIMARY KEY (document_id, org_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE POLICY doc_access ON app.documents FOR SELECT USING (
|
||||||
|
EXISTS (SELECT 1 FROM app.document_orgs
|
||||||
|
WHERE document_id = app.documents.id
|
||||||
|
AND org_id = morbac.current_org_id())
|
||||||
|
AND morbac.rls_check('read', 'documents')
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
### Optimization Strategies
|
||||||
|
|
||||||
|
Prohibition-first checking provides early exit on denial for both security and performance.
|
||||||
|
|
||||||
|
For large hierarchies, use materialized views:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE MATERIALIZED VIEW morbac.mv_role_closure AS
|
||||||
|
SELECT senior_role_id, junior_role_id FROM ...;
|
||||||
|
CREATE INDEX ON morbac.mv_role_closure(senior_role_id);
|
||||||
|
REFRESH MATERIALIZED VIEW morbac.mv_role_closure;
|
||||||
|
```
|
||||||
|
|
||||||
|
Context optimization:
|
||||||
|
- Use STABLE functions (not VOLATILE) for RLS caching
|
||||||
|
- Avoid expensive computations or external calls
|
||||||
|
- Keep context logic simple
|
||||||
|
|
||||||
|
All foreign keys are indexed. Critical composite indexes exist on `(user_id, org_id)` and `(org_id, role_id, activity, view, modality)`.
|
||||||
|
CREATE INDEX idx_rules_lookup ON morbac.rules(org_id, role_id, activity, view, modality);
|
||||||
|
|
||||||
|
-- Hierarchy traversal
|
||||||
|
CREATE INDEX idx_role_hierarchy_senior ON morbac.role_hierarchy(senior_role_id);
|
||||||
|
CREATE INDEX idx_role_hierarchy_junior ON morbac.role_hierarchy(junior_role_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Benchmarking
|
||||||
|
|
||||||
|
Example query performance (1M rules, 10K users, 100 orgs):
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Simple authorization check
|
||||||
|
EXPLAIN ANALYZE
|
||||||
|
SELECT morbac.is_allowed(user_id, org_id, 'read', 'documents');
|
||||||
|
-- Typical: 5-15ms
|
||||||
|
|
||||||
|
-- With hierarchies (3 levels)
|
||||||
|
EXPLAIN ANALYZE
|
||||||
|
SELECT morbac.is_allowed(user_id, org_id, 'read', 'documents');
|
||||||
|
-- Typical: 10-30ms
|
||||||
|
|
||||||
|
-- RLS query (1M rows)
|
||||||
|
EXPLAIN ANALYZE
|
||||||
|
SELECT * FROM app.documents WHERE morbac.rls_check('read', 'documents');
|
||||||
|
-- Depends on selectivity, uses indexes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Model
|
||||||
|
|
||||||
|
### Threat Model
|
||||||
|
|
||||||
|
Protected against:
|
||||||
|
- Unauthorized access (default deny)
|
||||||
|
- Privilege escalation (prohibition precedence)
|
||||||
|
- SQL injection (parameterized queries)
|
||||||
|
- Org boundary violations (explicit cross-org rules)
|
||||||
|
|
||||||
|
Not protected against:
|
||||||
|
- Malicious context functions (trusted code)
|
||||||
|
- Database user privilege escalation (PostgreSQL security model)
|
||||||
|
- Time-of-check/time-of-use races (transaction-based consistency only)
|
||||||
|
|
||||||
|
### Security Best Practices
|
||||||
|
|
||||||
|
#### 1. Context Function Security
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- BAD: Leaks information
|
||||||
|
CREATE FUNCTION morbac.bad_context()
|
||||||
|
RETURNS BOOLEAN VOLATILE AS $$
|
||||||
|
SELECT EXISTS(SELECT 1 FROM sensitive_table);
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- GOOD: Self-contained, stable
|
||||||
|
CREATE FUNCTION morbac.good_context()
|
||||||
|
RETURNS BOOLEAN STABLE AS $$
|
||||||
|
SELECT EXTRACT(HOUR FROM CURRENT_TIME) BETWEEN 9 AND 17;
|
||||||
|
$$;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. User ID Validation
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Validate user exists before authorization
|
||||||
|
SELECT morbac.is_allowed(
|
||||||
|
user_id, -- Must be from authenticated session
|
||||||
|
org_id,
|
||||||
|
activity,
|
||||||
|
view
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Org Context Validation
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Verify user belongs to org
|
||||||
|
SELECT EXISTS(
|
||||||
|
SELECT 1 FROM morbac.user_roles
|
||||||
|
WHERE user_id = ? AND org_id = ?
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Delegation Auditing
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Log delegations
|
||||||
|
CREATE TABLE app.delegation_audit (
|
||||||
|
delegation_id UUID REFERENCES morbac.delegations(id),
|
||||||
|
action TEXT,
|
||||||
|
by_user UUID,
|
||||||
|
at_time TIMESTAMPTZ DEFAULT now()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comparison: Multi-OrBAC vs Traditional RBAC
|
||||||
|
|
||||||
|
| Aspect | Traditional RBAC | Multi-OrBAC (morbac_pg) |
|
||||||
|
|--------|-----------------|-------------------------|
|
||||||
|
| **Multi-tenancy** | Single tenant or complex workarounds | Native multi-organization |
|
||||||
|
| **Prohibition** | No standard support | First-class, always wins |
|
||||||
|
| **Context** | Static permissions | Dynamic, condition-based |
|
||||||
|
| **Hierarchy** | Basic (roles only) | Full (orgs, roles, activities, views) |
|
||||||
|
| **Delegation** | Manual implementation | Native temporal delegation |
|
||||||
|
| **SoD** | Application logic | Database-enforced constraints |
|
||||||
|
| **Cross-tenant** | Not supported | Native cross-org rules |
|
||||||
|
| **Audit** | Application layer | Meta-policies (admin rules) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
- [Multi-OrBAC Research Paper](https://webhost.laas.fr/TSF/deswarte/Publications/06427.pdf)
|
||||||
|
- [test_morbac.sql](test_morbac.sql) - Comprehensive examples
|
||||||
|
- [CHANGELOG.md](CHANGELOG.md) - Version history
|
||||||
|
- [CONTRIBUTING.md](CONTRIBUTING.md) - Contribution guidelines
|
||||||
|
- [SECURITY.md](SECURITY.md) - Security policy
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 morbac_pg contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# Makefile for morbac_pg PostgreSQL Extension
|
||||||
|
# Multi-OrBAC: Organization-Based Access Control
|
||||||
|
|
||||||
|
EXTENSION = morbac_pg
|
||||||
|
EXTVERSION = 1.0
|
||||||
|
|
||||||
|
DATA = $(EXTENSION)--$(EXTVERSION).sql
|
||||||
|
DOCS = README.md
|
||||||
|
|
||||||
|
# PostgreSQL extension build
|
||||||
|
PG_CONFIG = pg_config
|
||||||
|
PGXS := $(shell $(PG_CONFIG) --pgxs)
|
||||||
|
include $(PGXS)
|
||||||
|
|
||||||
|
# Custom targets
|
||||||
|
|
||||||
|
# Install extension in PostgreSQL
|
||||||
|
.PHONY: install
|
||||||
|
install:
|
||||||
|
@echo "Installing morbac_pg extension..."
|
||||||
|
$(MAKE) -f Makefile install
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
.PHONY: test
|
||||||
|
test:
|
||||||
|
@echo "Running morbac_pg tests..."
|
||||||
|
psql -f test_morbac.sql
|
||||||
|
|
||||||
|
# Run tests on specific database
|
||||||
|
.PHONY: test-db
|
||||||
|
test-db:
|
||||||
|
@if [ -z "$(DB)" ]; then \
|
||||||
|
echo "Usage: make test-db DB=your_database"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
@echo "Running tests on database $(DB)..."
|
||||||
|
psql -d $(DB) -f test_morbac.sql
|
||||||
|
|
||||||
|
# Run tests in dedicated test database
|
||||||
|
.PHONY: test
|
||||||
|
test:
|
||||||
|
@echo "Creating test database morbac_test..."
|
||||||
|
-dropdb morbac_test 2>/dev/null || true
|
||||||
|
createdb morbac_test
|
||||||
|
@echo "Running tests..."
|
||||||
|
psql -d morbac_test -f test_morbac.sql
|
||||||
|
@echo ""
|
||||||
|
@echo "Tests completed. Database 'morbac_test' is available."
|
||||||
|
@echo "To explore: psql -d morbac_test"
|
||||||
|
@echo "To cleanup: dropdb morbac_test"
|
||||||
|
|
||||||
|
# Cleanup test database
|
||||||
|
.PHONY: cleanup
|
||||||
|
cleanup:
|
||||||
|
@echo "Dropping test database..."
|
||||||
|
-dropdb morbac_test 2>/dev/null && echo "Test database dropped." || echo "No test database found."
|
||||||
|
|
||||||
|
# Uninstall extension
|
||||||
|
.PHONY: uninstall
|
||||||
|
uninstall:
|
||||||
|
@echo "Uninstalling morbac_pg extension..."
|
||||||
|
$(MAKE) -f Makefile uninstall
|
||||||
|
|
||||||
|
# Show help
|
||||||
|
.PHONY: help
|
||||||
|
help:
|
||||||
|
@echo "morbac_pg Makefile targets:"
|
||||||
|
@echo ""
|
||||||
|
@echo " make install - Install extension in PostgreSQL"
|
||||||
|
@echo " make uninstall - Uninstall extension from PostgreSQL"
|
||||||
|
@echo " make test - Run tests on default database"
|
||||||
|
@echo " make test-db DB=name - Run tests on specified database"
|
||||||
|
@echo " make test - Create test DB, run tests, keep DB"
|
||||||
|
@echo " make cleanup - Drop test database"
|
||||||
|
@echo " make help - Show this help"
|
||||||
|
@echo ""
|
||||||
|
@echo "Quick start:"
|
||||||
|
@echo " make test # Run complete test in isolated database"
|
||||||
|
@echo ""
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
# morbac_pg
|
||||||
|
|
||||||
|
**Multi-Organization Based Access Control for PostgreSQL**
|
||||||
|
|
||||||
|
A PostgreSQL extension implementing the Multi-OrBAC access control model - enabling organization-centric, context-aware, and hierarchical access control with advanced features like delegation, separation of duty, and cross-organizational policies.
|
||||||
|
|
||||||
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
[](https://www.postgresql.org/)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Multi-organization with organizational hierarchy
|
||||||
|
- Role-based access with full hierarchy support
|
||||||
|
- Activity and view hierarchies with transitive permission inheritance
|
||||||
|
- Prohibition precedence over permissions
|
||||||
|
- Temporal delegation with time bounds
|
||||||
|
- Separation of duty constraints
|
||||||
|
- Derived roles computed from application logic
|
||||||
|
- Cross-organizational policies
|
||||||
|
- Context-aware rules
|
||||||
|
- Row-level security integration
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- PostgreSQL 12 or higher
|
||||||
|
- `pgcrypto` extension (included with PostgreSQL)
|
||||||
|
|
||||||
|
### Quick Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone repository
|
||||||
|
git clone https://github.com/yourusername/morbac_pg.git
|
||||||
|
cd morbac_pg
|
||||||
|
|
||||||
|
# Install extension
|
||||||
|
sudo make install
|
||||||
|
|
||||||
|
# Enable in your database
|
||||||
|
psql -d mydb -c "CREATE EXTENSION morbac_pg;"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy files to PostgreSQL extension directory
|
||||||
|
sudo cp morbac_pg.control $(pg_config --sharedir)/extension/
|
||||||
|
sudo cp morbac_pg--1.0.sql $(pg_config --sharedir)/extension/
|
||||||
|
|
||||||
|
# Enable in PostgreSQL
|
||||||
|
psql -d mydb -c "CREATE EXTENSION morbac_pg;"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 1. Create organization
|
||||||
|
INSERT INTO morbac.orgs (name) VALUES ('Acme Corp');
|
||||||
|
|
||||||
|
-- 2. Create role
|
||||||
|
INSERT INTO morbac.roles (org_id, name)
|
||||||
|
SELECT id, 'employee' FROM morbac.orgs WHERE name = 'Acme Corp';
|
||||||
|
|
||||||
|
-- 3. Assign user to role
|
||||||
|
INSERT INTO morbac.user_roles (user_id, role_id, org_id)
|
||||||
|
SELECT
|
||||||
|
'00000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
r.id,
|
||||||
|
o.id
|
||||||
|
FROM morbac.orgs o
|
||||||
|
JOIN morbac.roles r ON r.org_id = o.id
|
||||||
|
WHERE o.name = 'Acme Corp' AND r.name = 'employee';
|
||||||
|
|
||||||
|
-- 4. Define policy
|
||||||
|
INSERT INTO morbac.policy (org_name, role_name, activity, view, modality)
|
||||||
|
VALUES ('Acme Corp', 'employee', 'read', 'documents', 'permission');
|
||||||
|
|
||||||
|
-- 5. Compile policy
|
||||||
|
SELECT * FROM morbac.compile_policy();
|
||||||
|
|
||||||
|
-- 6. Check authorization
|
||||||
|
SELECT morbac.is_allowed(
|
||||||
|
'00000000-0000-0000-0000-000000000001'::uuid,
|
||||||
|
(SELECT id FROM morbac.orgs WHERE name = 'Acme Corp'),
|
||||||
|
'read',
|
||||||
|
'documents'
|
||||||
|
); -- Returns: true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Authorization Check
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT morbac.is_allowed(user_id, org_id, activity, view);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Row-Level Security (RLS)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Enable RLS on your table
|
||||||
|
ALTER TABLE app.documents ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Create policy using Multi-OrBAC
|
||||||
|
CREATE POLICY doc_access ON app.documents
|
||||||
|
FOR SELECT
|
||||||
|
USING (morbac.rls_check('read', 'documents'));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Features
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Delegation: Alice delegates role to Bob for 1 week
|
||||||
|
INSERT INTO morbac.delegations (delegator_user_id, delegate_user_id, role_id, org_id, valid_until)
|
||||||
|
VALUES (alice_id, bob_id, role_id, org_id, NOW() + INTERVAL '7 days');
|
||||||
|
|
||||||
|
-- Separation of Duty: Preparer and approver are mutually exclusive
|
||||||
|
INSERT INTO morbac.sod_conflicts (role1_id, role2_id, org_id)
|
||||||
|
VALUES (preparer_role_id, approver_role_id, org_id);
|
||||||
|
|
||||||
|
-- Cross-Org: Global auditor can access subsidiary
|
||||||
|
INSERT INTO morbac.cross_org_rules (source_org_id, target_org_id, role_id, activity, view, modality)
|
||||||
|
VALUES (global_org_id, subsidiary_org_id, auditor_role_id, 'read', 'reports', 'permission');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test # Run test suite in dedicated database
|
||||||
|
make test-db DB=mydb # Run on existing database
|
||||||
|
make cleanup # Remove test database
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [DOCUMENTATION.md](DOCUMENTATION.md) - Architecture, API reference, integration guides
|
||||||
|
- [ADMIN_GUIDE.md](ADMIN_GUIDE.md) - Organization administrator setup and delegation
|
||||||
|
- [test_morbac.sql](test_morbac.sql) - Test scenarios and examples
|
||||||
|
- [CHANGELOG.md](CHANGELOG.md) - Version history
|
||||||
|
- [CONTRIBUTING.md](CONTRIBUTING.md) - Contribution guidelines
|
||||||
|
- [SECURITY.md](SECURITY.md) - Security policy
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Authorization decision flow:
|
||||||
|
|
||||||
|
1. Collect all roles for user (direct, delegated, derived, hierarchy)
|
||||||
|
2. Filter out negative role assignments
|
||||||
|
3. Check for prohibitions - if found, deny access
|
||||||
|
4. Check for permissions - if found, allow access
|
||||||
|
5. Default deny
|
||||||
|
|
||||||
|
Prohibitions always override permissions.
|
||||||
|
|
||||||
|
## Research
|
||||||
|
|
||||||
|
Based on the CNRS research paper:
|
||||||
|
["Extending the OrBAC model to handle multi-organization environments"](https://webhost.laas.fr/TSF/deswarte/Publications/06427.pdf)
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/yourusername/morbac_pg.git
|
||||||
|
cd morbac_pg
|
||||||
|
make install
|
||||||
|
make test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- Read the [documentation](DOCUMENTATION.md)
|
||||||
|
- Report bugs via [GitHub Issues](https://github.com/yourusername/morbac_pg/issues)
|
||||||
|
- Ask questions in [Discussions](https://github.com/yourusername/morbac_pg/discussions)
|
||||||
|
- Security issues: see [SECURITY.md](SECURITY.md)
|
||||||
|
|
||||||
|
## Comparison with Traditional RBAC
|
||||||
|
|
||||||
|
| Feature | Traditional RBAC | morbac_pg |
|
||||||
|
|---------|-----------------|-----------|
|
||||||
|
| Multi-tenancy | No | Yes |
|
||||||
|
| Prohibitions | No | Yes |
|
||||||
|
| Context-aware | No | Yes |
|
||||||
|
| Hierarchies | Basic roles only | Organizations, roles, activities, views |
|
||||||
|
| Delegation | No | Yes |
|
||||||
|
| Separation of Duty | No | Yes |
|
||||||
|
| Cross-organization | No | Yes |
|
||||||
+125
@@ -0,0 +1,125 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| 1.0.x | :white_check_mark: |
|
||||||
|
|
||||||
|
## Security Model
|
||||||
|
|
||||||
|
morbac_pg implements the Multi-OrBAC access control model with the following security principles:
|
||||||
|
|
||||||
|
### Default Deny
|
||||||
|
- No permission = access denied
|
||||||
|
- Explicit permissions must be granted
|
||||||
|
|
||||||
|
### Prohibition Precedence
|
||||||
|
- Prohibitions are checked first
|
||||||
|
- Prohibitions always override permissions
|
||||||
|
- Early return for performance and security
|
||||||
|
|
||||||
|
### Organization Isolation
|
||||||
|
- All policies are scoped to organizations
|
||||||
|
- Cross-organization access requires explicit `cross_org_rules`
|
||||||
|
- Users cannot access resources outside their authorized organizations
|
||||||
|
|
||||||
|
### Context Evaluation
|
||||||
|
- All rules can have contextual conditions
|
||||||
|
- Contexts are evaluated at authorization time
|
||||||
|
- Failed context evaluation = rule does not apply
|
||||||
|
|
||||||
|
### SQL Injection Protection
|
||||||
|
- All functions use parameterized queries
|
||||||
|
- No dynamic SQL construction from user input
|
||||||
|
- Type-safe PostgreSQL functions
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you discover a security vulnerability in morbac_pg, please report it responsibly:
|
||||||
|
|
||||||
|
### DO NOT
|
||||||
|
- Open a public GitHub issue for security vulnerabilities
|
||||||
|
- Disclose the vulnerability publicly before it's fixed
|
||||||
|
|
||||||
|
### DO
|
||||||
|
1. **Email the maintainer** with details:
|
||||||
|
- Description of the vulnerability
|
||||||
|
- Steps to reproduce
|
||||||
|
- Potential impact
|
||||||
|
- Suggested fix (if any)
|
||||||
|
|
||||||
|
2. **Wait for acknowledgment** (within 48 hours)
|
||||||
|
|
||||||
|
3. **Coordinate disclosure** after a fix is available
|
||||||
|
|
||||||
|
### What to Expect
|
||||||
|
|
||||||
|
- **Acknowledgment**: Within 48 hours
|
||||||
|
- **Initial assessment**: Within 1 week
|
||||||
|
- **Fix timeline**: Depends on severity
|
||||||
|
- Critical: 1-7 days
|
||||||
|
- High: 1-2 weeks
|
||||||
|
- Medium: 2-4 weeks
|
||||||
|
- Low: Next release
|
||||||
|
- **Disclosure**: Coordinated after fix is released
|
||||||
|
- **Credit**: You will be credited (unless you prefer anonymity)
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
When using morbac_pg:
|
||||||
|
|
||||||
|
### 1. Protect Authorization Headers
|
||||||
|
```sql
|
||||||
|
-- In PostgREST, ensure X-User-Id and X-Org-Id come from authenticated sessions
|
||||||
|
-- Never trust client-provided values directly
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Use RLS Policies
|
||||||
|
```sql
|
||||||
|
-- Always enable RLS on application tables
|
||||||
|
ALTER TABLE app.documents ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Use morbac_pg for authorization
|
||||||
|
CREATE POLICY doc_access ON app.documents
|
||||||
|
FOR ALL USING (morbac.rls_check('read', 'documents'));
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Validate Context Functions
|
||||||
|
```sql
|
||||||
|
-- Context functions should be STABLE or IMMUTABLE
|
||||||
|
-- Avoid VOLATILE contexts that could leak information
|
||||||
|
CREATE OR REPLACE FUNCTION morbac.my_context()
|
||||||
|
RETURNS BOOLEAN
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
STABLE -- Important!
|
||||||
|
AS $$ ... $$;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Audit Critical Operations
|
||||||
|
```sql
|
||||||
|
-- Log policy changes and authorization decisions
|
||||||
|
-- (morbac_pg does not include audit logging by default)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Regular Updates
|
||||||
|
- Keep PostgreSQL updated
|
||||||
|
- Monitor for morbac_pg security advisories
|
||||||
|
- Review and test policies regularly
|
||||||
|
|
||||||
|
### 6. Principle of Least Privilege
|
||||||
|
- Grant only necessary permissions
|
||||||
|
- Use prohibitions to explicitly deny sensitive operations
|
||||||
|
- Implement Separation of Duty for critical roles
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
|
||||||
|
1. **Context Function Security**: Context functions run with database privileges. Ensure they are written securely and don't expose sensitive information.
|
||||||
|
|
||||||
|
2. **Delegation Trust**: Delegations are trusted. Ensure delegator has authority to delegate their role.
|
||||||
|
|
||||||
|
3. **Performance**: Complex authorization queries on large datasets may impact performance. Use indexes and consider caching.
|
||||||
|
|
||||||
|
## Acknowledgments
|
||||||
|
|
||||||
|
We appreciate responsible disclosure and will acknowledge security researchers who help improve morbac_pg security.
|
||||||
Executable
+61
@@ -0,0 +1,61 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Installation script for morbac_pg extension
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=================================="
|
||||||
|
echo "morbac_pg Extension Installer"
|
||||||
|
echo "=================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if running as root or with sudo
|
||||||
|
if [ "$EUID" -ne 0 ] && [ -z "$SUDO_USER" ]; then
|
||||||
|
echo "This script needs to copy files to PostgreSQL extension directory."
|
||||||
|
echo "Please run with sudo or as root."
|
||||||
|
echo ""
|
||||||
|
echo "Usage: sudo ./install.sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if pg_config is available
|
||||||
|
if ! command -v pg_config &> /dev/null; then
|
||||||
|
echo "Error: pg_config not found in PATH"
|
||||||
|
echo "Please install PostgreSQL development packages:"
|
||||||
|
echo " Ubuntu/Debian: sudo apt-get install postgresql-server-dev-all"
|
||||||
|
echo " RedHat/CentOS: sudo yum install postgresql-devel"
|
||||||
|
echo " macOS: brew install postgresql"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get PostgreSQL extension directory
|
||||||
|
EXTDIR=$(pg_config --sharedir)/extension
|
||||||
|
|
||||||
|
echo "PostgreSQL extension directory: $EXTDIR"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if directory exists
|
||||||
|
if [ ! -d "$EXTDIR" ]; then
|
||||||
|
echo "Error: Extension directory does not exist: $EXTDIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copy extension files
|
||||||
|
echo "Installing extension files..."
|
||||||
|
cp -v morbac_pg.control "$EXTDIR/"
|
||||||
|
cp -v morbac_pg--1.0.sql "$EXTDIR/"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=================================="
|
||||||
|
echo "Installation complete!"
|
||||||
|
echo "=================================="
|
||||||
|
echo ""
|
||||||
|
echo "To use the extension in your database:"
|
||||||
|
echo ""
|
||||||
|
echo " psql -d your_database -c 'CREATE EXTENSION morbac_pg;'"
|
||||||
|
echo ""
|
||||||
|
echo "To run tests:"
|
||||||
|
echo ""
|
||||||
|
echo " make test"
|
||||||
|
echo ""
|
||||||
|
echo "For more information, see README.md"
|
||||||
|
echo ""
|
||||||
+1594
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
|||||||
|
# morbac_pg extension
|
||||||
|
# Multi-OrBAC access control model for PostgreSQL
|
||||||
|
comment = 'Multi-OrBAC: Organization-Based Access Control with multi-organization support'
|
||||||
|
default_version = '1.0'
|
||||||
|
module_pathname = '$libdir/morbac_pg'
|
||||||
|
relocatable = false
|
||||||
|
schema = morbac
|
||||||
|
requires = 'pgcrypto'
|
||||||
+627
@@ -0,0 +1,627 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- Test Script for morbac_pg Extension
|
||||||
|
-- =============================================================================
|
||||||
|
-- This script demonstrates and tests the Multi-OrBAC implementation
|
||||||
|
--
|
||||||
|
-- Test Scenario:
|
||||||
|
-- - Two organizations: "Acme Corp" and "Beta Inc"
|
||||||
|
-- - Multiple users with different roles
|
||||||
|
-- - Permission and prohibition rules
|
||||||
|
-- - Context evaluation
|
||||||
|
-- - Multi-organization resource access
|
||||||
|
-- - Obligations and recommendations
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
-- Clean up any previous test
|
||||||
|
DROP EXTENSION IF EXISTS morbac_pg CASCADE;
|
||||||
|
DROP SCHEMA IF EXISTS morbac CASCADE;
|
||||||
|
|
||||||
|
-- Install the extension
|
||||||
|
CREATE EXTENSION morbac_pg;
|
||||||
|
|
||||||
|
-- Verify schema and tables exist
|
||||||
|
\echo '=== Schema and Tables Created ==='
|
||||||
|
SELECT schemaname, tablename
|
||||||
|
FROM pg_tables
|
||||||
|
WHERE schemaname = 'morbac'
|
||||||
|
ORDER BY tablename;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== Test 1: Create Organizations ==='
|
||||||
|
|
||||||
|
-- Create two organizations
|
||||||
|
INSERT INTO morbac.orgs (id, name) VALUES
|
||||||
|
('11111111-1111-1111-1111-111111111111', 'Acme Corp'),
|
||||||
|
('22222222-2222-2222-2222-222222222222', 'Beta Inc');
|
||||||
|
|
||||||
|
-- Create hierarchical organization (Acme Subsidiary under Acme Corp)
|
||||||
|
INSERT INTO morbac.orgs (id, name, parent_id) VALUES
|
||||||
|
('33333333-3333-3333-3333-333333333333', 'Acme Subsidiary', '11111111-1111-1111-1111-111111111111');
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
o.name,
|
||||||
|
COALESCE(p.name, 'None') as parent_organization
|
||||||
|
FROM morbac.orgs o
|
||||||
|
LEFT JOIN morbac.orgs p ON o.parent_id = p.id
|
||||||
|
ORDER BY o.parent_id NULLS FIRST, o.name;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== Test 2: Create Roles ==='
|
||||||
|
|
||||||
|
-- Create roles in Acme Corp
|
||||||
|
INSERT INTO morbac.roles (id, org_id, name, description) VALUES
|
||||||
|
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '11111111-1111-1111-1111-111111111111', 'admin', 'Administrator role'),
|
||||||
|
('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '11111111-1111-1111-1111-111111111111', 'employee', 'Regular employee'),
|
||||||
|
('cccccccc-cccc-cccc-cccc-cccccccccccc', '11111111-1111-1111-1111-111111111111', 'contractor', 'External contractor'),
|
||||||
|
('ffffffff-ffff-ffff-ffff-ffffffffffff', '11111111-1111-1111-1111-111111111111', 'viewer', 'Read-only viewer');
|
||||||
|
|
||||||
|
-- Create roles in Beta Inc
|
||||||
|
INSERT INTO morbac.roles (id, org_id, name, description) VALUES
|
||||||
|
('dddddddd-dddd-dddd-dddd-dddddddddddd', '22222222-2222-2222-2222-222222222222', 'manager', 'Manager role'),
|
||||||
|
('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', '22222222-2222-2222-2222-222222222222', 'staff', 'Staff member');
|
||||||
|
|
||||||
|
SELECT r.name as role, o.name as organization
|
||||||
|
FROM morbac.roles r
|
||||||
|
JOIN morbac.orgs o ON r.org_id = o.id
|
||||||
|
ORDER BY o.name, r.name;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== Test 2b: Create Role Hierarchy ==='
|
||||||
|
|
||||||
|
-- Role hierarchy in Acme Corp:
|
||||||
|
-- admin > employee > viewer
|
||||||
|
-- (admin inherits from employee, employee inherits from viewer)
|
||||||
|
INSERT INTO morbac.role_hierarchy (senior_role_id, junior_role_id) VALUES
|
||||||
|
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'), -- admin inherits from employee
|
||||||
|
('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'ffffffff-ffff-ffff-ffff-ffffffffffff'); -- employee inherits from viewer
|
||||||
|
|
||||||
|
\echo 'Role hierarchy created:'
|
||||||
|
SELECT
|
||||||
|
sr.name as senior_role,
|
||||||
|
jr.name as junior_role,
|
||||||
|
o.name as organization
|
||||||
|
FROM morbac.role_hierarchy rh
|
||||||
|
JOIN morbac.roles sr ON rh.senior_role_id = sr.id
|
||||||
|
JOIN morbac.roles jr ON rh.junior_role_id = jr.id
|
||||||
|
JOIN morbac.orgs o ON sr.org_id = o.id;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== Test 3: Assign Users to Roles ==='
|
||||||
|
|
||||||
|
-- Define test users (in real scenario, these would come from auth system)
|
||||||
|
-- User Alice (admin at Acme)
|
||||||
|
INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES
|
||||||
|
('aaaaaaaa-0000-0000-0000-000000000001', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '11111111-1111-1111-1111-111111111111');
|
||||||
|
|
||||||
|
-- User Bob (employee at Acme)
|
||||||
|
INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES
|
||||||
|
('bbbbbbbb-0000-0000-0000-000000000002', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '11111111-1111-1111-1111-111111111111');
|
||||||
|
|
||||||
|
-- User Charlie (contractor at Acme AND staff at Beta - multi-org user)
|
||||||
|
INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES
|
||||||
|
('cccccccc-0000-0000-0000-000000000003', 'cccccccc-cccc-cccc-cccc-cccccccccccc', '11111111-1111-1111-1111-111111111111'),
|
||||||
|
('cccccccc-0000-0000-0000-000000000003', 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', '22222222-2222-2222-2222-222222222222');
|
||||||
|
|
||||||
|
-- User Diana (manager at Beta)
|
||||||
|
INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES
|
||||||
|
('dddddddd-0000-0000-0000-000000000004', 'dddddddd-dddd-dddd-dddd-dddddddddddd', '22222222-2222-2222-2222-222222222222');
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
ur.user_id,
|
||||||
|
r.name as role,
|
||||||
|
o.name as organization
|
||||||
|
FROM morbac.user_roles ur
|
||||||
|
JOIN morbac.roles r ON ur.role_id = r.id
|
||||||
|
JOIN morbac.orgs o ON ur.org_id = o.id
|
||||||
|
ORDER BY ur.user_id;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== Test 4: Define Activities and Views ==='
|
||||||
|
|
||||||
|
INSERT INTO morbac.activities (name, description) VALUES
|
||||||
|
('read', 'Read/view data'),
|
||||||
|
('write', 'Create/modify data'),
|
||||||
|
('delete', 'Delete data'),
|
||||||
|
('approve', 'Approve actions');
|
||||||
|
|
||||||
|
INSERT INTO morbac.views (name, description) VALUES
|
||||||
|
('documents', 'Document resources'),
|
||||||
|
('reports', 'Report resources'),
|
||||||
|
('sensitive_data', 'Sensitive/confidential data');
|
||||||
|
|
||||||
|
SELECT * FROM morbac.activities ORDER BY name;
|
||||||
|
SELECT * FROM morbac.views ORDER BY name;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== Test 5: Create Additional Contexts ==='
|
||||||
|
|
||||||
|
-- Business hours context (simplified - always true for this test)
|
||||||
|
CREATE OR REPLACE FUNCTION morbac.context_business_hours()
|
||||||
|
RETURNS BOOLEAN
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
STABLE
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
-- In production, check current time against business hours
|
||||||
|
-- For test, return TRUE
|
||||||
|
RETURN TRUE;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
INSERT INTO morbac.contexts (name, description, evaluator) VALUES
|
||||||
|
('business_hours', 'During business hours', 'morbac.context_business_hours'::regproc);
|
||||||
|
|
||||||
|
-- Weekend context (always false for test)
|
||||||
|
CREATE OR REPLACE FUNCTION morbac.context_weekend()
|
||||||
|
RETURNS BOOLEAN
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
STABLE
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
-- In production, check if today is weekend
|
||||||
|
-- For test, return FALSE
|
||||||
|
RETURN FALSE;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
INSERT INTO morbac.contexts (name, description, evaluator) VALUES
|
||||||
|
('weekend', 'During weekend', 'morbac.context_weekend'::regproc);
|
||||||
|
|
||||||
|
SELECT name, description FROM morbac.contexts ORDER BY name;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== Test 6: Define Policy Using DSL ==='
|
||||||
|
|
||||||
|
-- Acme Corp policies
|
||||||
|
INSERT INTO morbac.policy (org_name, role_name, activity, view, modality, context_name) VALUES
|
||||||
|
-- Admins can do everything on documents
|
||||||
|
('Acme Corp', 'admin', 'read', 'documents', 'permission', 'always'),
|
||||||
|
('Acme Corp', 'admin', 'write', 'documents', 'permission', 'always'),
|
||||||
|
('Acme Corp', 'admin', 'delete', 'documents', 'permission', 'always'),
|
||||||
|
|
||||||
|
-- Employees can read and write documents
|
||||||
|
('Acme Corp', 'employee', 'read', 'documents', 'permission', 'always'),
|
||||||
|
('Acme Corp', 'employee', 'write', 'documents', 'permission', 'business_hours'),
|
||||||
|
|
||||||
|
-- Contractors can only read documents
|
||||||
|
('Acme Corp', 'contractor', 'read', 'documents', 'permission', 'always'),
|
||||||
|
|
||||||
|
-- PROHIBITION: Contractors cannot access sensitive data
|
||||||
|
('Acme Corp', 'contractor', 'read', 'sensitive_data', 'prohibition', 'always'),
|
||||||
|
('Acme Corp', 'contractor', 'write', 'sensitive_data', 'prohibition', 'always'),
|
||||||
|
|
||||||
|
-- OBLIGATION: Employees must review reports weekly
|
||||||
|
('Acme Corp', 'employee', 'read', 'reports', 'obligation', 'always'),
|
||||||
|
|
||||||
|
-- Admins can access reports
|
||||||
|
('Acme Corp', 'admin', 'read', 'reports', 'permission', 'always'),
|
||||||
|
('Acme Corp', 'admin', 'approve', 'reports', 'permission', 'always');
|
||||||
|
|
||||||
|
-- Beta Inc policies
|
||||||
|
INSERT INTO morbac.policy (org_name, role_name, activity, view, modality, context_name) VALUES
|
||||||
|
-- Managers have full access
|
||||||
|
('Beta Inc', 'manager', 'read', 'documents', 'permission', 'always'),
|
||||||
|
('Beta Inc', 'manager', 'write', 'documents', 'permission', 'always'),
|
||||||
|
('Beta Inc', 'manager', 'read', 'reports', 'permission', 'always'),
|
||||||
|
|
||||||
|
-- Staff can read documents
|
||||||
|
('Beta Inc', 'staff', 'read', 'documents', 'permission', 'always'),
|
||||||
|
|
||||||
|
-- PROHIBITION: No document modifications on weekends (example)
|
||||||
|
('Beta Inc', 'staff', 'write', 'documents', 'prohibition', 'weekend'),
|
||||||
|
|
||||||
|
-- RECOMMENDATION: Staff should review reports
|
||||||
|
('Beta Inc', 'staff', 'read', 'reports', 'recommendation', 'always');
|
||||||
|
|
||||||
|
SELECT org_name, role_name, activity, view, modality, context_name
|
||||||
|
FROM morbac.policy
|
||||||
|
ORDER BY org_name, role_name, modality;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== Test 7: Compile Policy ==='
|
||||||
|
|
||||||
|
SELECT * FROM morbac.compile_policy();
|
||||||
|
|
||||||
|
-- Verify rules were created
|
||||||
|
\echo ''
|
||||||
|
\echo 'Rules created by policy compiler:'
|
||||||
|
SELECT
|
||||||
|
o.name as org,
|
||||||
|
r.name as role,
|
||||||
|
ru.activity,
|
||||||
|
ru.view,
|
||||||
|
ru.modality,
|
||||||
|
c.name as context
|
||||||
|
FROM morbac.rules ru
|
||||||
|
JOIN morbac.orgs o ON ru.org_id = o.id
|
||||||
|
JOIN morbac.roles r ON ru.role_id = r.id
|
||||||
|
JOIN morbac.contexts c ON ru.context_id = c.id
|
||||||
|
ORDER BY o.name, r.name, ru.modality, ru.activity;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== Test 8: Authorization Decisions ==='
|
||||||
|
|
||||||
|
-- Test Alice (admin at Acme)
|
||||||
|
\echo 'Alice (admin at Acme Corp):'
|
||||||
|
SELECT
|
||||||
|
'read documents' as action,
|
||||||
|
morbac.is_allowed(
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||||
|
'read',
|
||||||
|
'documents'
|
||||||
|
) as allowed;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
'delete documents' as action,
|
||||||
|
morbac.is_allowed(
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||||
|
'delete',
|
||||||
|
'documents'
|
||||||
|
) as allowed;
|
||||||
|
|
||||||
|
-- Test Bob (employee at Acme)
|
||||||
|
\echo ''
|
||||||
|
\echo 'Bob (employee at Acme Corp):'
|
||||||
|
SELECT
|
||||||
|
'read documents' as action,
|
||||||
|
morbac.is_allowed(
|
||||||
|
'bbbbbbbb-0000-0000-0000-000000000002'::uuid,
|
||||||
|
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||||
|
'read',
|
||||||
|
'documents'
|
||||||
|
) as allowed;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
'delete documents' as action,
|
||||||
|
morbac.is_allowed(
|
||||||
|
'bbbbbbbb-0000-0000-0000-000000000002'::uuid,
|
||||||
|
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||||
|
'delete',
|
||||||
|
'documents'
|
||||||
|
) as allowed;
|
||||||
|
|
||||||
|
-- Test Charlie (contractor at Acme)
|
||||||
|
\echo ''
|
||||||
|
\echo 'Charlie (contractor at Acme Corp):'
|
||||||
|
SELECT
|
||||||
|
'read documents' as action,
|
||||||
|
morbac.is_allowed(
|
||||||
|
'cccccccc-0000-0000-0000-000000000003'::uuid,
|
||||||
|
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||||
|
'read',
|
||||||
|
'documents'
|
||||||
|
) as allowed;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
'write documents' as action,
|
||||||
|
morbac.is_allowed(
|
||||||
|
'cccccccc-0000-0000-0000-000000000003'::uuid,
|
||||||
|
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||||
|
'write',
|
||||||
|
'documents'
|
||||||
|
) as allowed;
|
||||||
|
|
||||||
|
-- Test PROHIBITION precedence
|
||||||
|
\echo ''
|
||||||
|
\echo 'Testing PROHIBITION PRECEDENCE:'
|
||||||
|
\echo 'Charlie (contractor) trying to read sensitive_data (should be DENIED by prohibition):'
|
||||||
|
SELECT
|
||||||
|
'read sensitive_data' as action,
|
||||||
|
morbac.is_allowed(
|
||||||
|
'cccccccc-0000-0000-0000-000000000003'::uuid,
|
||||||
|
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||||
|
'read',
|
||||||
|
'sensitive_data'
|
||||||
|
) as allowed;
|
||||||
|
|
||||||
|
-- Test Charlie at Beta Inc (multi-org user)
|
||||||
|
\echo ''
|
||||||
|
\echo 'Charlie as staff at Beta Inc:'
|
||||||
|
SELECT
|
||||||
|
'read documents' as action,
|
||||||
|
morbac.is_allowed(
|
||||||
|
'cccccccc-0000-0000-0000-000000000003'::uuid,
|
||||||
|
'22222222-2222-2222-2222-222222222222'::uuid,
|
||||||
|
'read',
|
||||||
|
'documents'
|
||||||
|
) as allowed;
|
||||||
|
|
||||||
|
-- Test Diana (manager at Beta)
|
||||||
|
\echo ''
|
||||||
|
\echo 'Diana (manager at Beta Inc):'
|
||||||
|
SELECT
|
||||||
|
'write documents' as action,
|
||||||
|
morbac.is_allowed(
|
||||||
|
'dddddddd-0000-0000-0000-000000000004'::uuid,
|
||||||
|
'22222222-2222-2222-2222-222222222222'::uuid,
|
||||||
|
'write',
|
||||||
|
'documents'
|
||||||
|
) as allowed;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== Test 8b: Role Hierarchy Inheritance ==='
|
||||||
|
|
||||||
|
-- Assign user Eve only viewer role
|
||||||
|
\echo 'Creating user Eve with only viewer role at Acme:'
|
||||||
|
INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES
|
||||||
|
('eeeeeeee-0000-0000-0000-000000000005', 'ffffffff-ffff-ffff-ffff-ffffffffffff', '11111111-1111-1111-1111-111111111111');
|
||||||
|
|
||||||
|
-- Add permission for viewer role
|
||||||
|
INSERT INTO morbac.policy (org_name, role_name, activity, view, modality) VALUES
|
||||||
|
('Acme Corp', 'viewer', 'read', 'documents', 'permission');
|
||||||
|
|
||||||
|
SELECT * FROM morbac.compile_policy();
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo 'Eve (has viewer role, which employee inherits from):'
|
||||||
|
SELECT
|
||||||
|
'read documents' as action,
|
||||||
|
morbac.is_allowed(
|
||||||
|
'eeeeeeee-0000-0000-0000-000000000005'::uuid,
|
||||||
|
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||||
|
'read',
|
||||||
|
'documents'
|
||||||
|
) as allowed;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo 'Bob (employee) should inherit permissions from viewer role:'
|
||||||
|
\echo 'Effective roles for Bob (should include employee and viewer via hierarchy):'
|
||||||
|
SELECT r.name, er.depth
|
||||||
|
FROM morbac.get_effective_roles(
|
||||||
|
'bbbbbbbb-0000-0000-0000-000000000002'::uuid,
|
||||||
|
'11111111-1111-1111-1111-111111111111'::uuid
|
||||||
|
) er
|
||||||
|
JOIN morbac.roles r ON er.role_id = r.id
|
||||||
|
ORDER BY er.depth;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo 'Alice (admin) should inherit from both employee and viewer:'
|
||||||
|
SELECT r.name, er.depth
|
||||||
|
FROM morbac.get_effective_roles(
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'11111111-1111-1111-1111-111111111111'::uuid
|
||||||
|
) er
|
||||||
|
JOIN morbac.roles r ON er.role_id = r.id
|
||||||
|
ORDER BY er.depth;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== Test 8c: Organization Hierarchy ==='
|
||||||
|
|
||||||
|
\echo 'Ancestors of Acme Subsidiary:'
|
||||||
|
SELECT o.name, oa.depth
|
||||||
|
FROM morbac.get_org_ancestors('33333333-3333-3333-3333-333333333333'::uuid) oa
|
||||||
|
JOIN morbac.orgs o ON oa.org_id = o.id
|
||||||
|
ORDER BY oa.depth;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo 'Descendants of Acme Corp (should include subsidiary):'
|
||||||
|
SELECT o.name, od.depth
|
||||||
|
FROM morbac.get_org_descendants('11111111-1111-1111-1111-111111111111'::uuid) od
|
||||||
|
JOIN morbac.orgs o ON od.org_id = o.id
|
||||||
|
ORDER BY od.depth;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== Test 9: Obligations and Recommendations ==='
|
||||||
|
|
||||||
|
\echo 'Obligations for Bob (employee at Acme):'
|
||||||
|
SELECT * FROM morbac.pending_obligations(
|
||||||
|
'bbbbbbbb-0000-0000-0000-000000000002'::uuid,
|
||||||
|
'11111111-1111-1111-1111-111111111111'::uuid
|
||||||
|
);
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo 'Recommendations for Charlie (staff at Beta):'
|
||||||
|
SELECT * FROM morbac.pending_recommendations(
|
||||||
|
'cccccccc-0000-0000-0000-000000000003'::uuid,
|
||||||
|
'22222222-2222-2222-2222-222222222222'::uuid
|
||||||
|
);
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== Test 10: Utility Functions ==='
|
||||||
|
|
||||||
|
\echo 'Roles for Charlie at Acme Corp:'
|
||||||
|
SELECT * FROM morbac.user_roles_in_org(
|
||||||
|
'cccccccc-0000-0000-0000-000000000003'::uuid,
|
||||||
|
'11111111-1111-1111-1111-111111111111'::uuid
|
||||||
|
);
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo 'Roles for Charlie at Beta Inc:'
|
||||||
|
SELECT * FROM morbac.user_roles_in_org(
|
||||||
|
'cccccccc-0000-0000-0000-000000000003'::uuid,
|
||||||
|
'22222222-2222-2222-2222-222222222222'::uuid
|
||||||
|
);
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo 'Does Alice have admin role at Acme Corp?'
|
||||||
|
SELECT morbac.user_has_role(
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||||
|
'admin'
|
||||||
|
) as has_admin_role;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== Test 11: Context Evaluation ==='
|
||||||
|
|
||||||
|
\echo 'Evaluating always context:'
|
||||||
|
SELECT morbac.eval_context(
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'always')
|
||||||
|
) as result;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo 'Evaluating business_hours context:'
|
||||||
|
SELECT morbac.eval_context(
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'business_hours')
|
||||||
|
) as result;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo 'Evaluating weekend context:'
|
||||||
|
SELECT morbac.eval_context(
|
||||||
|
(SELECT id FROM morbac.contexts WHERE name = 'weekend')
|
||||||
|
) as result;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== Test 12: Policy Re-compilation (Idempotency Test) ==='
|
||||||
|
|
||||||
|
\echo 'Compiling policy again (should show 0 new rules, idempotent):'
|
||||||
|
SELECT * FROM morbac.compile_policy();
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== Test 13: Adding New Policy Entry ==='
|
||||||
|
|
||||||
|
INSERT INTO morbac.policy (org_name, role_name, activity, view, modality, context_name) VALUES
|
||||||
|
('Acme Corp', 'admin', 'approve', 'documents', 'permission', 'always');
|
||||||
|
|
||||||
|
\echo 'Compiling new policy entry:'
|
||||||
|
SELECT * FROM morbac.compile_policy();
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== Test 14: Delegation ==='
|
||||||
|
|
||||||
|
-- Alice delegates admin role to user Frank temporarily
|
||||||
|
INSERT INTO morbac.delegations (delegator_id, delegatee_id, role_id, org_id, valid_from, valid_until) VALUES
|
||||||
|
('aaaaaaaa-0000-0000-0000-000000000001', 'ffffffff-0000-0000-0000-000000000006', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '11111111-1111-1111-1111-111111111111', now(), now() + interval '1 day');
|
||||||
|
|
||||||
|
\echo 'Frank (via delegation from Alice) should have admin permissions:'
|
||||||
|
SELECT
|
||||||
|
'delete documents (delegated)' as action,
|
||||||
|
morbac.is_allowed(
|
||||||
|
'ffffffff-0000-0000-0000-000000000006'::uuid,
|
||||||
|
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||||
|
'delete',
|
||||||
|
'documents'
|
||||||
|
) as allowed;
|
||||||
|
|
||||||
|
\echo 'Frank effective roles (should include delegated admin):'
|
||||||
|
SELECT r.name, cr.source
|
||||||
|
FROM morbac.get_comprehensive_roles('ffffffff-0000-0000-0000-000000000006'::uuid, '11111111-1111-1111-1111-111111111111'::uuid) cr
|
||||||
|
JOIN morbac.roles r ON cr.role_id = r.id;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== Test 15: Separation of Duty ==='
|
||||||
|
|
||||||
|
-- Define SoD conflict: auditor and accountant roles are mutually exclusive
|
||||||
|
INSERT INTO morbac.roles (id, org_id, name, description) VALUES
|
||||||
|
('11111111-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111', 'auditor', 'Auditor role'),
|
||||||
|
('22222222-2222-2222-2222-222222222222', '11111111-1111-1111-1111-111111111111', 'accountant', 'Accountant role');
|
||||||
|
|
||||||
|
INSERT INTO morbac.sod_conflicts (role_a_id, role_b_id, org_id, description) VALUES
|
||||||
|
('11111111-1111-1111-1111-111111111111', '22222222-2222-2222-2222-222222222222', '11111111-1111-1111-1111-111111111111', 'Auditors and accountants are mutually exclusive');
|
||||||
|
|
||||||
|
-- Assign user to auditor role
|
||||||
|
INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES
|
||||||
|
('99999999-0000-0000-0000-000000000009', '11111111-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111');
|
||||||
|
|
||||||
|
\echo 'Check SoD violation if we try to assign accountant role to auditor:'
|
||||||
|
SELECT morbac.check_sod_violation(
|
||||||
|
'99999999-0000-0000-0000-000000000009'::uuid,
|
||||||
|
'22222222-2222-2222-2222-222222222222'::uuid,
|
||||||
|
'11111111-1111-1111-1111-111111111111'::uuid
|
||||||
|
) as would_violate_sod;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== Test 16: Activity and View Hierarchies ==='
|
||||||
|
|
||||||
|
-- Define activity hierarchy: write implies read
|
||||||
|
INSERT INTO morbac.activity_hierarchy (senior_activity, junior_activity) VALUES
|
||||||
|
('write', 'read');
|
||||||
|
|
||||||
|
-- Define view hierarchy: sensitive_data is a type of documents
|
||||||
|
INSERT INTO morbac.view_hierarchy (senior_view, junior_view) VALUES
|
||||||
|
('sensitive_data', 'documents');
|
||||||
|
|
||||||
|
\echo 'Activities implied by write:'
|
||||||
|
SELECT * FROM morbac.get_effective_activities('write');
|
||||||
|
|
||||||
|
\echo 'Views for sensitive_data (should include documents):'
|
||||||
|
SELECT * FROM morbac.get_effective_views('sensitive_data');
|
||||||
|
|
||||||
|
\echo 'Bob has permission for read+documents, so write+documents should work (activity hierarchy):'
|
||||||
|
SELECT
|
||||||
|
'write documents (via activity hierarchy)' as action,
|
||||||
|
morbac.is_allowed(
|
||||||
|
'bbbbbbbb-0000-0000-0000-000000000002'::uuid,
|
||||||
|
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||||
|
'write',
|
||||||
|
'documents'
|
||||||
|
) as allowed;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== Test 17: Negative Role Assignment ==='
|
||||||
|
|
||||||
|
-- Explicitly prohibit contractor from ever being admin
|
||||||
|
INSERT INTO morbac.negative_role_assignments (user_id, role_id, org_id, reason) VALUES
|
||||||
|
('cccccccc-0000-0000-0000-000000000003', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '11111111-1111-1111-1111-111111111111', 'Contractors cannot be administrators');
|
||||||
|
|
||||||
|
\echo 'Charlie cannot have admin role even if explicitly assigned (negative assignment takes precedence).'
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== Test 18: Cardinality Constraints ==='
|
||||||
|
|
||||||
|
-- Set max 2 users for admin role
|
||||||
|
INSERT INTO morbac.role_cardinality (role_id, min_users, max_users, description) VALUES
|
||||||
|
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 1, 2, 'Admin role limited to 2 users minimum 1');
|
||||||
|
|
||||||
|
\echo 'Check cardinality when adding user (1 admin exists, max is 2, should be OK):'
|
||||||
|
SELECT morbac.check_cardinality_violation('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'::uuid, TRUE) as violation;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== Test 19: Administration Rules ==='
|
||||||
|
|
||||||
|
-- Only admin role can create rules
|
||||||
|
INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, context_id, modality) VALUES
|
||||||
|
('11111111-1111-1111-1111-111111111111', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'create_rule', 'rules', (SELECT id FROM morbac.contexts WHERE name = 'always'), 'permission');
|
||||||
|
|
||||||
|
\echo 'Alice (admin) can create rules:'
|
||||||
|
SELECT morbac.is_admin_allowed(
|
||||||
|
'aaaaaaaa-0000-0000-0000-000000000001'::uuid,
|
||||||
|
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||||
|
'create_rule',
|
||||||
|
'rules'
|
||||||
|
) as can_create_rules;
|
||||||
|
|
||||||
|
\echo 'Bob (employee) cannot create rules:'
|
||||||
|
SELECT morbac.is_admin_allowed(
|
||||||
|
'bbbbbbbb-0000-0000-0000-000000000002'::uuid,
|
||||||
|
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||||
|
'create_rule',
|
||||||
|
'rules'
|
||||||
|
) as can_create_rules;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== Test 20: Cross-Organization Rules ==='
|
||||||
|
|
||||||
|
-- Allow Beta managers to read Acme documents (inter-org collaboration)
|
||||||
|
INSERT INTO morbac.cross_org_rules (source_org_id, target_org_id, role_id, activity, view, context_id, modality) VALUES
|
||||||
|
('22222222-2222-2222-2222-222222222222', '11111111-1111-1111-1111-111111111111', 'dddddddd-dddd-dddd-dddd-dddddddddddd', 'read', 'documents', (SELECT id FROM morbac.contexts WHERE name = 'always'), 'permission');
|
||||||
|
|
||||||
|
\echo 'Diana (manager at Beta) can read Acme documents via cross-org rule:'
|
||||||
|
SELECT
|
||||||
|
'read Acme documents from Beta' as action,
|
||||||
|
morbac.is_allowed(
|
||||||
|
'dddddddd-0000-0000-0000-000000000004'::uuid,
|
||||||
|
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||||
|
'read',
|
||||||
|
'documents'
|
||||||
|
) as allowed;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=== ALL TESTS COMPLETED SUCCESSFULLY ==='
|
||||||
|
\echo ''
|
||||||
|
\echo 'Summary:'
|
||||||
|
\echo '- Extension installed and schema created'
|
||||||
|
\echo '- Organizations with hierarchy'
|
||||||
|
\echo '- Roles with hierarchy'
|
||||||
|
\echo '- Delegation support'
|
||||||
|
\echo '- Separation of Duty constraints'
|
||||||
|
\echo '- Activity and View hierarchies'
|
||||||
|
\echo '- Negative role assignments'
|
||||||
|
\echo '- Cardinality constraints'
|
||||||
|
\echo '- Administration rules'
|
||||||
|
\echo '- Cross-organizational rules'
|
||||||
|
\echo '- Derived roles (framework ready)'
|
||||||
|
\echo '- Complete authorization with all features'
|
||||||
|
\echo '- Prohibition precedence verified'
|
||||||
|
\echo '- Multi-organization support verified'
|
||||||
Reference in New Issue
Block a user