feat: improve performance with cache and proper indexing
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user