docs: Add RBAC database schema from Gemini followup
Complete PostgreSQL schema for Option C (Roles + Direct Overrides): - roles table - permissions table (auto-populated from module.json) - role_permissions mapping - user_roles mapping - user_permission_overrides (the magic table) - Performance indexes Includes permission resolution logic: 1. Check overrides first (explicit allow/deny trumps role) 2. Check role permissions second 3. Default to deny Signed-off-by: Claude (Chronicler #61) <claude@firefrostgaming.com>
This commit is contained in:
@@ -63,7 +63,83 @@ You asked whether to use Direct Permissions (Option A), Roles (Option B), or Bot
|
||||
|
||||
**Option C (Roles with Direct Overrides) is the only way this scales.** As The Wizard, you don't want to assign 50 individual permissions to a new moderator. You assign them the "Moderator" role. But, if you want Holly to have access to the Financials module (which Builder normally wouldn't have), you need a direct override without creating a hyper-specific "Builder + Finance" role.
|
||||
|
||||
**[VISUALIZATION: RBAC Resolver Logic - See screenshot]**
|
||||
**[VISUALIZATION: RBAC Resolver Logic - Interactive matrix showing users vs permissions with role inheritance and override controls]**
|
||||
|
||||
#### RBAC Database Schema
|
||||
|
||||
```sql
|
||||
-- 1. Roles Table
|
||||
-- Defines the high-level buckets (e.g., 'Admin', 'Community', 'Builder')
|
||||
CREATE TABLE roles (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(50) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 2. Permissions Table
|
||||
-- Populated automatically when Trinity Console boots and reads module.json files.
|
||||
-- Using 'permission_key' (e.g., 'tasks.view') as the primary key makes lookups fast.
|
||||
CREATE TABLE permissions (
|
||||
permission_key VARCHAR(100) PRIMARY KEY,
|
||||
module_id VARCHAR(50) NOT NULL, -- e.g., 'tasks', 'scheduler'
|
||||
name VARCHAR(100) NOT NULL, -- e.g., 'View tasks'
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 3. Role Permissions Table
|
||||
-- Maps which permissions belong to which roles.
|
||||
CREATE TABLE role_permissions (
|
||||
role_id INT REFERENCES roles(id) ON DELETE CASCADE,
|
||||
permission_key VARCHAR(100) REFERENCES permissions(permission_key) ON DELETE CASCADE,
|
||||
PRIMARY KEY (role_id, permission_key)
|
||||
);
|
||||
|
||||
-- 4. User Roles Table
|
||||
-- Maps a user's Discord ID (or internal user ID) to a specific role.
|
||||
CREATE TABLE user_roles (
|
||||
user_id VARCHAR(50) PRIMARY KEY, -- Assuming Discord ID from OAuth
|
||||
role_id INT REFERENCES roles(id) ON DELETE CASCADE,
|
||||
assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
assigned_by VARCHAR(50) -- Discord ID of the admin who assigned it
|
||||
);
|
||||
|
||||
-- 5. User Permission Overrides Table
|
||||
-- The "Option C" magic. This table holds explicit ALLOWs and DENYs that trump the role.
|
||||
CREATE TABLE user_permission_overrides (
|
||||
user_id VARCHAR(50) NOT NULL, -- Discord ID
|
||||
permission_key VARCHAR(100) REFERENCES permissions(permission_key) ON DELETE CASCADE,
|
||||
is_granted BOOLEAN NOT NULL, -- TRUE = Explicit Allow, FALSE = Explicit Deny
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(50), -- Discord ID of the admin who set the override
|
||||
PRIMARY KEY (user_id, permission_key)
|
||||
);
|
||||
|
||||
-- Performance Indexes
|
||||
CREATE INDEX idx_user_overrides ON user_permission_overrides(user_id);
|
||||
CREATE INDEX idx_role_permissions ON role_permissions(role_id);
|
||||
```
|
||||
|
||||
#### Permission Resolution Logic
|
||||
|
||||
When a user tries to hit a protected route, resolve access using this strict hierarchy:
|
||||
|
||||
1. **Check Overrides First:** Does this `user_id` have a row in `user_permission_overrides` for this `permission_key`?
|
||||
- If yes, and `is_granted` is TRUE → **Access Granted**
|
||||
- If yes, and `is_granted` is FALSE → **Access Denied** (Even if their role allows it, this acts as a hard block)
|
||||
|
||||
2. **Check Role Second:** If there is no override, look up their `role_id` in `user_roles`. Does that `role_id` have the `permission_key` in `role_permissions`?
|
||||
- If yes → **Access Granted**
|
||||
|
||||
3. **Default to Deny:** If the permission is not in the overrides and not in the role → **Access Denied**
|
||||
|
||||
#### Real-World Example
|
||||
|
||||
- Meg has the `Community` role
|
||||
- The `Community` role DOES NOT have the `financials.view` permission
|
||||
- You insert a row into `user_permission_overrides` for Meg's Discord ID, setting `permission_key = 'financials.view'` and `is_granted = TRUE`
|
||||
- **Result:** Meg can view the Financials module without needing a completely new "Community + Finance" role
|
||||
|
||||
**Wildcards:** Support `module.*`. It makes your middleware much cleaner.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user