roles.ts340 lines · main
1/**
2 * briven-engine roles + permissions on Doltgres.
3 */
4
5import { getEnginePool } from './db.js';
6import { isAuthCoreInitialized } from './engine.js';
7import { projectIdToTenantId } from './project-map.js';
8
9async function ensureTenant(tenantId: string, projectId?: string): Promise<void> {
10 const pool = getEnginePool();
11 const existing = await pool.query(
12 `SELECT tenant_id FROM be_tenants WHERE tenant_id = $1 LIMIT 1`,
13 [tenantId],
14 );
15 if (!existing.rowCount) {
16 await pool.query(
17 `INSERT INTO be_tenants (tenant_id, project_id) VALUES ($1, $2)`,
18 [tenantId, projectId ?? tenantId],
19 );
20 }
21}
22
23export async function createBrivenEngineRole(
24 role: string,
25 permissions: string[] = [],
26 opts?: { projectId?: string; tenantId?: string },
27): Promise<{ ok: boolean; engine: 'briven-engine'; storage: 'doltgres'; message?: string }> {
28 if (!isAuthCoreInitialized()) {
29 return {
30 ok: false,
31 engine: 'briven-engine',
32 storage: 'doltgres',
33 message: 'engine not ready',
34 };
35 }
36 const name = role.trim().toLowerCase();
37 if (!name) {
38 return {
39 ok: false,
40 engine: 'briven-engine',
41 storage: 'doltgres',
42 message: 'role required',
43 };
44 }
45 const tenantId =
46 opts?.tenantId ??
47 (opts?.projectId ? projectIdToTenantId(opts.projectId) : 'public');
48 await ensureTenant(tenantId, opts?.projectId);
49 const pool = getEnginePool();
50 const existing = await pool.query(
51 `SELECT role_name FROM be_roles WHERE tenant_id = $1 AND role_name = $2 LIMIT 1`,
52 [tenantId, name],
53 );
54 if (existing.rowCount) {
55 await pool.query(
56 `UPDATE be_roles SET permissions_json = $3 WHERE tenant_id = $1 AND role_name = $2`,
57 [tenantId, name, JSON.stringify(permissions)],
58 );
59 return {
60 ok: true,
61 engine: 'briven-engine',
62 storage: 'doltgres',
63 message: 'updated',
64 };
65 }
66 await pool.query(
67 `INSERT INTO be_roles (tenant_id, role_name, permissions_json)
68 VALUES ($1, $2, $3)`,
69 [tenantId, name, JSON.stringify(permissions)],
70 );
71 return {
72 ok: true,
73 engine: 'briven-engine',
74 storage: 'doltgres',
75 message: 'created',
76 };
77}
78
79export async function assignBrivenEngineRole(
80 userId: string,
81 role: string,
82 opts?: { projectId?: string; tenantId?: string },
83): Promise<{ ok: boolean; engine: 'briven-engine'; storage: 'doltgres'; message?: string }> {
84 if (!isAuthCoreInitialized()) {
85 return {
86 ok: false,
87 engine: 'briven-engine',
88 storage: 'doltgres',
89 message: 'engine not ready',
90 };
91 }
92 const name = role.trim().toLowerCase();
93 if (!name || name.length > 64 || !/^[a-z0-9][a-z0-9._-]*$/.test(name)) {
94 return {
95 ok: false,
96 engine: 'briven-engine',
97 storage: 'doltgres',
98 message: 'invalid role name',
99 };
100 }
101 const uid = userId?.trim();
102 if (!uid) {
103 return {
104 ok: false,
105 engine: 'briven-engine',
106 storage: 'doltgres',
107 message: 'userId required',
108 };
109 }
110 const tenantId =
111 opts?.tenantId ??
112 (opts?.projectId ? projectIdToTenantId(opts.projectId) : 'public');
113 const pool = getEnginePool();
114 const roleRow = await pool.query(
115 `SELECT role_name FROM be_roles WHERE tenant_id = $1 AND role_name = $2 LIMIT 1`,
116 [tenantId, name],
117 );
118 if (!roleRow.rowCount) {
119 return {
120 ok: false,
121 engine: 'briven-engine',
122 storage: 'doltgres',
123 message: 'role does not exist',
124 };
125 }
126 // be_users primary key is `id` (beu_…), not user_id — user_id only exists on be_user_roles.
127 const userRow = await pool.query(
128 `SELECT id FROM be_users WHERE id = $1 AND tenant_id = $2 LIMIT 1`,
129 [uid, tenantId],
130 );
131 if (!userRow.rowCount) {
132 return {
133 ok: false,
134 engine: 'briven-engine',
135 storage: 'doltgres',
136 message: 'user does not exist in this tenant',
137 };
138 }
139 const has = await pool.query(
140 `SELECT 1 FROM be_user_roles WHERE tenant_id = $1 AND user_id = $2 AND role_name = $3`,
141 [tenantId, uid, name],
142 );
143 if (!has.rowCount) {
144 await pool.query(
145 `INSERT INTO be_user_roles (tenant_id, user_id, role_name) VALUES ($1, $2, $3)`,
146 [tenantId, uid, name],
147 );
148 }
149 return { ok: true, engine: 'briven-engine', storage: 'doltgres', message: 'assigned' };
150}
151
152export async function unassignBrivenEngineRole(
153 userId: string,
154 role: string,
155 opts?: { projectId?: string; tenantId?: string },
156): Promise<{ ok: boolean; engine: 'briven-engine'; storage: 'doltgres'; message?: string }> {
157 if (!isAuthCoreInitialized()) {
158 return {
159 ok: false,
160 engine: 'briven-engine',
161 storage: 'doltgres',
162 message: 'engine not ready',
163 };
164 }
165 const name = role.trim().toLowerCase();
166 const uid = userId?.trim();
167 if (!name || !uid) {
168 return {
169 ok: false,
170 engine: 'briven-engine',
171 storage: 'doltgres',
172 message: 'userId and role required',
173 };
174 }
175 const tenantId =
176 opts?.tenantId ??
177 (opts?.projectId ? projectIdToTenantId(opts.projectId) : 'public');
178 const pool = getEnginePool();
179 const res = await pool.query(
180 `DELETE FROM be_user_roles WHERE tenant_id = $1 AND user_id = $2 AND role_name = $3`,
181 [tenantId, uid, name],
182 );
183 return {
184 ok: true,
185 engine: 'briven-engine',
186 storage: 'doltgres',
187 message: (res.rowCount ?? 0) > 0 ? 'unassigned' : 'not_assigned',
188 };
189}
190
191export async function deleteBrivenEngineRole(
192 role: string,
193 opts?: { projectId?: string; tenantId?: string },
194): Promise<{ ok: boolean; engine: 'briven-engine'; storage: 'doltgres'; message?: string }> {
195 if (!isAuthCoreInitialized()) {
196 return {
197 ok: false,
198 engine: 'briven-engine',
199 storage: 'doltgres',
200 message: 'engine not ready',
201 };
202 }
203 const name = role.trim().toLowerCase();
204 if (!name) {
205 return {
206 ok: false,
207 engine: 'briven-engine',
208 storage: 'doltgres',
209 message: 'role required',
210 };
211 }
212 const tenantId =
213 opts?.tenantId ??
214 (opts?.projectId ? projectIdToTenantId(opts.projectId) : 'public');
215 const pool = getEnginePool();
216 await pool.query(
217 `DELETE FROM be_user_roles WHERE tenant_id = $1 AND role_name = $2`,
218 [tenantId, name],
219 );
220 const res = await pool.query(
221 `DELETE FROM be_roles WHERE tenant_id = $1 AND role_name = $2`,
222 [tenantId, name],
223 );
224 return {
225 ok: (res.rowCount ?? 0) > 0,
226 engine: 'briven-engine',
227 storage: 'doltgres',
228 message: (res.rowCount ?? 0) > 0 ? 'deleted' : 'not_found',
229 };
230}
231
232export async function getBrivenEngineUserRoles(
233 userId: string,
234 opts?: { projectId?: string; tenantId?: string },
235): Promise<{
236 roles: string[];
237 permissions: string[];
238 engine: 'briven-engine';
239 storage: 'doltgres';
240}> {
241 if (!isAuthCoreInitialized()) {
242 return {
243 roles: [],
244 permissions: [],
245 engine: 'briven-engine',
246 storage: 'doltgres',
247 };
248 }
249 const tenantId =
250 opts?.tenantId ??
251 (opts?.projectId ? projectIdToTenantId(opts.projectId) : 'public');
252 const pool = getEnginePool();
253 const res = await pool.query(
254 `SELECT ur.role_name, r.permissions_json
255 FROM be_user_roles ur
256 JOIN be_roles r ON r.tenant_id = ur.tenant_id AND r.role_name = ur.role_name
257 WHERE ur.tenant_id = $1 AND ur.user_id = $2`,
258 [tenantId, userId],
259 );
260 const roles: string[] = [];
261 const permSet = new Set<string>();
262 for (const row of res.rows as Array<{
263 role_name: string;
264 permissions_json: string;
265 }>) {
266 roles.push(row.role_name);
267 try {
268 const perms = JSON.parse(row.permissions_json) as string[];
269 for (const p of perms) permSet.add(p);
270 } catch {
271 /* ignore */
272 }
273 }
274 return {
275 roles,
276 permissions: [...permSet],
277 engine: 'briven-engine',
278 storage: 'doltgres',
279 };
280}
281
282export async function listBrivenEngineRoles(opts?: {
283 projectId?: string;
284 tenantId?: string;
285}): Promise<{
286 roles: Array<{ name: string; permissions: string[]; tenantId: string }>;
287 engine: 'briven-engine';
288 storage: 'doltgres';
289}> {
290 if (!isAuthCoreInitialized()) {
291 return { roles: [], engine: 'briven-engine', storage: 'doltgres' };
292 }
293 const pool = getEnginePool();
294 // Dashboard (no filter): all tenants. With project/tenant: that slice only.
295 const scoped =
296 opts?.tenantId ??
297 (opts?.projectId ? projectIdToTenantId(opts.projectId) : null);
298 const res = scoped
299 ? await pool.query(
300 `SELECT tenant_id, role_name, permissions_json FROM be_roles
301 WHERE tenant_id = $1 ORDER BY role_name`,
302 [scoped],
303 )
304 : await pool.query(
305 `SELECT tenant_id, role_name, permissions_json FROM be_roles
306 ORDER BY tenant_id, role_name`,
307 );
308 return {
309 engine: 'briven-engine',
310 storage: 'doltgres',
311 roles: (
312 res.rows as Array<{
313 tenant_id: string;
314 role_name: string;
315 permissions_json: string;
316 }>
317 ).map((r) => {
318 let permissions: string[] = [];
319 try {
320 permissions = JSON.parse(r.permissions_json) as string[];
321 } catch {
322 permissions = [];
323 }
324 return {
325 name: r.role_name,
326 permissions,
327 tenantId: r.tenant_id,
328 };
329 }),
330 };
331}
332
333export async function userHasPermission(
334 userId: string,
335 permission: string,
336 opts?: { projectId?: string; tenantId?: string },
337): Promise<boolean> {
338 const { permissions } = await getBrivenEngineUserRoles(userId, opts);
339 return permissions.includes(permission) || permissions.includes('*');
340}