@oondemand/oon-core-back 0.3.74 → 0.3.76

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.
@@ -7,10 +7,17 @@ const {
7
7
  getAppCode,
8
8
  getTimeout,
9
9
  } = require("../utils/activationProvider");
10
+ const { resolveUserAccess } = require("../rbac/storage");
11
+ const {
12
+ TENANT_HEADER,
13
+ readRequestedTenantId,
14
+ createAccessContext,
15
+ } = require("../tenancy");
10
16
 
11
17
  async function defaultVerifyToken(token, { req } = {}) {
12
18
  const baseUrl = getBaseUrl(req);
13
19
  const appCode = getAppCode();
20
+ const requestedTenantId = readRequestedTenantId(req);
14
21
 
15
22
  let response;
16
23
  try {
@@ -20,6 +27,7 @@ async function defaultVerifyToken(token, { req } = {}) {
20
27
  headers: {
21
28
  Authorization: `Bearer ${token}`,
22
29
  "x-app-code": appCode,
30
+ ...(requestedTenantId ? { [TENANT_HEADER]: requestedTenantId } : {}),
23
31
  },
24
32
  });
25
33
  } catch (error) {
@@ -27,6 +35,7 @@ async function defaultVerifyToken(token, { req } = {}) {
27
35
  error.response?.data?.message || error.message || "Token inválido ou erro na autenticação.",
28
36
  );
29
37
  err.statusCode = error.response?.status || error.statusCode || 401;
38
+ err.code = error.response?.data?.code || error.code;
30
39
  throw err;
31
40
  }
32
41
 
@@ -40,6 +49,12 @@ async function defaultVerifyToken(token, { req } = {}) {
40
49
  const tipoAcesso = data.aplicativo?.tipoAcesso || data.tipoAcesso || data.tipo || data.perfil;
41
50
  return {
42
51
  ...data,
52
+ acesso: response.data.acesso || data.acesso,
53
+ tenant: response.data.tenant || data.tenant,
54
+ tenancyModel:
55
+ response.data.acesso?.app?.tenancyModel ||
56
+ data.aplicativo?.tenancyModel ||
57
+ data.tenancyModel,
43
58
  tipo: tipoAcesso === "master" ? "admin" : tipoAcesso,
44
59
  perfil: data.perfil || tipoAcesso,
45
60
  nome: data.nome,
@@ -47,18 +62,25 @@ async function defaultVerifyToken(token, { req } = {}) {
47
62
  };
48
63
  }
49
64
 
50
- function localDevelopmentVerifyToken(token) {
65
+ function localDevelopmentVerifyToken(token, { req } = {}) {
51
66
  const expected = String(process.env.DEV_TOKEN || "");
52
67
  if (process.env.NODE_ENV !== "development" || !expected || token !== expected) {
53
68
  const error = new Error("Token local de desenvolvimento inválido.");
54
69
  error.statusCode = 401;
55
70
  throw error;
56
71
  }
72
+ const requestedTenantId = readRequestedTenantId(req);
57
73
  return {
74
+ id: process.env.DEV_USER_ID || "dev-local",
75
+ usuarioId: process.env.DEV_USER_ID || "dev-local",
58
76
  tipo: process.env.DEV_USER_ROLE || "admin",
59
77
  perfil: process.env.DEV_USER_ROLE || "admin",
78
+ roleCode: process.env.DEV_USER_ROLE || "admin",
60
79
  nome: process.env.DEV_USER_NAME || "Dev Local",
61
80
  email: process.env.DEV_USER_EMAIL || "dev@local",
81
+ tenantId: requestedTenantId || process.env.DEV_TENANT_ID,
82
+ appCode: process.env.APP_CODE,
83
+ tenancyModel: process.env.DEV_TENANCY_MODEL || ctx.config.tenancyModel || "single_tenant",
62
84
  capabilities: ctx.config.capabilities || [],
63
85
  };
64
86
  }
@@ -71,6 +93,24 @@ function resolveVerifyToken() {
71
93
  return defaultVerifyToken;
72
94
  }
73
95
 
96
+ function isFirstAccessRoute(req) {
97
+ const path = String(req.originalUrl || req.url || "").split("?")[0];
98
+ return path.endsWith("/auth/validar-token") || path.endsWith("/auth/first-access/password");
99
+ }
100
+
101
+ async function applyEffectiveAccess(usuario) {
102
+ const access = await resolveUserAccess(usuario);
103
+ return {
104
+ ...usuario,
105
+ roleCode: access.roleCode,
106
+ permissions: access.permissions,
107
+ accessStatus: access.status,
108
+ mustChangePassword: access.mustChangePassword,
109
+ firstAccessPending: access.mustChangePassword,
110
+ rbacMode: access.mode,
111
+ };
112
+ }
113
+
74
114
  const authMiddleware = async (req, res, next) => {
75
115
  const token = req.headers.authorization?.split(" ")[1];
76
116
  if (!token) {
@@ -82,19 +122,45 @@ const authMiddleware = async (req, res, next) => {
82
122
  }
83
123
 
84
124
  try {
85
- const usuario = await resolveVerifyToken()(token, { req });
86
- if (!usuario) {
125
+ const requestedTenantId = readRequestedTenantId(req);
126
+ const providerUser = await resolveVerifyToken()(token, { req, requestedTenantId });
127
+ if (!providerUser) {
87
128
  const error = new Error("Usuário não resolvido.");
88
129
  error.statusCode = 401;
89
130
  throw error;
90
131
  }
91
- req.usuario = usuario;
132
+ const usuario = await applyEffectiveAccess(providerUser);
133
+ const accessContext = createAccessContext(usuario, {
134
+ requestedTenantId,
135
+ tenancyModel: ctx.config.tenancyModel || ctx.config.app?.tenancyModel,
136
+ appCode: getAppCode(),
137
+ });
138
+
139
+ req.usuario = {
140
+ ...usuario,
141
+ tenantId: accessContext.tenantId,
142
+ appCode: accessContext.appCode || usuario.appCode,
143
+ };
144
+ // Contexto imutável e validado. Controllers e rotas customizadas devem
145
+ // consumir req.accessContext em vez de tenantId vindo de query/body.
146
+ req.accessContext = accessContext;
147
+
148
+ if (usuario.mustChangePassword && !isFirstAccessRoute(req)) {
149
+ return Helpers.sendErrorResponse({
150
+ res,
151
+ message: "Conclua a definição da nova senha antes de acessar a Central.",
152
+ statusCode: 403,
153
+ code: "FIRST_ACCESS_REQUIRED",
154
+ });
155
+ }
156
+
92
157
  next();
93
158
  } catch (error) {
94
159
  return Helpers.sendErrorResponse({
95
160
  res,
96
161
  message: error.message || "Token inválido ou erro na autenticação.",
97
162
  statusCode: error.statusCode || error.response?.status || 401,
163
+ code: error.code,
98
164
  });
99
165
  }
100
166
  };
@@ -103,3 +169,5 @@ module.exports = authMiddleware;
103
169
  module.exports.defaultVerifyToken = defaultVerifyToken;
104
170
  module.exports.localDevelopmentVerifyToken = localDevelopmentVerifyToken;
105
171
  module.exports.resolveVerifyToken = resolveVerifyToken;
172
+ module.exports.applyEffectiveAccess = applyEffectiveAccess;
173
+ module.exports.isFirstAccessRoute = isFirstAccessRoute;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+
3
+ const Helpers = require("../utils/helpers");
4
+ const { granted } = require("../rbac/policy");
5
+ const { audit } = require("../rbac/storage");
6
+
7
+ function requirePermission(permission) {
8
+ return async (req, res, next) => {
9
+ if (!req.usuario) {
10
+ return Helpers.sendErrorResponse({
11
+ res,
12
+ statusCode: 401,
13
+ message: "Acesso não autorizado.",
14
+ });
15
+ }
16
+
17
+ if (granted(req.usuario.permissions, permission)) return next();
18
+
19
+ await audit("permission.denied", {
20
+ actor: req.usuario,
21
+ target: permission,
22
+ context: {
23
+ method: req.method,
24
+ path: req.originalUrl || req.url,
25
+ requestId: req.id || req.headers["x-request-id"],
26
+ },
27
+ }).catch(() => undefined);
28
+
29
+ return Helpers.sendErrorResponse({
30
+ res,
31
+ statusCode: 403,
32
+ message: "Acesso negado para esta operação.",
33
+ });
34
+ };
35
+ }
36
+
37
+ module.exports = { requirePermission };
@@ -1,22 +1,50 @@
1
1
  const Helpers = require("../utils/helpers");
2
2
 
3
- /**
4
- * RBAC simples baseado em `req.usuario.tipo`. `roles` vazio => qualquer
5
- * usuário autenticado. `admin` sempre passa.
6
- *
7
- * @param {string[]} roles papéis permitidos
8
- */
3
+ const ROLE_ALIASES = Object.freeze({
4
+ admin: ["admin", "administrador", "master"],
5
+ administrador: ["admin", "administrador", "master"],
6
+ master: ["admin", "administrador", "master"],
7
+ operator: ["operator", "operador"],
8
+ operador: ["operator", "operador"],
9
+ manager: ["manager", "gestor"],
10
+ gestor: ["manager", "gestor"],
11
+ developer: ["developer", "desenvolvedor", "dev"],
12
+ desenvolvedor: ["developer", "desenvolvedor", "dev"],
13
+ dev: ["developer", "desenvolvedor", "dev"],
14
+ viewer: ["viewer", "leitura", "consulta", "read"],
15
+ leitura: ["viewer", "leitura", "consulta", "read"],
16
+ consulta: ["viewer", "leitura", "consulta", "read"],
17
+ read: ["viewer", "leitura", "consulta", "read"],
18
+ });
19
+
20
+ function normalize(value) {
21
+ return String(value || "").trim().toLowerCase();
22
+ }
23
+
24
+ function effectiveRoles(usuario) {
25
+ const raw = [usuario?.roleCode, usuario?.tipo, usuario?.perfil]
26
+ .map(normalize)
27
+ .filter(Boolean);
28
+ return [...new Set(raw.flatMap((role) => ROLE_ALIASES[role] || [role]))];
29
+ }
30
+
9
31
  function rbac(roles = []) {
10
32
  return (req, res, next) => {
11
33
  if (!roles.length) return next();
12
- const tipo = req.usuario?.tipo;
13
- if (tipo === "admin" || roles.includes(tipo)) return next();
34
+ const allowed = roles.map(normalize);
35
+ const current = effectiveRoles(req.usuario);
36
+
37
+ // Wildcard é uma permissão efetiva resolvida pelo backend, não um bypass
38
+ // implícito baseado somente no nome do papel.
39
+ if (req.usuario?.permissions?.includes("*")) return next();
40
+ if (current.some((role) => allowed.includes(role))) return next();
41
+
14
42
  return Helpers.sendErrorResponse({
15
43
  res,
16
- statusCode: 403,
17
44
  message: "Acesso negado para o seu perfil.",
45
+ statusCode: 403,
18
46
  });
19
47
  };
20
48
  }
21
49
 
22
- module.exports = { rbac };
50
+ module.exports = { rbac, effectiveRoles };
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+
3
+ const ctx = require("../context");
4
+ const GenericError = require("../errors/GenericError");
5
+ const {
6
+ ensureInstance,
7
+ instanceHeaders,
8
+ providerPost,
9
+ } = require("../utils/instanceActivation");
10
+
11
+ const IDENTITY_PATHS = Object.freeze({
12
+ provisionInvitation: "/integracao/autenticacao-aplicativos/convites/provisionar",
13
+ changeFirstAccessPassword: "/integracao/autenticacao-aplicativos/primeiro-acesso/senha",
14
+ });
15
+
16
+ async function defaultProviderCall(req, path, payload) {
17
+ const instance = await ensureInstance();
18
+ if (!instance?.instanceId || !instance?.credentialEncrypted) {
19
+ throw new GenericError("A instância precisa estar ativa para gerenciar identidades.", {
20
+ statusCode: 423,
21
+ code: "RBAC_INSTANCE_CREDENTIAL_REQUIRED",
22
+ });
23
+ }
24
+
25
+ try {
26
+ const headers = instanceHeaders(instance);
27
+ if (req?.headers?.authorization) headers.Authorization = req.headers.authorization;
28
+ const response = await providerPost(req, path, payload, headers);
29
+ return response.data;
30
+ } catch (error) {
31
+ const statusCode = error.response?.status || error.statusCode || 502;
32
+ const message = error.response?.data?.message || error.message || "Falha ao atualizar a identidade no provedor.";
33
+ throw new GenericError(message, {
34
+ statusCode,
35
+ code: error.response?.data?.code || error.code || "RBAC_IDENTITY_PROVIDER_ERROR",
36
+ });
37
+ }
38
+ }
39
+
40
+ async function provisionInvitation({ req, invitation, password }) {
41
+ const hook = ctx.config.auth?.provisionInvitation;
42
+ if (typeof hook === "function") return hook({ req, invitation, password });
43
+ return defaultProviderCall(req, IDENTITY_PATHS.provisionInvitation, {
44
+ email: invitation.email,
45
+ nome: invitation.name,
46
+ password,
47
+ invitationFingerprint: invitation.tokenFingerprint,
48
+ });
49
+ }
50
+
51
+ async function changeFirstAccessPassword({ req, user, password }) {
52
+ const hook = ctx.config.auth?.changeFirstAccessPassword;
53
+ if (typeof hook === "function") return hook({ req, user, password });
54
+ return defaultProviderCall(req, IDENTITY_PATHS.changeFirstAccessPassword, {
55
+ identityId: user?.id || user?._id || user?.usuarioId,
56
+ email: user?.email,
57
+ password,
58
+ });
59
+ }
60
+
61
+ module.exports = {
62
+ IDENTITY_PATHS,
63
+ defaultProviderCall,
64
+ provisionInvitation,
65
+ changeFirstAccessPassword,
66
+ };
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+
3
+ const ctx = require("../context");
4
+
5
+ const CORE_PERMISSIONS = Object.freeze([
6
+ "users.read",
7
+ "users.invite",
8
+ "users.update",
9
+ "users.disable",
10
+ "roles.read",
11
+ ]);
12
+
13
+ const PERMISSION_PATTERN = /^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$/;
14
+ const ROLE_PATTERN = /^[a-z][a-z0-9_-]*$/;
15
+
16
+ function normalizeText(value) {
17
+ return String(value || "")
18
+ .normalize("NFD")
19
+ .replace(/[\u0300-\u036f]/g, "")
20
+ .trim()
21
+ .toLowerCase();
22
+ }
23
+
24
+ function isPermission(value) {
25
+ return value === "*" || PERMISSION_PATTERN.test(String(value || ""));
26
+ }
27
+
28
+ function isRoleCode(value) {
29
+ return ROLE_PATTERN.test(String(value || ""));
30
+ }
31
+
32
+ function unique(values = []) {
33
+ return [...new Set(values.filter(Boolean).map(String))];
34
+ }
35
+
36
+ function policy(config = ctx.config) {
37
+ const raw = config?.rbac;
38
+ const roles = Array.isArray(raw?.roles) ? raw.roles : [];
39
+ const customPermissions = Array.isArray(raw?.permissions) ? raw.permissions : [];
40
+ return {
41
+ enabled: roles.length > 0,
42
+ roles,
43
+ permissions: unique([...CORE_PERMISSIONS, ...customPermissions]),
44
+ invitationTtlHours: Number(raw?.invitationTtlHours) || 72,
45
+ };
46
+ }
47
+
48
+ function roleByCode(code, config = ctx.config) {
49
+ const requested = normalizeText(code);
50
+ if (!requested) return null;
51
+ return policy(config).roles.find((role) => normalizeText(role.code) === requested) || null;
52
+ }
53
+
54
+ function adminRoles(config = ctx.config) {
55
+ return policy(config).roles.filter((role) => role.admin === true || role.permissions?.includes("*"));
56
+ }
57
+
58
+ function rolePermissions(role) {
59
+ return unique(role?.permissions || []);
60
+ }
61
+
62
+ function granted(permissions, required) {
63
+ if (!required) return true;
64
+ const available = unique(permissions || []);
65
+ if (available.includes("*")) return true;
66
+ const requested = Array.isArray(required) ? required : [required];
67
+ if (!requested.length) return true;
68
+ return requested.some((permission) => available.includes(permission));
69
+ }
70
+
71
+ function legacyPermissions(user) {
72
+ const tipo = normalizeText(user?.tipo);
73
+ const perfil = normalizeText(user?.perfil);
74
+ if ([tipo, perfil].some((value) => ["admin", "master", "administrador"].includes(value))) {
75
+ return ["*"];
76
+ }
77
+
78
+ const values = [
79
+ ...(Array.isArray(user?.permissions) ? user.permissions : []),
80
+ ...(Array.isArray(user?.capabilities) ? user.capabilities : []),
81
+ ...(Array.isArray(user?.permissoes?.capabilities) ? user.permissoes.capabilities : []),
82
+ ...(Array.isArray(user?.aplicativo?.capabilities) ? user.aplicativo.capabilities : []),
83
+ ...(Array.isArray(user?.aplicativo?.permissoes?.capabilities)
84
+ ? user.aplicativo.permissoes.capabilities
85
+ : []),
86
+ ];
87
+ return unique(values.filter(isPermission));
88
+ }
89
+
90
+ function legacyRoleCandidates(user) {
91
+ const raw = unique([user?.roleCode, user?.perfil, user?.tipo]).map(normalizeText);
92
+ const aliases = {
93
+ administrador: "admin",
94
+ master: "admin",
95
+ operador: "operator",
96
+ gestor: "manager",
97
+ leitura: "viewer",
98
+ consulta: "viewer",
99
+ desenvolvedor: "developer",
100
+ };
101
+ return unique(raw.flatMap((value) => [value, aliases[value]]));
102
+ }
103
+
104
+ function matchingRoleForLegacyUser(user, config = ctx.config) {
105
+ const currentPolicy = policy(config);
106
+ const candidates = legacyRoleCandidates(user);
107
+ const direct = currentPolicy.roles.find((role) => {
108
+ const code = normalizeText(role.code);
109
+ const name = normalizeText(role.name);
110
+ return candidates.includes(code) || candidates.includes(name);
111
+ });
112
+ if (direct) return direct;
113
+
114
+ if (legacyPermissions(user).includes("*")) {
115
+ return adminRoles(config)[0] || null;
116
+ }
117
+
118
+ const nonAdmin = currentPolicy.roles.filter((role) => !role.admin && !role.permissions?.includes("*"));
119
+ return nonAdmin.length === 1 ? nonAdmin[0] : null;
120
+ }
121
+
122
+ function canAssignRole(actorPermissions, targetRole) {
123
+ const actor = unique(actorPermissions || []);
124
+ if (actor.includes("*")) return true;
125
+ const target = rolePermissions(targetRole);
126
+ if (target.includes("*")) return false;
127
+ return target.every((permission) => actor.includes(permission));
128
+ }
129
+
130
+ function publicPolicy(config = ctx.config) {
131
+ const currentPolicy = policy(config);
132
+ return {
133
+ central: {
134
+ appCode: String(config.appCode || config.slug || "central"),
135
+ name: String(config.name || config.slug || "Central Oon"),
136
+ },
137
+ enabled: currentPolicy.enabled,
138
+ permissions: currentPolicy.permissions,
139
+ roles: currentPolicy.roles.map((role) => ({
140
+ code: role.code,
141
+ name: role.name,
142
+ description: role.description,
143
+ admin: Boolean(role.admin),
144
+ permissions: rolePermissions(role),
145
+ })),
146
+ invitationTtlHours: currentPolicy.invitationTtlHours,
147
+ };
148
+ }
149
+
150
+ module.exports = {
151
+ CORE_PERMISSIONS,
152
+ PERMISSION_PATTERN,
153
+ ROLE_PATTERN,
154
+ normalizeText,
155
+ isPermission,
156
+ isRoleCode,
157
+ unique,
158
+ policy,
159
+ roleByCode,
160
+ adminRoles,
161
+ rolePermissions,
162
+ granted,
163
+ legacyPermissions,
164
+ legacyRoleCandidates,
165
+ matchingRoleForLegacyUser,
166
+ canAssignRole,
167
+ publicPolicy,
168
+ };