panels-origin/api/scope_enforcement.ts
Cursor Agent aed2f4531b
Implement client IAM v2: roles per tenant, CRUD matrix, scope, and panel UI
- Add IAM v2 auth with role_id, is_owner, status checks, and API key without tenant_admin bypass
- Enforce CRUD permissions and project/warehouse scope across API routes
- Rewrite usuarios.vue with ribbon, tree, user/role management, and permission matrix
- Add dynamic menu, route guards, usePermissions/useScope composables
- Apply role templates on create; filter project docs by category; hide cost tab without permission
- Add iam_test unit tests; update bootstrap and SaaS tenant admin to use role_id

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 18:16:59 +00:00

116 lines
3.9 KiB
TypeScript

import type { Context } from "hono";
import type { AuthUser } from "./auth.ts";
import type { Db } from "./db.ts";
import { respondApiError, routeLabel, type RpcEnvelope } from "./http_errors.ts";
import { hasPermission } from "./permissions.ts";
import { projectDocPerm } from "./iam_catalog.ts";
import { assertProjectAccess, getUserScope, type UserScope } from "./user_scope.ts";
export async function denyUnlessProjectScope(
c: Context,
projectId: number | null | undefined,
): Promise<boolean> {
if (projectId == null || !Number.isFinite(projectId)) return true;
const user = c.get("user") as AuthUser;
if (user.realm === "platform" || user.is_owner) return true;
const scope = await getUserScope(user);
if (assertProjectAccess(scope, projectId)) return true;
respondApiError(c, "FORBIDDEN", "Obra fuera de su alcance", {
route: routeLabel(c),
project_id: projectId,
});
return false;
}
export function filterProjectsByScope<T extends { id: number }>(
items: T[],
scope: UserScope,
): T[] {
if (scope.allProjects) return items;
const allowed = new Set(scope.projectIds);
return items.filter((p) => allowed.has(Number(p.id)));
}
export async function filterProjectListEnvelope(
c: Context,
env: RpcEnvelope<{ projects?: Array<{ id: number }> }>,
): Promise<RpcEnvelope<{ projects?: Array<{ id: number }> }>> {
const user = c.get("user") as AuthUser;
if (!env.ok || !env.data?.projects || user.realm === "platform" || user.is_owner) return env;
const scope = await getUserScope(user);
return {
...env,
data: {
...env.data,
projects: filterProjectsByScope(env.data.projects, scope),
},
};
}
export async function projectDocCategory(
db: Db,
typeCode: string,
): Promise<string> {
const row = await db.prepare(
"SELECT category FROM project_document_types WHERE code = ?",
).get(typeCode) as { category: string } | undefined;
return row?.category ?? "otro";
}
export async function denyUnlessProjectDocView(
c: Context,
db: Db,
typeCode: string,
): Promise<boolean> {
const user = c.get("user") as AuthUser;
if (user.realm === "platform" || user.is_owner) return true;
const category = await projectDocCategory(db, typeCode);
if (await hasPermission(user, projectDocPerm(category, "view"))) return true;
respondApiError(c, "FORBIDDEN", "Sin permiso para este tipo de documento de obra", {
route: routeLabel(c),
type_code: typeCode,
category,
});
return false;
}
export async function denyUnlessProjectDocCreate(
c: Context,
db: Db,
typeCode: string,
): Promise<boolean> {
const user = c.get("user") as AuthUser;
if (user.realm === "platform" || user.is_owner) return true;
const category = await projectDocCategory(db, typeCode);
if (await hasPermission(user, projectDocPerm(category, "create"))) return true;
respondApiError(c, "FORBIDDEN", "Sin permiso para subir este tipo de documento", {
route: routeLabel(c),
type_code: typeCode,
category,
});
return false;
}
export async function filterProjectDocumentsForUser(
user: AuthUser,
db: Db,
documents: Array<{ type_code?: string; type?: string }>,
): Promise<typeof documents> {
if (user.realm === "platform" || user.is_owner) return documents;
const out: typeof documents = [];
for (const doc of documents) {
const typeCode = String(doc.type_code ?? doc.type ?? "");
const category = await projectDocCategory(db, typeCode);
if (await hasPermission(user, projectDocPerm(category, "view"))) out.push(doc);
}
return out;
}
export async function denyUnlessWarehouseCentral(c: Context): Promise<boolean> {
const user = c.get("user") as AuthUser;
if (user.realm === "platform" || user.is_owner) return true;
const scope = await getUserScope(user);
if (scope.warehouseCentral) return true;
respondApiError(c, "FORBIDDEN", "Sin acceso al almacén central", { route: routeLabel(c) });
return false;
}