import type { Context, Next } from "hono"; import type { AuthUser } from "./auth.ts"; import { tenantScope } from "./auth.ts"; import { withIamTenant } from "./iam_db.ts"; import { respondApiError, routeLabel } from "./http_errors.ts"; import { ALL_PERMISSION_CODES, IAM_SENSITIVE_PREFIXES } from "./iam_catalog.ts"; const permissionCache = new Map; at: number }>(); const CACHE_TTL_MS = 60_000; export function invalidatePermissionCache(roleId?: number) { if (roleId != null) { for (const key of [...permissionCache.keys()]) { if (key.startsWith(`${roleId}:`)) permissionCache.delete(key); } return; } permissionCache.clear(); } export async function userPermissionsByRoleId( tenantId: number | null, roleId: number | null, isOwner: boolean, ): Promise> { if (isOwner) return new Set(ALL_PERMISSION_CODES); if (roleId == null) return new Set(); const key = `${roleId}:${tenantId ?? 0}`; const hit = permissionCache.get(key); if (hit && Date.now() - hit.at < CACHE_TTL_MS) return hit.perms; const rows = await withIamTenant(tenantId, async (db) => await db.prepare( "SELECT permission_code FROM role_permissions WHERE role_id = ?", ).all(roleId) as { permission_code: string }[], ); const perms = new Set(rows.map((r) => r.permission_code)); permissionCache.set(key, { perms, at: Date.now() }); return perms; } /** @deprecated use userPermissionsByRoleId */ export async function userPermissions( tenantId: number | null, roleCode: string, ): Promise> { if (roleCode === "tenant_admin") return new Set(ALL_PERMISSION_CODES); const rows = await withIamTenant(tenantId, async (db) => await db.prepare( `SELECT rp.permission_code FROM role_permissions rp JOIN roles r ON r.id = rp.role_id WHERE r.code = ? AND (r.tenant_id = ? OR (r.is_system AND r.tenant_id IS NULL))`, ).all(roleCode, tenantId) as { permission_code: string }[], ); return new Set(rows.map((r) => r.permission_code)); } export async function hasPermission(user: AuthUser, code: string): Promise { if (user.realm === "platform") return true; if (user.is_owner) return true; const perms = await userPermissionsByRoleId(user.tenant_id, user.role_id, false); return perms.has(code); } export function requirePermission(code: string) { return async (c: Context, next: Next) => { const user = c.get("user") as AuthUser; if (user.realm === "platform") { await next(); return; } if (!await hasPermission(user, code)) { return respondApiError( c, "FORBIDDEN", `Permiso requerido: ${code}`, { route: routeLabel(c), permission: code, role: user.role_code }, ); } await next(); }; } export function requireAnyPermission(...codes: string[]) { return async (c: Context, next: Next) => { const user = c.get("user") as AuthUser; if (user.realm === "platform") { await next(); return; } for (const code of codes) { if (await hasPermission(user, code)) { await next(); return; } } return respondApiError( c, "FORBIDDEN", `Se requiere alguno de: ${codes.join(", ")}`, { route: routeLabel(c), permissions: codes }, ); }; } export function filterSensitivePermissions( permissions: string[], actorIsOwner: boolean, ): string[] { if (actorIsOwner) return permissions; return permissions.filter( (p) => !IAM_SENSITIVE_PREFIXES.some((prefix) => p.startsWith(prefix)), ); } export async function callIamFn( tenantId: number | null, fn: string, payload: Record = {}, ): Promise { const row = await withIamTenant(tenantId, async (db) => await db.prepare(`SELECT ${fn}($1::jsonb) AS result`).get(payload) as { result: unknown }, ); const raw = row?.result; if (raw && typeof raw === "object" && "ok" in (raw as object)) { return raw as T; } return null; } export async function usersWithRole(tenantId: number | null, roleId: number): Promise { const rows = await withIamTenant(tenantId, async (db) => await db.prepare("SELECT id FROM users WHERE role_id = ?").all(roleId) as { id: number }[], ); return rows.map((r) => Number(r.id)); } export async function revokeSessionsForRoleUsers( tenantId: number | null, roleId: number, ): Promise { const { revokeAllSessionsForUser } = await import("./sessions.ts"); const ids = await usersWithRole(tenantId, roleId); await Promise.all(ids.map((id) => revokeAllSessionsForUser(id, "app"))); }