@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.
- package/package.json +1 -1
- package/src/api/access.routes.js +353 -0
- package/src/api/auth.routes.js +39 -34
- package/src/boot/createApp.js +2 -2
- package/src/core/define.js +5 -1
- package/src/core/factories/modelRouter.js +18 -15
- package/src/core/middlewares/auth.js +35 -2
- package/src/core/middlewares/permissions.js +37 -0
- package/src/core/middlewares/rbac.js +38 -10
- package/src/core/rbac/identityProvider.js +66 -0
- package/src/core/rbac/policy.js +168 -0
- package/src/core/rbac/storage.js +387 -0
- package/src/index.js +8 -0
- package/src/manifest/appManifest.js +26 -104
- package/src/manifest/rbacManifest.js +89 -0
|
@@ -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
|
|
86
|
-
if (!
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
13
|
-
|
|
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
|
+
};
|