@oondemand/oon-core-back 0.4.20 → 0.4.22
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/auth.routes.js +1 -0
- package/src/core/define.js +3 -1
- package/src/core/middlewares/auth.js +4 -1
- package/src/core/rbac/storage.js +14 -0
- package/src/core/tenancy.js +35 -0
- package/src/core/utils/instanceActivation.js +16 -2
- package/src/manifest/appManifest.js +18 -1
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,
|
|
@@ -398,11 +398,13 @@ function decrypt(instance) {
|
|
|
398
398
|
|
|
399
399
|
function instanceHeaders(instance) {
|
|
400
400
|
if (instance.deploymentId) {
|
|
401
|
+
const tenantAware =
|
|
402
|
+
(ctx.config.tenancyModel || ctx.config.ecosystem?.tenancyModel) !== "none";
|
|
401
403
|
return {
|
|
402
404
|
"x-oon-deployment-id": instance.deploymentId,
|
|
403
405
|
"x-oon-deployment-token": decrypt(instance),
|
|
404
406
|
"x-app-code": getAppCode(),
|
|
405
|
-
...(instance.tenantId ? { "x-oon-tenant-id": instance.tenantId } : {}),
|
|
407
|
+
...(tenantAware && instance.tenantId ? { "x-oon-tenant-id": instance.tenantId } : {}),
|
|
406
408
|
};
|
|
407
409
|
}
|
|
408
410
|
return {
|
|
@@ -426,11 +428,23 @@ async function operationalRequestHeaders(options = {}, dependencies = {}) {
|
|
|
426
428
|
code: "OPERATIONAL_CREDENTIAL_REQUIRED",
|
|
427
429
|
});
|
|
428
430
|
}
|
|
429
|
-
|
|
431
|
+
const tenancyModel = ctx.config.tenancyModel || ctx.config.ecosystem?.tenancyModel;
|
|
432
|
+
if (tenancyModel === "none" && options.tenantId) {
|
|
433
|
+
throw new GenericError("Apps globais devem usar targetTenantId, não tenantId.", {
|
|
434
|
+
statusCode: 400,
|
|
435
|
+
code: "TENANT_CONTEXT_NOT_ALLOWED",
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
const headers = {
|
|
430
439
|
...buildHeaders(instance),
|
|
431
440
|
...(options.tenantId ? { "x-oon-tenant-id": String(options.tenantId) } : {}),
|
|
441
|
+
...(options.targetTenantId
|
|
442
|
+
? { "x-oon-target-tenant-id": String(options.targetTenantId) }
|
|
443
|
+
: {}),
|
|
432
444
|
...(options.correlationId ? { "x-correlation-id": String(options.correlationId) } : {}),
|
|
433
445
|
};
|
|
446
|
+
if (tenancyModel === "none") delete headers["x-oon-tenant-id"];
|
|
447
|
+
return headers;
|
|
434
448
|
}
|
|
435
449
|
|
|
436
450
|
function validateUrl(value) {
|
|
@@ -14,7 +14,7 @@ const SUPPORTED_MODULES = Object.freeze([
|
|
|
14
14
|
]);
|
|
15
15
|
const SUPPORTED_TENANCY_MODELS = Object.freeze(["none", "single_tenant", "multi_tenant"]);
|
|
16
16
|
const SUPPORTED_AVAILABILITIES = Object.freeze(["system", "common", "licensed"]);
|
|
17
|
-
const SUPPORTED_ACCESS_MODES = Object.freeze(["tenant_members", "explicit_users"]);
|
|
17
|
+
const SUPPORTED_ACCESS_MODES = Object.freeze(["tenant_members", "explicit_users", "authenticated_users"]);
|
|
18
18
|
const SUPPORTED_AUTHORIZATION_MODES = Object.freeze(["none", "rbac"]);
|
|
19
19
|
// Compatibilidade de leitura para manifestos anteriores à separação das duas dimensões.
|
|
20
20
|
const SUPPORTED_ACCESS_POLICIES = Object.freeze(["tenant_members", "explicit_users", "rbac"]);
|
|
@@ -264,6 +264,23 @@ function validateAppManifest(input, { coreVersion = corePackage.version } = {})
|
|
|
264
264
|
if (dimensions.tenancyModel === "none" && dimensions.accessMode === "tenant_members") {
|
|
265
265
|
addIssue(issues, "accessMode", "tenant_members exige uma aplicação tenant-aware.");
|
|
266
266
|
}
|
|
267
|
+
if (dimensions.accessMode === "authenticated_users") {
|
|
268
|
+
if (dimensions.tenancyModel !== "none") {
|
|
269
|
+
addIssue(issues, "tenancyModel", "authenticated_users exige tenancyModel=none.");
|
|
270
|
+
}
|
|
271
|
+
if (dimensions.availability !== "common") {
|
|
272
|
+
addIssue(issues, "availability", "authenticated_users exige availability=common.");
|
|
273
|
+
}
|
|
274
|
+
if (dimensions.authorizationMode !== "rbac") {
|
|
275
|
+
addIssue(issues, "authorizationMode", "authenticated_users exige authorizationMode=rbac.");
|
|
276
|
+
}
|
|
277
|
+
if (dimensions.deploymentMode !== "singleton") {
|
|
278
|
+
addIssue(issues, "deploymentMode", "authenticated_users exige deploymentMode=singleton.");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (dimensions.tenancyModel === "single_tenant" && dimensions.deploymentMode === "shared") {
|
|
282
|
+
addIssue(issues, "deploymentMode", "single_tenant não pode usar deploymentMode=shared; use dedicated.");
|
|
283
|
+
}
|
|
267
284
|
if (!isPlainObject(input.modules)) {
|
|
268
285
|
addIssue(issues, "modules", "deve ser um objeto.");
|
|
269
286
|
} else {
|