@oondemand/oon-core-back 0.4.21 → 0.4.23
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
CHANGED
package/src/api/auth.routes.js
CHANGED
|
@@ -60,6 +60,7 @@ function configuredSessionScope() {
|
|
|
60
60
|
function tenantForLogin(req, instance) {
|
|
61
61
|
const tenancyModel =
|
|
62
62
|
ctx.config.tenancyModel || ctx.config.ecosystem?.tenancyModel;
|
|
63
|
+
if (tenancyModel === "none") return undefined;
|
|
63
64
|
if (tenancyModel === "multi_tenant") {
|
|
64
65
|
// Uma instância compartilhada não pertence ao tenant que a ativou
|
|
65
66
|
// originalmente. Sem seleção explícita, a Central de Ativações resolve o
|
package/src/core/define.js
CHANGED
|
@@ -47,8 +47,10 @@ function addDataScopeFields(schema, definition) {
|
|
|
47
47
|
if (!["tenant", "user"].includes(scope)) return schema;
|
|
48
48
|
|
|
49
49
|
const fields = {};
|
|
50
|
+
const tenantScoped = scope === "tenant"
|
|
51
|
+
|| (scope === "user" && (ctx.config.tenancyModel || ctx.config.ecosystem?.tenancyModel) !== "none");
|
|
50
52
|
const scopedTenantField = tenantField(definition);
|
|
51
|
-
if (!schema.path(scopedTenantField)) {
|
|
53
|
+
if (tenantScoped && !schema.path(scopedTenantField)) {
|
|
52
54
|
fields[scopedTenantField] = {
|
|
53
55
|
type: String,
|
|
54
56
|
required: true,
|
|
@@ -17,7 +17,10 @@ const {
|
|
|
17
17
|
async function defaultVerifyToken(token, { req } = {}) {
|
|
18
18
|
const baseUrl = getBaseUrl(req);
|
|
19
19
|
const appCode = getAppCode();
|
|
20
|
-
const requestedTenantId =
|
|
20
|
+
const requestedTenantId =
|
|
21
|
+
(ctx.config.tenancyModel || ctx.config.ecosystem?.tenancyModel) === "none"
|
|
22
|
+
? undefined
|
|
23
|
+
: readRequestedTenantId(req);
|
|
21
24
|
|
|
22
25
|
let response;
|
|
23
26
|
try {
|
package/src/core/rbac/storage.js
CHANGED
|
@@ -178,6 +178,20 @@ async function resolveUserAccess(user, { touch = true } = {}) {
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
let access = await findAccessForUser(user);
|
|
181
|
+
const authenticatedAdmission =
|
|
182
|
+
(ctx.config.accessMode || ctx.config.ecosystem?.accessMode) === "authenticated_users";
|
|
183
|
+
if (!access && authenticatedAdmission) {
|
|
184
|
+
const globalPermissions = Array.isArray(user?.globalPermissions)
|
|
185
|
+
? [...new Set(user.globalPermissions.map(String))]
|
|
186
|
+
: [];
|
|
187
|
+
return {
|
|
188
|
+
mode: "authenticated_users",
|
|
189
|
+
status: "active",
|
|
190
|
+
roleCode: "authenticated_user",
|
|
191
|
+
permissions: globalPermissions,
|
|
192
|
+
mustChangePassword: Boolean(user?.mustChangePassword || user?.firstAccessPending),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
181
195
|
if (!access) access = await migrateLegacyAccess(user);
|
|
182
196
|
if (!access) {
|
|
183
197
|
throw accessError("Usuário autenticado sem perfil local nesta Central.", "RBAC_LOCAL_ACCESS_REQUIRED");
|
package/src/core/tenancy.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const GenericError = require("./errors/GenericError");
|
|
2
2
|
|
|
3
3
|
const TENANT_HEADER = "x-oon-tenant-id";
|
|
4
|
+
const TARGET_TENANT_HEADER = "x-oon-target-tenant-id";
|
|
4
5
|
const TENANCY_MODELS = Object.freeze(["none", "single_tenant", "multi_tenant"]);
|
|
5
6
|
const DATA_SCOPES = Object.freeze(["legacy", "system", "tenant", "user"]);
|
|
6
7
|
|
|
@@ -21,6 +22,21 @@ function readRequestedTenantId(req) {
|
|
|
21
22
|
return stringId(req?.headers?.[TENANT_HEADER]);
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
function readTargetTenantId(req) {
|
|
26
|
+
return stringId(req?.headers?.[TARGET_TENANT_HEADER]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function requireTargetTenantId(req) {
|
|
30
|
+
const targetTenantId = readTargetTenantId(req);
|
|
31
|
+
if (!targetTenantId) {
|
|
32
|
+
throw new GenericError("Informe o tenant-alvo da operação.", {
|
|
33
|
+
statusCode: 400,
|
|
34
|
+
code: "TARGET_TENANT_REQUIRED",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return targetTenantId;
|
|
38
|
+
}
|
|
39
|
+
|
|
24
40
|
function inferVerifiedTenantId(usuario) {
|
|
25
41
|
return stringId(
|
|
26
42
|
usuario?.tenantId ||
|
|
@@ -124,6 +140,16 @@ function scopeFilter(entryOrDefinition, accessContext) {
|
|
|
124
140
|
const scope = dataScope(entryOrDefinition);
|
|
125
141
|
if (scope === "legacy" || scope === "system") return {};
|
|
126
142
|
|
|
143
|
+
if (scope === "user" && accessContext?.tenancyModel === "none") {
|
|
144
|
+
return {
|
|
145
|
+
[userField(entryOrDefinition)]: requireContextValue(
|
|
146
|
+
accessContext?.userId,
|
|
147
|
+
"USER_CONTEXT_REQUIRED",
|
|
148
|
+
"Contexto de usuário obrigatório para este recurso.",
|
|
149
|
+
),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
127
153
|
const tenantId = requireContextValue(
|
|
128
154
|
accessContext?.tenantId,
|
|
129
155
|
"TENANT_CONTEXT_REQUIRED",
|
|
@@ -144,6 +170,9 @@ function scopeFilter(entryOrDefinition, accessContext) {
|
|
|
144
170
|
|
|
145
171
|
function mergeScopedFilter(entryOrDefinition, filter = {}, accessContext) {
|
|
146
172
|
const clean = { ...(filter || {}) };
|
|
173
|
+
if (dataScope(entryOrDefinition) === "user" && accessContext?.tenancyModel === "none") {
|
|
174
|
+
delete clean[tenantField(entryOrDefinition)];
|
|
175
|
+
}
|
|
147
176
|
const enforced = scopeFilter(entryOrDefinition, accessContext);
|
|
148
177
|
|
|
149
178
|
// Remove tentativas de substituir escopo via query/body. O valor confiável
|
|
@@ -162,6 +191,9 @@ function scopeMutation(entryOrDefinition, data = {}, accessContext) {
|
|
|
162
191
|
|
|
163
192
|
const scope = dataScope(entryOrDefinition);
|
|
164
193
|
if (scope === "legacy" || scope === "system") return result;
|
|
194
|
+
if (scope === "user" && accessContext?.tenancyModel === "none") {
|
|
195
|
+
delete result[tenantField(entryOrDefinition)];
|
|
196
|
+
}
|
|
165
197
|
|
|
166
198
|
const enforced = scopeFilter(entryOrDefinition, accessContext);
|
|
167
199
|
Object.assign(result, enforced);
|
|
@@ -182,10 +214,13 @@ function assertScopedEntry(entryOrDefinition) {
|
|
|
182
214
|
|
|
183
215
|
module.exports = {
|
|
184
216
|
TENANT_HEADER,
|
|
217
|
+
TARGET_TENANT_HEADER,
|
|
185
218
|
TENANCY_MODELS,
|
|
186
219
|
DATA_SCOPES,
|
|
187
220
|
stringId,
|
|
188
221
|
readRequestedTenantId,
|
|
222
|
+
readTargetTenantId,
|
|
223
|
+
requireTargetTenantId,
|
|
189
224
|
inferVerifiedTenantId,
|
|
190
225
|
inferTenancyModel,
|
|
191
226
|
createAccessContext,
|
|
@@ -114,6 +114,27 @@ function role() {
|
|
|
114
114
|
return ctx.config.ecosystem?.role === "root" ? "root" : "member";
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
function deploymentBootstrap() {
|
|
118
|
+
const deploymentId = String(process.env.OON_DEPLOYMENT_ID || "").trim();
|
|
119
|
+
const token = String(process.env.OON_DEPLOYMENT_TOKEN || "").trim();
|
|
120
|
+
if (!deploymentId && !token) return null;
|
|
121
|
+
if (!deploymentId || !token) {
|
|
122
|
+
throw new GenericError(
|
|
123
|
+
"OON_DEPLOYMENT_ID e OON_DEPLOYMENT_TOKEN devem ser configurados juntos.",
|
|
124
|
+
{ statusCode: 500, code: "DEPLOYMENT_BOOTSTRAP_INCOMPLETE" },
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
const tenancyModel =
|
|
128
|
+
ctx.config.tenancyModel || ctx.config.ecosystem?.tenancyModel;
|
|
129
|
+
if (tenancyModel !== "none") {
|
|
130
|
+
throw new GenericError(
|
|
131
|
+
"Bootstrap direto de deployment é permitido somente para Apps globais.",
|
|
132
|
+
{ statusCode: 500, code: "DEPLOYMENT_BOOTSTRAP_TENANCY_INVALID" },
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return { deploymentId, token };
|
|
136
|
+
}
|
|
137
|
+
|
|
117
138
|
function ambiente() {
|
|
118
139
|
const value =
|
|
119
140
|
process.env.APP_ENVIRONMENT || process.env.NODE_ENV || "desenvolvimento";
|
|
@@ -264,6 +285,38 @@ async function ensureInstance() {
|
|
|
264
285
|
await instance.save();
|
|
265
286
|
}
|
|
266
287
|
|
|
288
|
+
const bootstrap = deploymentBootstrap();
|
|
289
|
+
if (bootstrap) {
|
|
290
|
+
let currentToken = null;
|
|
291
|
+
try {
|
|
292
|
+
currentToken = decrypt(instance);
|
|
293
|
+
} catch {
|
|
294
|
+
currentToken = null;
|
|
295
|
+
}
|
|
296
|
+
if (
|
|
297
|
+
instance.deploymentId !== bootstrap.deploymentId ||
|
|
298
|
+
instance.credentialType !== "deployment" ||
|
|
299
|
+
currentToken !== bootstrap.token ||
|
|
300
|
+
instance.status !== "ativa"
|
|
301
|
+
) {
|
|
302
|
+
clearInstanceIdentity(instance);
|
|
303
|
+
instance.deploymentId = bootstrap.deploymentId;
|
|
304
|
+
instance.credentialType = "deployment";
|
|
305
|
+
instance.status = "ativa";
|
|
306
|
+
instance.appCode = getAppCode();
|
|
307
|
+
instance.ambiente = ambiente();
|
|
308
|
+
instance.ecosystemUrl = process.env.CENTRAL_ATIVACAO_URL;
|
|
309
|
+
instance.publicUrl = process.env.PUBLIC_APP_URL;
|
|
310
|
+
instance.coreVersion = version();
|
|
311
|
+
instance.appVersion = version();
|
|
312
|
+
instance.ativadaEm = new Date();
|
|
313
|
+
instance.concluidaEm = new Date();
|
|
314
|
+
instance.credentialVersion = Number(instance.credentialVersion || 0) + 1;
|
|
315
|
+
Object.assign(instance, encrypt(bootstrap.token));
|
|
316
|
+
await instance.save();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
267
320
|
// Versões anteriores do Core marcavam a aplicação root como ativa sem criar
|
|
268
321
|
// o App root e o grafo mínimo do ecossistema. Quando o domínio disponibiliza
|
|
269
322
|
// o verificador, o App cadastrado passa a ser a fonte de verdade.
|
|
@@ -398,11 +451,13 @@ function decrypt(instance) {
|
|
|
398
451
|
|
|
399
452
|
function instanceHeaders(instance) {
|
|
400
453
|
if (instance.deploymentId) {
|
|
454
|
+
const tenantAware =
|
|
455
|
+
(ctx.config.tenancyModel || ctx.config.ecosystem?.tenancyModel) !== "none";
|
|
401
456
|
return {
|
|
402
457
|
"x-oon-deployment-id": instance.deploymentId,
|
|
403
458
|
"x-oon-deployment-token": decrypt(instance),
|
|
404
459
|
"x-app-code": getAppCode(),
|
|
405
|
-
...(instance.tenantId ? { "x-oon-tenant-id": instance.tenantId } : {}),
|
|
460
|
+
...(tenantAware && instance.tenantId ? { "x-oon-tenant-id": instance.tenantId } : {}),
|
|
406
461
|
};
|
|
407
462
|
}
|
|
408
463
|
return {
|
|
@@ -426,11 +481,23 @@ async function operationalRequestHeaders(options = {}, dependencies = {}) {
|
|
|
426
481
|
code: "OPERATIONAL_CREDENTIAL_REQUIRED",
|
|
427
482
|
});
|
|
428
483
|
}
|
|
429
|
-
|
|
484
|
+
const tenancyModel = ctx.config.tenancyModel || ctx.config.ecosystem?.tenancyModel;
|
|
485
|
+
if (tenancyModel === "none" && options.tenantId) {
|
|
486
|
+
throw new GenericError("Apps globais devem usar targetTenantId, não tenantId.", {
|
|
487
|
+
statusCode: 400,
|
|
488
|
+
code: "TENANT_CONTEXT_NOT_ALLOWED",
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
const headers = {
|
|
430
492
|
...buildHeaders(instance),
|
|
431
493
|
...(options.tenantId ? { "x-oon-tenant-id": String(options.tenantId) } : {}),
|
|
494
|
+
...(options.targetTenantId
|
|
495
|
+
? { "x-oon-target-tenant-id": String(options.targetTenantId) }
|
|
496
|
+
: {}),
|
|
432
497
|
...(options.correlationId ? { "x-correlation-id": String(options.correlationId) } : {}),
|
|
433
498
|
};
|
|
499
|
+
if (tenancyModel === "none") delete headers["x-oon-tenant-id"];
|
|
500
|
+
return headers;
|
|
434
501
|
}
|
|
435
502
|
|
|
436
503
|
function validateUrl(value) {
|
|
@@ -516,6 +583,7 @@ module.exports = {
|
|
|
516
583
|
globalFields,
|
|
517
584
|
rootFields,
|
|
518
585
|
role,
|
|
586
|
+
deploymentBootstrap,
|
|
519
587
|
ambiente,
|
|
520
588
|
publicUrl,
|
|
521
589
|
version,
|