feat: bitwise RBAC engine with channel-level permission overrides

Replace string-based role checks (role === 'admin') with a bitwise BigInt
permission system. Adds computePermissions() resolution engine following
Discord's model: @everyone base → role union → admin shortcut → channel
overrides (role deny/allow → member deny/allow). Ready payload now filters
channels by VIEW_CHANNEL and attaches per-user myPermissions to each
server and channel. Includes channel_overrides table, @everyone role
auto-creation, migration for existing servers, and override CRUD API.
This commit is contained in:
Jannis Braun
2026-02-24 05:08:59 +01:00
parent 024833c470
commit 8030c89c6c
19 changed files with 568 additions and 93 deletions
+71 -1
View File
@@ -1,4 +1,5 @@
import Database from 'better-sqlite3';
import { DEFAULT_EVERYONE_PERMISSIONS, PermissionBits, ALL_PERMISSIONS, permissionsToString } from '@opencord/shared/src/permissions.js';
export function runMigrations(db: Database.Database): void {
console.log('Checking for database migrations...');
@@ -65,6 +66,75 @@ export function runMigrations(db: Database.Database): void {
}
}
}
// Ensure channel_overrides table exists (idempotent)
db.exec(`
CREATE TABLE IF NOT EXISTS channel_overrides (
channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
target_type TEXT NOT NULL,
target_id TEXT NOT NULL,
allow TEXT NOT NULL DEFAULT '0',
deny TEXT NOT NULL DEFAULT '0',
PRIMARY KEY (channel_id, target_type, target_id)
);
`);
// ─── RBAC Migration: Ensure @everyone roles exist for all servers ─────────
migrateEveryoneRoles(db);
console.log('Migrations complete.');
}
/** For each server, ensure an @everyone role exists with id === server.id */
function migrateEveryoneRoles(db: Database.Database): void {
const servers = db.prepare('SELECT id FROM servers').all() as { id: string }[];
const now = Date.now();
const defaultPerms = permissionsToString(DEFAULT_EVERYONE_PERMISSIONS);
const adminPerms = permissionsToString(ALL_PERMISSIONS);
const insertRole = db.prepare(
'INSERT OR IGNORE INTO roles (id, server_id, name, color, position, permissions, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)'
);
for (const server of servers) {
// Create @everyone role if it doesn't exist (id = server.id)
insertRole.run(server.id, server.id, '@everyone', '#b9bbbe', 0, defaultPerms, now);
}
// Migrate existing admin members: ensure an Admin role exists and assign it
const adminMembers = db.prepare(
"SELECT server_id, user_id FROM server_members WHERE role = 'admin'"
).all() as { server_id: string; user_id: string }[];
if (adminMembers.length > 0) {
// Group by server
const serverAdmins = new Map<string, string[]>();
for (const row of adminMembers) {
let arr = serverAdmins.get(row.server_id);
if (!arr) { arr = []; serverAdmins.set(row.server_id, arr); }
arr.push(row.user_id);
}
const checkAdminRole = db.prepare(
"SELECT id FROM roles WHERE server_id = ? AND name = 'Admin' AND permissions = ?"
);
const insertMemberRole = db.prepare(
'INSERT OR IGNORE INTO member_roles (server_id, user_id, role_id) VALUES (?, ?, ?)'
);
for (const [serverId, userIds] of serverAdmins) {
// Find or create Admin role for this server
let adminRole = checkAdminRole.get(serverId, adminPerms) as { id: string } | undefined;
if (!adminRole) {
// Generate a simple unique ID for the admin role
const adminRoleId = `${serverId}-admin`;
insertRole.run(adminRoleId, serverId, 'Admin', '#e74c3c', 1, adminPerms, now);
adminRole = { id: adminRoleId };
}
for (const userId of userIds) {
insertMemberRole.run(serverId, userId, adminRole.id);
}
}
}
}