@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.
- 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/errors/GenericError.js +3 -2
- package/src/core/factories/crudController.js +96 -27
- package/src/core/factories/crudService.js +101 -22
- package/src/core/factories/modelRouter.js +18 -15
- package/src/core/middlewares/auth.js +72 -4
- 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/core/tenancy.js +200 -0
- package/src/index.js +19 -0
- package/src/manifest/appManifest.js +26 -104
- package/src/manifest/rbacManifest.js +89 -0
|
@@ -4,22 +4,13 @@ const fs = require("node:fs");
|
|
|
4
4
|
const path = require("node:path");
|
|
5
5
|
const ctx = require("../core/context");
|
|
6
6
|
const corePackage = require("../../package.json");
|
|
7
|
+
const { validateRbac } = require("./rbacManifest");
|
|
7
8
|
|
|
8
9
|
const APP_MANIFEST_FILENAME = "central.app.json";
|
|
9
10
|
const APP_MANIFEST_SCHEMA_VERSION = 1;
|
|
10
|
-
const SUPPORTED_APP_KINDS = Object.freeze([
|
|
11
|
-
"root-central",
|
|
12
|
-
"member-central",
|
|
13
|
-
"portal-cockpit",
|
|
14
|
-
]);
|
|
11
|
+
const SUPPORTED_APP_KINDS = Object.freeze(["root-central", "member-central", "portal-cockpit"]);
|
|
15
12
|
const SUPPORTED_MODULES = Object.freeze([
|
|
16
|
-
"collections",
|
|
17
|
-
"documents",
|
|
18
|
-
"pipelines",
|
|
19
|
-
"integrations",
|
|
20
|
-
"omie",
|
|
21
|
-
"assistants",
|
|
22
|
-
"currencies",
|
|
13
|
+
"collections", "documents", "pipelines", "integrations", "omie", "assistants", "currencies",
|
|
23
14
|
]);
|
|
24
15
|
const APP_KIND_ROLE = Object.freeze({
|
|
25
16
|
"root-central": "root",
|
|
@@ -41,22 +32,17 @@ class AppManifestError extends Error {
|
|
|
41
32
|
function isPlainObject(value) {
|
|
42
33
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
43
34
|
}
|
|
44
|
-
|
|
45
35
|
function hasOwn(object, key) {
|
|
46
36
|
return Object.prototype.hasOwnProperty.call(object, key);
|
|
47
37
|
}
|
|
48
|
-
|
|
49
38
|
function addIssue(issues, fieldPath, message) {
|
|
50
39
|
issues.push({ path: fieldPath, message });
|
|
51
40
|
}
|
|
52
|
-
|
|
53
41
|
function parseVersion(value) {
|
|
54
42
|
if (typeof value !== "string") return null;
|
|
55
43
|
const match = value.trim().match(/^(\d+)\.(\d+)\.(\d+)(?:[-+][0-9A-Za-z.-]+)?$/);
|
|
56
|
-
|
|
57
|
-
return match.slice(1, 4).map(Number);
|
|
44
|
+
return match ? match.slice(1, 4).map(Number) : null;
|
|
58
45
|
}
|
|
59
|
-
|
|
60
46
|
function compareVersions(left, right) {
|
|
61
47
|
for (let index = 0; index < 3; index += 1) {
|
|
62
48
|
if (left[index] !== right[index]) return left[index] < right[index] ? -1 : 1;
|
|
@@ -73,50 +59,28 @@ function validateCompatibility(compatibility, issues, coreVersion) {
|
|
|
73
59
|
addIssue(issues, "compatibility.core", "deve ser um objeto.");
|
|
74
60
|
return;
|
|
75
61
|
}
|
|
76
|
-
|
|
77
62
|
const allowedKeys = new Set(["minVersion", "maxVersionExclusive"]);
|
|
78
63
|
for (const key of Object.keys(compatibility.core)) {
|
|
79
64
|
if (!allowedKeys.has(key)) addIssue(issues, `compatibility.core.${key}`, "propriedade não suportada.");
|
|
80
65
|
}
|
|
81
|
-
|
|
82
66
|
const minVersion = parseVersion(compatibility.core.minVersion);
|
|
83
67
|
const maxVersion = hasOwn(compatibility.core, "maxVersionExclusive")
|
|
84
68
|
? parseVersion(compatibility.core.maxVersionExclusive)
|
|
85
69
|
: null;
|
|
86
|
-
|
|
87
|
-
if (!minVersion) {
|
|
88
|
-
addIssue(issues, "compatibility.core.minVersion", "deve usar versão semântica, por exemplo 0.3.45.");
|
|
89
|
-
}
|
|
70
|
+
if (!minVersion) addIssue(issues, "compatibility.core.minVersion", "deve usar versão semântica, por exemplo 0.3.45.");
|
|
90
71
|
if (hasOwn(compatibility.core, "maxVersionExclusive") && !maxVersion) {
|
|
91
|
-
addIssue(
|
|
92
|
-
issues,
|
|
93
|
-
"compatibility.core.maxVersionExclusive",
|
|
94
|
-
"deve usar versão semântica, por exemplo 0.4.0.",
|
|
95
|
-
);
|
|
72
|
+
addIssue(issues, "compatibility.core.maxVersionExclusive", "deve usar versão semântica, por exemplo 0.4.0.");
|
|
96
73
|
}
|
|
97
74
|
if (minVersion && maxVersion && compareVersions(minVersion, maxVersion) >= 0) {
|
|
98
|
-
addIssue(
|
|
99
|
-
issues,
|
|
100
|
-
"compatibility.core",
|
|
101
|
-
"minVersion deve ser menor que maxVersionExclusive.",
|
|
102
|
-
);
|
|
75
|
+
addIssue(issues, "compatibility.core", "minVersion deve ser menor que maxVersionExclusive.");
|
|
103
76
|
}
|
|
104
|
-
|
|
105
77
|
const installedVersion = parseVersion(coreVersion);
|
|
106
78
|
if (!installedVersion || !minVersion) return;
|
|
107
79
|
if (compareVersions(installedVersion, minVersion) < 0) {
|
|
108
|
-
addIssue(
|
|
109
|
-
issues,
|
|
110
|
-
"compatibility.core.minVersion",
|
|
111
|
-
`requer OonCore ${compatibility.core.minVersion} ou superior; instalado ${coreVersion}.`,
|
|
112
|
-
);
|
|
80
|
+
addIssue(issues, "compatibility.core.minVersion", `requer OonCore ${compatibility.core.minVersion} ou superior; instalado ${coreVersion}.`);
|
|
113
81
|
}
|
|
114
82
|
if (maxVersion && compareVersions(installedVersion, maxVersion) >= 0) {
|
|
115
|
-
addIssue(
|
|
116
|
-
issues,
|
|
117
|
-
"compatibility.core.maxVersionExclusive",
|
|
118
|
-
`requer OonCore anterior a ${compatibility.core.maxVersionExclusive}; instalado ${coreVersion}.`,
|
|
119
|
-
);
|
|
83
|
+
addIssue(issues, "compatibility.core.maxVersionExclusive", `requer OonCore anterior a ${compatibility.core.maxVersionExclusive}; instalado ${coreVersion}.`);
|
|
120
84
|
}
|
|
121
85
|
}
|
|
122
86
|
|
|
@@ -133,10 +97,7 @@ function validateActivation(activation, issues) {
|
|
|
133
97
|
if (hasOwn(activation, "enabled") && typeof activation.enabled !== "boolean") {
|
|
134
98
|
addIssue(issues, "activation.enabled", "deve ser booleano.");
|
|
135
99
|
}
|
|
136
|
-
if (
|
|
137
|
-
hasOwn(activation, "configurationVersion")
|
|
138
|
-
&& (!Number.isInteger(activation.configurationVersion) || activation.configurationVersion < 1)
|
|
139
|
-
) {
|
|
100
|
+
if (hasOwn(activation, "configurationVersion") && (!Number.isInteger(activation.configurationVersion) || activation.configurationVersion < 1)) {
|
|
140
101
|
addIssue(issues, "activation.configurationVersion", "deve ser inteiro maior ou igual a 1.");
|
|
141
102
|
}
|
|
142
103
|
if (hasOwn(activation, "fields") && !Array.isArray(activation.fields)) {
|
|
@@ -147,84 +108,50 @@ function validateActivation(activation, issues) {
|
|
|
147
108
|
function validateAppManifest(input, { coreVersion = corePackage.version } = {}) {
|
|
148
109
|
const issues = [];
|
|
149
110
|
if (!isPlainObject(input)) {
|
|
150
|
-
throw new AppManifestError("central.app.json inválido.", [
|
|
151
|
-
{ path: "$", message: "deve conter um objeto JSON." },
|
|
152
|
-
]);
|
|
111
|
+
throw new AppManifestError("central.app.json inválido.", [{ path: "$", message: "deve conter um objeto JSON." }]);
|
|
153
112
|
}
|
|
154
|
-
|
|
155
113
|
const allowedTopLevel = new Set([
|
|
156
|
-
"$schema",
|
|
157
|
-
"
|
|
158
|
-
"id",
|
|
159
|
-
"name",
|
|
160
|
-
"slug",
|
|
161
|
-
"appKind",
|
|
162
|
-
"modules",
|
|
163
|
-
"capabilities",
|
|
164
|
-
"compatibility",
|
|
165
|
-
"activation",
|
|
114
|
+
"$schema", "schemaVersion", "id", "name", "slug", "appKind", "modules", "capabilities",
|
|
115
|
+
"compatibility", "activation", "rbac",
|
|
166
116
|
]);
|
|
167
117
|
for (const key of Object.keys(input)) {
|
|
168
118
|
if (!allowedTopLevel.has(key)) addIssue(issues, key, "propriedade não suportada.");
|
|
169
119
|
}
|
|
170
|
-
|
|
171
120
|
if (input.schemaVersion !== APP_MANIFEST_SCHEMA_VERSION) {
|
|
172
|
-
addIssue(
|
|
173
|
-
issues,
|
|
174
|
-
"schemaVersion",
|
|
175
|
-
`versão não suportada. Use ${APP_MANIFEST_SCHEMA_VERSION}.`,
|
|
176
|
-
);
|
|
121
|
+
addIssue(issues, "schemaVersion", `versão não suportada. Use ${APP_MANIFEST_SCHEMA_VERSION}.`);
|
|
177
122
|
}
|
|
178
123
|
if (typeof input.id !== "string" || !/^[a-z0-9][a-z0-9._-]*$/.test(input.id)) {
|
|
179
124
|
addIssue(issues, "id", "deve usar letras minúsculas, números, ponto, hífen ou sublinhado.");
|
|
180
125
|
}
|
|
181
|
-
if (typeof input.name !== "string" || !input.name.trim())
|
|
182
|
-
addIssue(issues, "name", "é obrigatório e deve ser texto não vazio.");
|
|
183
|
-
}
|
|
126
|
+
if (typeof input.name !== "string" || !input.name.trim()) addIssue(issues, "name", "é obrigatório e deve ser texto não vazio.");
|
|
184
127
|
if (typeof input.slug !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(input.slug)) {
|
|
185
128
|
addIssue(issues, "slug", "deve usar slug em letras minúsculas separado por hífen.");
|
|
186
129
|
}
|
|
187
130
|
if (!SUPPORTED_APP_KINDS.includes(input.appKind)) {
|
|
188
131
|
addIssue(issues, "appKind", `deve ser um de: ${SUPPORTED_APP_KINDS.join(", ")}.`);
|
|
189
132
|
}
|
|
190
|
-
|
|
191
133
|
if (!isPlainObject(input.modules)) {
|
|
192
134
|
addIssue(issues, "modules", "deve ser um objeto.");
|
|
193
135
|
} else {
|
|
194
136
|
for (const key of Object.keys(input.modules)) {
|
|
195
|
-
if (!SUPPORTED_MODULES.includes(key)) {
|
|
196
|
-
|
|
197
|
-
} else if (typeof input.modules[key] !== "boolean") {
|
|
198
|
-
addIssue(issues, `modules.${key}`, "deve ser booleano.");
|
|
199
|
-
}
|
|
137
|
+
if (!SUPPORTED_MODULES.includes(key)) addIssue(issues, `modules.${key}`, `módulo não suportado. Use: ${SUPPORTED_MODULES.join(", ")}.`);
|
|
138
|
+
else if (typeof input.modules[key] !== "boolean") addIssue(issues, `modules.${key}`, "deve ser booleano.");
|
|
200
139
|
}
|
|
201
140
|
}
|
|
202
|
-
|
|
203
141
|
if (!Array.isArray(input.capabilities)) {
|
|
204
142
|
addIssue(issues, "capabilities", "deve ser uma lista.");
|
|
205
143
|
} else {
|
|
206
144
|
const seen = new Set();
|
|
207
145
|
input.capabilities.forEach((capability, index) => {
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
)
|
|
212
|
-
addIssue(
|
|
213
|
-
issues,
|
|
214
|
-
`capabilities[${index}]`,
|
|
215
|
-
"deve ser um identificador namespaced, por exemplo core.collections.",
|
|
216
|
-
);
|
|
217
|
-
} else if (seen.has(capability)) {
|
|
218
|
-
addIssue(issues, `capabilities[${index}]`, `capability duplicada: ${capability}.`);
|
|
219
|
-
} else {
|
|
220
|
-
seen.add(capability);
|
|
221
|
-
}
|
|
146
|
+
if (typeof capability !== "string" || !/^[a-z][a-z0-9]*(?:[._:-][a-z0-9][a-z0-9-]*)+$/.test(capability)) {
|
|
147
|
+
addIssue(issues, `capabilities[${index}]`, "deve ser um identificador namespaced, por exemplo core.collections.");
|
|
148
|
+
} else if (seen.has(capability)) addIssue(issues, `capabilities[${index}]`, `capability duplicada: ${capability}.`);
|
|
149
|
+
else seen.add(capability);
|
|
222
150
|
});
|
|
223
151
|
}
|
|
224
|
-
|
|
225
152
|
validateCompatibility(input.compatibility, issues, coreVersion);
|
|
226
153
|
validateActivation(input.activation, issues);
|
|
227
|
-
|
|
154
|
+
validateRbac(input.rbac, issues);
|
|
228
155
|
if (issues.length) throw new AppManifestError("central.app.json inválido.", issues);
|
|
229
156
|
return JSON.parse(JSON.stringify(input));
|
|
230
157
|
}
|
|
@@ -233,9 +160,7 @@ function resolveAppManifestPath(cwd = process.cwd()) {
|
|
|
233
160
|
const absolute = path.resolve(cwd);
|
|
234
161
|
const candidates = [path.join(absolute, APP_MANIFEST_FILENAME)];
|
|
235
162
|
const parent = path.dirname(absolute);
|
|
236
|
-
if (["backend", "frontend"].includes(path.basename(absolute)))
|
|
237
|
-
candidates.push(path.join(parent, APP_MANIFEST_FILENAME));
|
|
238
|
-
}
|
|
163
|
+
if (["backend", "frontend"].includes(path.basename(absolute))) candidates.push(path.join(parent, APP_MANIFEST_FILENAME));
|
|
239
164
|
return candidates.find((candidate) => fs.existsSync(candidate)) || candidates[0];
|
|
240
165
|
}
|
|
241
166
|
|
|
@@ -254,6 +179,7 @@ function appManifestToConfig(manifest, manifestPath) {
|
|
|
254
179
|
},
|
|
255
180
|
modules: { ...manifest.modules },
|
|
256
181
|
capabilities: [...manifest.capabilities],
|
|
182
|
+
rbac: manifest.rbac ? JSON.parse(JSON.stringify(manifest.rbac)) : undefined,
|
|
257
183
|
appManifest: manifest,
|
|
258
184
|
appManifestPath: manifestPath,
|
|
259
185
|
};
|
|
@@ -266,17 +192,13 @@ function registerAppManifest(input, options = {}) {
|
|
|
266
192
|
}
|
|
267
193
|
|
|
268
194
|
function loadAppManifest(cwdOrFile = process.cwd(), options = {}) {
|
|
269
|
-
const manifestPath = cwdOrFile.endsWith?.(".json")
|
|
270
|
-
? path.resolve(cwdOrFile)
|
|
271
|
-
: resolveAppManifestPath(cwdOrFile);
|
|
195
|
+
const manifestPath = cwdOrFile.endsWith?.(".json") ? path.resolve(cwdOrFile) : resolveAppManifestPath(cwdOrFile);
|
|
272
196
|
let input;
|
|
273
197
|
try {
|
|
274
198
|
input = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
275
199
|
} catch (error) {
|
|
276
200
|
if (error.code === "ENOENT") throw error;
|
|
277
|
-
throw new AppManifestError("Não foi possível ler central.app.json.", [
|
|
278
|
-
{ path: "$", message: error.message },
|
|
279
|
-
], { cause: error });
|
|
201
|
+
throw new AppManifestError("Não foi possível ler central.app.json.", [{ path: "$", message: error.message }], { cause: error });
|
|
280
202
|
}
|
|
281
203
|
return registerAppManifest(input, { ...options, manifestPath });
|
|
282
204
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
CORE_PERMISSIONS,
|
|
5
|
+
isPermission,
|
|
6
|
+
isRoleCode,
|
|
7
|
+
} = require("../core/rbac/policy");
|
|
8
|
+
|
|
9
|
+
function isPlainObject(value) {
|
|
10
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function validateRbac(rbac, issues) {
|
|
14
|
+
if (rbac === undefined) return;
|
|
15
|
+
if (!isPlainObject(rbac)) {
|
|
16
|
+
issues.push({ path: "rbac", message: "deve ser um objeto." });
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const allowedKeys = new Set(["roles", "permissions", "invitationTtlHours"]);
|
|
21
|
+
for (const key of Object.keys(rbac)) {
|
|
22
|
+
if (!allowedKeys.has(key)) issues.push({ path: `rbac.${key}`, message: "propriedade não suportada." });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const customPermissions = Array.isArray(rbac.permissions) ? rbac.permissions : [];
|
|
26
|
+
if (rbac.permissions !== undefined && !Array.isArray(rbac.permissions)) {
|
|
27
|
+
issues.push({ path: "rbac.permissions", message: "deve ser uma lista." });
|
|
28
|
+
}
|
|
29
|
+
const seenPermissions = new Set();
|
|
30
|
+
customPermissions.forEach((permission, index) => {
|
|
31
|
+
if (!isPermission(permission) || permission === "*") {
|
|
32
|
+
issues.push({ path: `rbac.permissions[${index}]`, message: "deve usar o formato recurso.acao." });
|
|
33
|
+
} else if (seenPermissions.has(permission)) {
|
|
34
|
+
issues.push({ path: `rbac.permissions[${index}]`, message: `permissão duplicada: ${permission}.` });
|
|
35
|
+
}
|
|
36
|
+
seenPermissions.add(permission);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!Array.isArray(rbac.roles) || !rbac.roles.length) {
|
|
40
|
+
issues.push({ path: "rbac.roles", message: "deve conter ao menos um perfil." });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const catalog = new Set([...CORE_PERMISSIONS, ...customPermissions]);
|
|
45
|
+
const seenRoles = new Set();
|
|
46
|
+
let adminCount = 0;
|
|
47
|
+
rbac.roles.forEach((role, index) => {
|
|
48
|
+
const base = `rbac.roles[${index}]`;
|
|
49
|
+
if (!isPlainObject(role)) {
|
|
50
|
+
issues.push({ path: base, message: "deve ser um objeto." });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const allowedRoleKeys = new Set(["code", "name", "description", "permissions", "admin"]);
|
|
54
|
+
for (const key of Object.keys(role)) {
|
|
55
|
+
if (!allowedRoleKeys.has(key)) issues.push({ path: `${base}.${key}`, message: "propriedade não suportada." });
|
|
56
|
+
}
|
|
57
|
+
if (!isRoleCode(role.code)) issues.push({ path: `${base}.code`, message: "deve ser um código estável em minúsculas." });
|
|
58
|
+
if (seenRoles.has(role.code)) issues.push({ path: `${base}.code`, message: `perfil duplicado: ${role.code}.` });
|
|
59
|
+
seenRoles.add(role.code);
|
|
60
|
+
if (typeof role.name !== "string" || !role.name.trim()) issues.push({ path: `${base}.name`, message: "é obrigatório." });
|
|
61
|
+
if (role.admin !== undefined && typeof role.admin !== "boolean") issues.push({ path: `${base}.admin`, message: "deve ser booleano." });
|
|
62
|
+
if (!Array.isArray(role.permissions)) {
|
|
63
|
+
issues.push({ path: `${base}.permissions`, message: "deve ser uma lista." });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const seenRolePermissions = new Set();
|
|
67
|
+
role.permissions.forEach((permission, permissionIndex) => {
|
|
68
|
+
const path = `${base}.permissions[${permissionIndex}]`;
|
|
69
|
+
if (!isPermission(permission)) issues.push({ path, message: "permissão inválida." });
|
|
70
|
+
else if (permission !== "*" && !catalog.has(permission)) issues.push({ path, message: `permissão não declarada: ${permission}.` });
|
|
71
|
+
if (seenRolePermissions.has(permission)) issues.push({ path, message: `permissão duplicada: ${permission}.` });
|
|
72
|
+
seenRolePermissions.add(permission);
|
|
73
|
+
});
|
|
74
|
+
if (role.permissions.includes("*") && role.admin !== true) {
|
|
75
|
+
issues.push({ path: `${base}.admin`, message: "deve ser true quando o perfil usa wildcard (*)." });
|
|
76
|
+
}
|
|
77
|
+
if (role.admin === true || role.permissions.includes("*")) adminCount += 1;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (!adminCount) issues.push({ path: "rbac.roles", message: "deve existir ao menos um perfil administrativo." });
|
|
81
|
+
|
|
82
|
+
if (rbac.invitationTtlHours !== undefined) {
|
|
83
|
+
if (!Number.isInteger(rbac.invitationTtlHours) || rbac.invitationTtlHours < 1 || rbac.invitationTtlHours > 720) {
|
|
84
|
+
issues.push({ path: "rbac.invitationTtlHours", message: "deve ser inteiro entre 1 e 720 horas." });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { validateRbac };
|