@oondemand/oon-core-back 0.3.73 → 0.3.75

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,6 +7,7 @@ const {
7
7
  getAppCode,
8
8
  getTimeout,
9
9
  } = require("../utils/activationProvider");
10
+ const { resolveUserAccess } = require("../rbac/storage");
10
11
 
11
12
  async function defaultVerifyToken(token, { req } = {}) {
12
13
  const baseUrl = getBaseUrl(req);
@@ -71,6 +72,24 @@ function resolveVerifyToken() {
71
72
  return defaultVerifyToken;
72
73
  }
73
74
 
75
+ function isFirstAccessRoute(req) {
76
+ const path = String(req.originalUrl || req.url || "").split("?")[0];
77
+ return path.endsWith("/auth/validar-token") || path.endsWith("/auth/first-access/password");
78
+ }
79
+
80
+ async function applyEffectiveAccess(usuario) {
81
+ const access = await resolveUserAccess(usuario);
82
+ return {
83
+ ...usuario,
84
+ roleCode: access.roleCode,
85
+ permissions: access.permissions,
86
+ accessStatus: access.status,
87
+ mustChangePassword: access.mustChangePassword,
88
+ firstAccessPending: access.mustChangePassword,
89
+ rbacMode: access.mode,
90
+ };
91
+ }
92
+
74
93
  const authMiddleware = async (req, res, next) => {
75
94
  const token = req.headers.authorization?.split(" ")[1];
76
95
  if (!token) {
@@ -82,19 +101,31 @@ const authMiddleware = async (req, res, next) => {
82
101
  }
83
102
 
84
103
  try {
85
- const usuario = await resolveVerifyToken()(token, { req });
86
- if (!usuario) {
104
+ const providerUser = await resolveVerifyToken()(token, { req });
105
+ if (!providerUser) {
87
106
  const error = new Error("Usuário não resolvido.");
88
107
  error.statusCode = 401;
89
108
  throw error;
90
109
  }
110
+ const usuario = await applyEffectiveAccess(providerUser);
91
111
  req.usuario = usuario;
112
+
113
+ if (usuario.mustChangePassword && !isFirstAccessRoute(req)) {
114
+ return Helpers.sendErrorResponse({
115
+ res,
116
+ message: "Conclua a definição da nova senha antes de acessar a Central.",
117
+ statusCode: 403,
118
+ code: "FIRST_ACCESS_REQUIRED",
119
+ });
120
+ }
121
+
92
122
  next();
93
123
  } catch (error) {
94
124
  return Helpers.sendErrorResponse({
95
125
  res,
96
126
  message: error.message || "Token inválido ou erro na autenticação.",
97
127
  statusCode: error.statusCode || error.response?.status || 401,
128
+ code: error.code,
98
129
  });
99
130
  }
100
131
  };
@@ -103,3 +134,5 @@ module.exports = authMiddleware;
103
134
  module.exports.defaultVerifyToken = defaultVerifyToken;
104
135
  module.exports.localDevelopmentVerifyToken = localDevelopmentVerifyToken;
105
136
  module.exports.resolveVerifyToken = resolveVerifyToken;
137
+ module.exports.applyEffectiveAccess = applyEffectiveAccess;
138
+ 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
+ };