roles.ts340 lines · main
| 1 | /** |
| 2 | * briven-engine roles + permissions on Doltgres. |
| 3 | */ |
| 4 | |
| 5 | import { getEnginePool } from './db.js'; |
| 6 | import { isAuthCoreInitialized } from './engine.js'; |
| 7 | import { projectIdToTenantId } from './project-map.js'; |
| 8 | |
| 9 | async 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 | |
| 23 | export 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 | |
| 79 | export 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 | |
| 152 | export 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 | |
| 191 | export 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 | |
| 232 | export 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 | |
| 282 | export 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 | |
| 333 | export 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 | } |