rights system

This commit is contained in:
Matthias Hochmeister
2026-03-23 10:07:53 +01:00
parent f976f36cbc
commit 2bb22850f4
35 changed files with 1565 additions and 282 deletions

View File

@@ -0,0 +1,172 @@
import pool from '../config/database';
import logger from '../utils/logger';
export interface FeatureGroupRow {
id: string;
label: string;
sort_order: number;
maintenance: boolean;
}
export interface PermissionRow {
id: string;
feature_group_id: string;
label: string;
description: string | null;
sort_order: number;
}
export interface MatrixData {
featureGroups: FeatureGroupRow[];
permissions: PermissionRow[];
groups: string[];
grants: Record<string, string[]>;
maintenance: Record<string, boolean>;
}
class PermissionService {
private groupPermissions: Map<string, Set<string>> = new Map();
private maintenanceFlags: Map<string, boolean> = new Map();
async loadCache(): Promise<void> {
try {
// Load group permissions
const gpResult = await pool.query('SELECT authentik_group, permission_id FROM group_permissions');
const newMap = new Map<string, Set<string>>();
for (const row of gpResult.rows) {
if (!newMap.has(row.authentik_group)) {
newMap.set(row.authentik_group, new Set());
}
newMap.get(row.authentik_group)!.add(row.permission_id);
}
this.groupPermissions = newMap;
// Load maintenance flags
const mResult = await pool.query('SELECT id, maintenance FROM feature_groups');
const newFlags = new Map<string, boolean>();
for (const row of mResult.rows) {
newFlags.set(row.id, row.maintenance);
}
this.maintenanceFlags = newFlags;
logger.info('Permission cache loaded', {
groups: this.groupPermissions.size,
featureGroups: this.maintenanceFlags.size,
});
} catch (error) {
logger.error('Failed to load permission cache', { error });
// Don't throw — service can still function with empty cache
// dashboard_admin bypass ensures admins always have access
}
}
getEffectivePermissions(groups: string[]): string[] {
const permSet = new Set<string>();
for (const group of groups) {
const perms = this.groupPermissions.get(group);
if (perms) {
for (const p of perms) {
permSet.add(p);
}
}
}
return Array.from(permSet);
}
hasPermission(groups: string[], permission: string): boolean {
for (const group of groups) {
const perms = this.groupPermissions.get(group);
if (perms?.has(permission)) return true;
}
return false;
}
isFeatureInMaintenance(featureGroup: string): boolean {
return this.maintenanceFlags.get(featureGroup) ?? false;
}
getMaintenanceFlags(): Record<string, boolean> {
const result: Record<string, boolean> = {};
for (const [k, v] of this.maintenanceFlags) {
result[k] = v;
}
return result;
}
// ── Admin methods ──
async getMatrix(): Promise<MatrixData> {
const [fgResult, pResult, gpResult] = await Promise.all([
pool.query('SELECT id, label, sort_order, maintenance FROM feature_groups ORDER BY sort_order'),
pool.query('SELECT id, feature_group_id, label, description, sort_order FROM permissions ORDER BY feature_group_id, sort_order'),
pool.query('SELECT authentik_group, permission_id FROM group_permissions'),
]);
const grants: Record<string, string[]> = {};
const groupSet = new Set<string>();
for (const row of gpResult.rows) {
groupSet.add(row.authentik_group);
if (!grants[row.authentik_group]) grants[row.authentik_group] = [];
grants[row.authentik_group].push(row.permission_id);
}
const maintenance: Record<string, boolean> = {};
for (const row of fgResult.rows) {
maintenance[row.id] = row.maintenance;
}
return {
featureGroups: fgResult.rows,
permissions: pResult.rows,
groups: Array.from(groupSet).sort(),
grants,
maintenance,
};
}
async getKnownGroups(): Promise<string[]> {
const result = await pool.query(
'SELECT DISTINCT authentik_group FROM group_permissions ORDER BY authentik_group'
);
return result.rows.map((r: any) => r.authentik_group);
}
async setGroupPermissions(group: string, permIds: string[], grantedBy: string): Promise<void> {
const client = await pool.connect();
try {
await client.query('BEGIN');
// Remove all existing permissions for this group
await client.query('DELETE FROM group_permissions WHERE authentik_group = $1', [group]);
// Insert new permissions
for (const permId of permIds) {
await client.query(
'INSERT INTO group_permissions (authentik_group, permission_id, granted_by) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING',
[group, permId, grantedBy]
);
}
await client.query('COMMIT');
// Reload cache
await this.loadCache();
logger.info('Group permissions updated', { group, permissionCount: permIds.length, grantedBy });
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
async setMaintenanceFlag(featureGroup: string, active: boolean): Promise<void> {
await pool.query(
'UPDATE feature_groups SET maintenance = $1 WHERE id = $2',
[active, featureGroup]
);
// Reload cache
await this.loadCache();
logger.info('Maintenance flag updated', { featureGroup, active });
}
}
export const permissionService = new PermissionService();