@mybuildex/pkg 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,6 +51,32 @@ createAuthMiddleware({
51
51
  createTenantMiddleware({getTenantClient, defaultClient, strictTenant: true});
52
52
  ```
53
53
 
54
+ ## 0.7.0 — ACL por endpoint, auditoria de escrita e permissões
55
+
56
+ Módulos novos, todos opt-in (API que não os registra não muda nada). Nova dependência: `redis`.
57
+
58
+ - **Auditoria** — `createMultitenantPrisma({..., audit: {api, tables: "*"}})` aplica uma extensão Prisma que registra toda escrita (create/update/upsert/delete e `*Many`) em `audit_logs` da escola: quem (uid, e-mail, `users.id`, `users.id_people`, `peoples.name`), rota, ação de ACL, antes/depois (no update, só o diff) e status. `createAuditPlugin({api, getTenantClient})` abre o contexto por request e grava num único INSERT por schema no `onSend`, **antes** da resposta (no Cloud Run a CPU é cortada depois). Todo 403 vira linha `acl_denied`/`forbidden` (limite de 30 por usuário+rota por minuto) e cada request gera um log `http_access`. Falha na gravação nunca derruba a escrita (log `audit_write_failed`). Exige a tabela `audit_logs` (migrations `2026_09_29_000173`–`000175` do mybuilder-mysql). SQL cru: `auditRaw()`.
59
+ - **ACL por endpoint** — `createAclPlugin({api, mode})` uma vez no `loadPlugins` + `useModuleAcl(fastify, router, overrides?, {resource?})` no início de cada controller. A regra vem do mapa do front (`ACL_MAP`, gerado de `mybuilder-plataforma-front/src/utils/permissions.js` — o mesmo agregador que o front usa, com a ordem dos spreads — por `npm run acl:generate`; o teste `acl-map.spec.ts` acusa divergência). Ações padrão do CRUD gerado: `ALL→list`, `FIND→find`, `PAGINATE→paginate`, `TRASH→trash` (recurso sem a ação própria cai em `list`), `CREATE/CLONE→add`, `UPDATE→edit`, `REMOVE→remover`, `RESTORE→restore`, `DELETE→delete`. Overrides: `"recurso.acao"`, lista (qualquer uma libera), `{anyOf, allOf}`, `"@authenticated"`, `"@public"`. Modos: `off`, `report` (só loga `acl_would_deny`) e `enforce` (403). `getAclRouteReport(fastify)` lista rotas sem ACL ou com configuração inválida (para teste de cobertura).
60
+ - **Permissões** — `createPermissionsResolver({redis})` como `onTenantResolved` do tenant middleware: preenche `request.permissions`, `is_full`/`is_teacher`/`is_student`/`is_responsible` pela cadeia users → users_profiles → profiles_policies → policies_rules → rules_permissions → permissions. Fail-closed (erro = tudo false + `permissions_error`). Cache Redis versionado por escola; `invalidatePermissionsCache(redis, idSchool)` (INCR, O(1)) depois de escrever em perfis/políticas/regras/permissões.
61
+ - **Redis** — `createRedisClient()` lê o JSON da env `REDIS`; best-effort (sem env, JSON inválido ou falha → `undefined`), timeout de conexão de 3 s.
62
+ - **Serviço interno** — request autenticado pelo `internalAuth` do auth (`request.internal === true`) não passa pela ACL, não consulta permissões e entra na auditoria com `origin = SERVICE`.
63
+
64
+ ```ts
65
+ const mt = createMultitenantPrismaMariaDb<PrismaClient>({..., audit: {api: settings.PREFIX, tables: "*"}});
66
+
67
+ // loadPlugins — depois do request-scope, antes do autoload dos módulos
68
+ const redis = await createRedisClient();
69
+ await createAuditPlugin({api: settings.PREFIX, getTenantClient})(fastify);
70
+ await createAclPlugin({api: settings.PREFIX, mode: aclMode()})(fastify); // off | report | enforce
71
+
72
+ createTenantMiddleware({getTenantClient, defaultClient, strictTenant: true,
73
+ onTenantResolved: createPermissionsResolver({redis})});
74
+
75
+ // cada controller
76
+ useModuleAcl(fastify, citiesRouter);
77
+ useModuleAcl(fastify, payRouter, {PUBLIC_HASH: "@public", SETTLE: "pay.settle"});
78
+ ```
79
+
54
80
  ## 0.6.0 — segurança e confiabilidade
55
81
 
56
82
  Correções em módulos existentes. **Três mudam comportamento por padrão** (marcadas com ⚠️) — conferir antes de atualizar uma API.
@@ -0,0 +1,32 @@
1
+ export type AclMap = Readonly<Record<string, Readonly<Record<string, readonly string[]>>>>;
2
+ /** Regra de ACL de uma rota. Entradas no formato `recurso.acao` do mapa do front. */
3
+ export type AclRule = string | string[] | {
4
+ anyOf?: string[];
5
+ allOf?: string[];
6
+ }
7
+ /** Qualquer usuário autenticado (a auth já barrou quem não tem token). */
8
+ | "@authenticated"
9
+ /** Rota pública: a ACL não se aplica. */
10
+ | "@public";
11
+ export interface ResolvedAclRule {
12
+ kind: "entries" | "authenticated" | "public";
13
+ /** Entradas `recurso.acao` que liberam (qualquer uma). */
14
+ anyOf: string[];
15
+ /** Entradas `recurso.acao` que precisam liberar TODAS. */
16
+ allOf: string[];
17
+ /** União dos identificadores do anyOf (o que o isAclArray recebe). */
18
+ anyOfIds: string[];
19
+ /** Identificadores de cada entrada do allOf. */
20
+ allOfIds: string[][];
21
+ }
22
+ export declare class AclConfigError extends Error {
23
+ constructor(message: string);
24
+ }
25
+ /** Espelho do isAclArray do front: `required` é a lista `acl` de uma entrada. */
26
+ export declare function checkAcl(userPermissions: readonly string[] | null | undefined, required: readonly string[]): boolean;
27
+ /** Identificadores de uma entrada `recurso.acao` (lança se não existir no mapa). */
28
+ export declare function aclEntryIds(entry: string, map?: AclMap): readonly string[];
29
+ /** Valida a regra contra o mapa e pré-calcula os identificadores. */
30
+ export declare function resolveAclRule(rule: AclRule, map?: AclMap): ResolvedAclRule;
31
+ /** Avalia a regra já resolvida contra as permissões do usuário. */
32
+ export declare function evaluateAclRule(userPermissions: readonly string[] | null | undefined, rule: ResolvedAclRule): boolean;
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ // Checagem de ACL — espelho EXATO do `isAclArray` do front
3
+ // (src/utils/validations.js + utils/roles.js do front da plataforma):
4
+ //
5
+ // - libera se o usuário tem QUALQUER identificador da lista (OR);
6
+ // - papéis aceitam o nome em en ou pt-BR (teacher/professor, student/aluno):
7
+ // 'teacher' na lista libera quem tem 'professor' e vice-versa;
8
+ // - `full` libera tudo;
9
+ // - lista só com nomes de UM papel (['teacher'], ['teacher','professor'],
10
+ // ['aluno']...) nega sempre (regra do front: papel sozinho não libera ação).
11
+ //
12
+ // Como o front aplica a mesma lista para mostrar/esconder botão e rota, a
13
+ // API não pode ser nem mais nem menos permissiva que isso.
14
+ //
15
+ // Uma rota da API pode aceitar MAIS DE UMA entrada do mapa (`recurso.acao`):
16
+ // a mesma chamada é feita por telas diferentes (ex.: o diário do professor
17
+ // grava em classes_dailys pela tela de turmas, liberada por classes.daily).
18
+ // - lista/`anyOf`: a união dos identificadores das entradas, avaliada de
19
+ // uma vez (qualquer um libera);
20
+ // - `allOf`: cada entrada avaliada à parte — todas precisam liberar.
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.AclConfigError = void 0;
23
+ exports.checkAcl = checkAcl;
24
+ exports.aclEntryIds = aclEntryIds;
25
+ exports.resolveAclRule = resolveAclRule;
26
+ exports.evaluateAclRule = evaluateAclRule;
27
+ const map_generated_1 = require("./map.generated");
28
+ const roles_1 = require("./roles");
29
+ class AclConfigError extends Error {
30
+ constructor(message) {
31
+ super(message);
32
+ this.name = "AclConfigError";
33
+ }
34
+ }
35
+ exports.AclConfigError = AclConfigError;
36
+ /** Espelho do isAclArray do front: `required` é a lista `acl` de uma entrada. */
37
+ function checkAcl(userPermissions, required) {
38
+ const acl = Array.isArray(userPermissions) ? userPermissions.map(String) : [];
39
+ const names = required.map(n => String(n).trim());
40
+ let result = false;
41
+ for (const name of names) {
42
+ if ((0, roles_1.hasAclName)(acl, name))
43
+ result = true;
44
+ }
45
+ if ((0, roles_1.isSingleRoleList)(names))
46
+ return false;
47
+ return result || acl.includes("full");
48
+ }
49
+ /** Identificadores de uma entrada `recurso.acao` (lança se não existir no mapa). */
50
+ function aclEntryIds(entry, map = map_generated_1.ACL_MAP) {
51
+ const dot = entry.lastIndexOf(".");
52
+ if (dot <= 0 || dot === entry.length - 1) {
53
+ throw new AclConfigError(`entrada de ACL inválida: "${entry}" (use recurso.acao, ex.: classes.edit)`);
54
+ }
55
+ const resource = entry.slice(0, dot);
56
+ const action = entry.slice(dot + 1);
57
+ const ids = map[resource]?.[action];
58
+ if (!ids)
59
+ throw new AclConfigError(`entrada de ACL inexistente no mapa do front: "${entry}"`);
60
+ return ids;
61
+ }
62
+ function union(lists) {
63
+ const out = [];
64
+ for (const list of lists)
65
+ for (const id of list)
66
+ if (!out.includes(id))
67
+ out.push(id);
68
+ return out;
69
+ }
70
+ /** Valida a regra contra o mapa e pré-calcula os identificadores. */
71
+ function resolveAclRule(rule, map = map_generated_1.ACL_MAP) {
72
+ if (rule === "@authenticated" || rule === "@public") {
73
+ return { kind: rule === "@public" ? "public" : "authenticated", anyOf: [], allOf: [], anyOfIds: [], allOfIds: [] };
74
+ }
75
+ let anyOf;
76
+ let allOf;
77
+ if (typeof rule === "string") {
78
+ anyOf = [rule];
79
+ allOf = [];
80
+ }
81
+ else if (Array.isArray(rule)) {
82
+ anyOf = [...rule];
83
+ allOf = [];
84
+ }
85
+ else if (rule && typeof rule === "object") {
86
+ anyOf = [...(rule.anyOf ?? [])];
87
+ allOf = [...(rule.allOf ?? [])];
88
+ }
89
+ else {
90
+ throw new AclConfigError(`regra de ACL inválida: ${JSON.stringify(rule)}`);
91
+ }
92
+ if (anyOf.length === 0 && allOf.length === 0) {
93
+ throw new AclConfigError("regra de ACL vazia: informe ao menos uma entrada recurso.acao");
94
+ }
95
+ return {
96
+ kind: "entries",
97
+ anyOf,
98
+ allOf,
99
+ anyOfIds: union(anyOf.map(e => aclEntryIds(e, map))),
100
+ allOfIds: allOf.map(e => [...aclEntryIds(e, map)]),
101
+ };
102
+ }
103
+ /** Avalia a regra já resolvida contra as permissões do usuário. */
104
+ function evaluateAclRule(userPermissions, rule) {
105
+ if (rule.kind !== "entries")
106
+ return true;
107
+ if (rule.anyOf.length > 0 && !checkAcl(userPermissions, rule.anyOfIds))
108
+ return false;
109
+ for (const ids of rule.allOfIds)
110
+ if (!checkAcl(userPermissions, ids))
111
+ return false;
112
+ return true;
113
+ }
114
+ //# sourceMappingURL=check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.js","sourceRoot":"","sources":["../../src/acl/check.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export declare const ACL_MAP: Readonly<Record<string, Readonly<Record<string, readonly string[]>>>>;