rights system
This commit is contained in:
172
backend/src/services/permission.service.ts
Normal file
172
backend/src/services/permission.service.ts
Normal 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();
|
||||
Reference in New Issue
Block a user