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:
Claude (Chronicler #61)
2026-04-05 11:23:01 +00:00
parent f692b81357
commit 4b8525fabd

View File

@@ -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.