@oondemand/oon-core-back 0.3.75 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oondemand/oon-core-back",
3
- "version": "0.3.75",
3
+ "version": "0.3.76",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/oondemand/oon-platform.git",
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Erro de domínio reconhecido pelo error middleware do Core.
3
- * Permite que services/triggers sinalizem statusCode e detalhes.
3
+ * Permite que services/triggers sinalizem statusCode, code e detalhes.
4
4
  */
5
5
  class GenericError extends Error {
6
- constructor(message, { statusCode = 400, details = null } = {}) {
6
+ constructor(message, { statusCode = 400, code = undefined, details = null } = {}) {
7
7
  super(message);
8
8
  this.name = "GenericError";
9
9
  this.statusCode = statusCode;
10
+ this.code = code;
10
11
  this.details = details;
11
12
  }
12
13
  }
@@ -1,5 +1,6 @@
1
1
  const Helpers = require("../utils/helpers");
2
2
  const registry = require("../registry");
3
+ const { scopeMutation } = require("../tenancy");
3
4
  const {
4
5
  prepareProcessMutation,
5
6
  assertAtomicInvariants,
@@ -38,13 +39,27 @@ function createCrudController(entry, service) {
38
39
  if (validate) await validate(data, context);
39
40
  };
40
41
 
41
- const prepareCreate = async (data, method = "post", session = null) => {
42
- const prepared = await prepareProcessMutation(entry, {}, data, {
42
+ const prepareCreate = async (
43
+ data,
44
+ method = "post",
45
+ session = null,
46
+ accessContext = undefined,
47
+ ) => {
48
+ const scopedData = scopeMutation(entry, data, accessContext);
49
+ const prepared = await prepareProcessMutation(entry, {}, scopedData, {
43
50
  op: "create",
44
51
  session,
52
+ accessContext,
45
53
  });
46
54
 
47
- await assertReferencePolicies(entry, prepared.consolidated, { session });
55
+ // Reaplica o escopo depois das regras para impedir que fórmula/hook de
56
+ // domínio substitua tenantId/usuarioId confiável. O controller usa o
57
+ // contrato público de tenancy e não exige helpers extras em services
58
+ // customizados/legados.
59
+ prepared.changes = scopeMutation(entry, prepared.changes, accessContext);
60
+ prepared.consolidated = scopeMutation(entry, prepared.consolidated, accessContext);
61
+
62
+ await assertReferencePolicies(entry, prepared.consolidated, { session, accessContext });
48
63
  await runCustomValidation(prepared.consolidated, {
49
64
  op: "create",
50
65
  method,
@@ -54,14 +69,24 @@ function createCrudController(entry, service) {
54
69
  consolidated: prepared.consolidated,
55
70
  changedFields: prepared.changedFields,
56
71
  session,
72
+ accessContext,
57
73
  });
58
74
  return prepared;
59
75
  };
60
76
 
61
- const prepareUpdate = async (id, changes, method, session = null) => {
77
+ const prepareUpdate = async (
78
+ id,
79
+ changes,
80
+ method,
81
+ session = null,
82
+ accessContext = undefined,
83
+ ) => {
84
+ const scopedChanges = scopeMutation(entry, changes, accessContext);
62
85
  const validate = registry.getValidation(entry.name);
63
86
  if (!validate && !hasDomainRules(entry) && !hasProcessRules(entry)) {
64
- const normalizedChanges = changes && typeof changes === "object" ? { ...changes } : {};
87
+ const normalizedChanges = scopedChanges && typeof scopedChanges === "object"
88
+ ? { ...scopedChanges }
89
+ : {};
65
90
  return {
66
91
  current: null,
67
92
  requestedChanges: normalizedChanges,
@@ -71,14 +96,17 @@ function createCrudController(entry, service) {
71
96
  };
72
97
  }
73
98
 
74
- const currentRecord = await service.getById(id, { session });
99
+ const currentRecord = await service.getById(id, { session, accessContext });
75
100
  const current = toPlainRecord(currentRecord);
76
- const prepared = await prepareProcessMutation(entry, current, changes, {
101
+ const prepared = await prepareProcessMutation(entry, current, scopedChanges, {
77
102
  op: "update",
78
103
  session,
104
+ accessContext,
79
105
  });
106
+ prepared.changes = scopeMutation(entry, prepared.changes, accessContext);
107
+ prepared.consolidated = scopeMutation(entry, prepared.consolidated, accessContext);
80
108
 
81
- await assertReferencePolicies(entry, prepared.consolidated, { session });
109
+ await assertReferencePolicies(entry, prepared.consolidated, { session, accessContext });
82
110
  if (validate) {
83
111
  await validate(prepared.consolidated, {
84
112
  op: "update",
@@ -90,30 +118,33 @@ function createCrudController(entry, service) {
90
118
  consolidated: prepared.consolidated,
91
119
  changedFields: prepared.changedFields,
92
120
  session,
121
+ accessContext,
93
122
  });
94
123
  }
95
124
 
96
125
  return { current, ...prepared };
97
126
  };
98
127
 
99
- async function createOne(data, method, session) {
100
- const prepared = await prepareCreate(data, method, session);
128
+ async function createOne(data, method, session, accessContext) {
129
+ const prepared = await prepareCreate(data, method, session, accessContext);
101
130
  await assertAtomicInvariants(entry, prepared.consolidated, null, session);
102
- const record = await service.create(prepared.changes, { session });
131
+ const record = await service.create(prepared.changes, { session, accessContext });
103
132
  await recalculateDependents(entry, null, toPlainRecord(record), {
104
133
  session,
105
134
  changedFields: prepared.changedFields,
135
+ accessContext,
106
136
  });
107
137
  return record;
108
138
  }
109
139
 
110
- async function updateOne(id, changes, method, session) {
111
- const prepared = await prepareUpdate(id, changes, method, session);
140
+ async function updateOne(id, changes, method, session, accessContext) {
141
+ const prepared = await prepareUpdate(id, changes, method, session, accessContext);
112
142
  await assertAtomicInvariants(entry, prepared.consolidated, prepared.current, session);
113
- const record = await service.update(id, prepared.changes, { session });
143
+ const record = await service.update(id, prepared.changes, { session, accessContext });
114
144
  await recalculateDependents(entry, prepared.current, toPlainRecord(record), {
115
145
  session,
116
146
  changedFields: prepared.changedFields,
147
+ accessContext,
117
148
  });
118
149
  return record;
119
150
  }
@@ -127,19 +158,27 @@ function createCrudController(entry, service) {
127
158
  searchTerm,
128
159
  sort,
129
160
  filtros,
161
+ accessContext: req.accessContext,
130
162
  });
131
163
  Helpers.sendPaginatedResponse({ res, statusCode: 200, results, pagination });
132
164
  },
133
165
 
134
166
  async getById(req, res) {
135
- const registro = await service.getById(req.params.id);
167
+ const registro = await service.getById(req.params.id, {
168
+ accessContext: req.accessContext,
169
+ });
136
170
  Helpers.sendResponse({ res, statusCode: 200, [entry.singular]: registro });
137
171
  },
138
172
 
139
173
  async create(req, res) {
140
174
  const registro = await withProcessTransaction(
141
175
  entry,
142
- (session) => createOne(req.body ?? {}, "post", session),
176
+ (session) => createOne(
177
+ req.body ?? {},
178
+ "post",
179
+ session,
180
+ req.accessContext,
181
+ ),
143
182
  );
144
183
  Helpers.sendResponse({ res, statusCode: 201, [entry.singular]: registro });
145
184
  },
@@ -147,7 +186,13 @@ function createCrudController(entry, service) {
147
186
  async update(req, res) {
148
187
  const registro = await withProcessTransaction(
149
188
  entry,
150
- (session) => updateOne(req.params.id, req.body ?? {}, "put", session),
189
+ (session) => updateOne(
190
+ req.params.id,
191
+ req.body ?? {},
192
+ "put",
193
+ session,
194
+ req.accessContext,
195
+ ),
151
196
  );
152
197
  Helpers.sendResponse({ res, statusCode: 200, [entry.singular]: registro });
153
198
  },
@@ -155,20 +200,36 @@ function createCrudController(entry, service) {
155
200
  async patch(req, res) {
156
201
  const registro = await withProcessTransaction(
157
202
  entry,
158
- (session) => updateOne(req.params.id, req.body ?? {}, "patch", session),
203
+ (session) => updateOne(
204
+ req.params.id,
205
+ req.body ?? {},
206
+ "patch",
207
+ session,
208
+ req.accessContext,
209
+ ),
159
210
  );
160
211
  Helpers.sendResponse({ res, statusCode: 200, [entry.singular]: registro });
161
212
  },
162
213
 
163
214
  async remove(req, res) {
164
215
  const registro = await withProcessTransaction(entry, async (session) => {
165
- const currentRecord = await service.getById(req.params.id, { session });
216
+ const currentRecord = await service.getById(req.params.id, {
217
+ session,
218
+ accessContext: req.accessContext,
219
+ });
166
220
  const current = toPlainRecord(currentRecord);
167
- await assertDeleteAllowed(entry, req.params.id, { session });
168
- const removed = await service.remove(req.params.id, { session });
221
+ await assertDeleteAllowed(entry, req.params.id, {
222
+ session,
223
+ accessContext: req.accessContext,
224
+ });
225
+ const removed = await service.remove(req.params.id, {
226
+ session,
227
+ accessContext: req.accessContext,
228
+ });
169
229
  await recalculateDependents(entry, current, null, {
170
230
  session,
171
231
  changedFields: Object.keys(current),
232
+ accessContext: req.accessContext,
172
233
  });
173
234
  return removed;
174
235
  });
@@ -178,16 +239,22 @@ function createCrudController(entry, service) {
178
239
  async importMany(req, res) {
179
240
  const rows = req.body?.rows || req.body;
180
241
  if (!Array.isArray(rows)) {
181
- const result = await service.importMany(rows);
242
+ const result = await service.importMany(rows, {
243
+ accessContext: req.accessContext,
244
+ });
182
245
  Helpers.sendResponse({ res, statusCode: 201, ...result });
183
246
  return;
184
247
  }
185
248
 
186
249
  if (!hasProcessRules(entry)) {
187
250
  const preparedRows = await Promise.all(
188
- rows.map(async (row) => (await prepareCreate(row, "import")).changes),
251
+ rows.map(async (row) => (
252
+ await prepareCreate(row, "import", null, req.accessContext)
253
+ ).changes),
189
254
  );
190
- const result = await service.importMany(preparedRows);
255
+ const result = await service.importMany(preparedRows, {
256
+ accessContext: req.accessContext,
257
+ });
191
258
  Helpers.sendResponse({ res, statusCode: 201, ...result });
192
259
  return;
193
260
  }
@@ -195,7 +262,7 @@ function createCrudController(entry, service) {
195
262
  let inserted = 0;
196
263
  for (const row of rows) {
197
264
  await withProcessTransaction(entry, async (session) => {
198
- await createOne(row, "import", session);
265
+ await createOne(row, "import", session, req.accessContext);
199
266
  inserted += 1;
200
267
  });
201
268
  }
@@ -203,7 +270,9 @@ function createCrudController(entry, service) {
203
270
  },
204
271
 
205
272
  async exportAll(req, res) {
206
- const results = await service.exportAll(req.query);
273
+ const results = await service.exportAll(req.query, {
274
+ accessContext: req.accessContext,
275
+ });
207
276
  Helpers.sendResponse({ res, statusCode: 200, results });
208
277
  },
209
278
  };
@@ -214,4 +283,4 @@ module.exports = {
214
283
  toPlainRecord,
215
284
  hasDomainRules,
216
285
  hasProcessRules,
217
- };
286
+ };
@@ -1,5 +1,12 @@
1
1
  const GenericError = require("../errors/GenericError");
2
2
  const registry = require("../registry");
3
+ const {
4
+ dataScope,
5
+ scopeFilter,
6
+ mergeScopedFilter,
7
+ scopeMutation,
8
+ scopedIdFilter,
9
+ } = require("../tenancy");
3
10
 
4
11
  const COMMON_REFERENCE_LABEL_FIELDS = [
5
12
  "nome",
@@ -15,7 +22,7 @@ const COMMON_REFERENCE_LABEL_FIELDS = [
15
22
  "sigla",
16
23
  ];
17
24
 
18
- function referencePopulateSpecs(entry) {
25
+ function referencePopulateSpecs(entry, accessContext) {
19
26
  const setting = entry.definition?.crud?.populateRefs;
20
27
  if (!setting) return [];
21
28
 
@@ -35,10 +42,15 @@ function referencePopulateSpecs(entry) {
35
42
  ...COMMON_REFERENCE_LABEL_FIELDS,
36
43
  ...(referencedEntry?.searchableFields || []),
37
44
  ];
45
+ let match;
46
+ if (referencedEntry && dataScope(referencedEntry) !== "legacy") {
47
+ match = scopeFilter(referencedEntry, accessContext);
48
+ }
38
49
 
39
50
  return {
40
51
  path: field.name,
41
52
  select: [...new Set(select)].join(" "),
53
+ ...(match && Object.keys(match).length ? { match } : {}),
42
54
  };
43
55
  });
44
56
  }
@@ -62,21 +74,47 @@ function withSession(query, session) {
62
74
  return session && query && typeof query.session === "function" ? query.session(session) : query;
63
75
  }
64
76
 
77
+ function scopeDenied() {
78
+ return new GenericError("Registro fora do contexto de acesso atual.", {
79
+ statusCode: 403,
80
+ code: "TENANT_SCOPE_DENIED",
81
+ });
82
+ }
83
+
84
+ function notFound() {
85
+ return new GenericError("Registro não encontrado.", { statusCode: 404 });
86
+ }
87
+
65
88
  function createCrudService(entry) {
66
89
  const { mongooseModel: Model, searchableFields } = entry;
67
- const populateSpecs = referencePopulateSpecs(entry);
68
90
 
69
- async function listPaginated({ pageIndex = 0, pageSize = 20, searchTerm, sort, filtros = {} } = {}) {
91
+ async function assertScopedMiss(id, accessContext, session) {
92
+ const scope = dataScope(entry);
93
+ if (scope === "legacy" || scope === "system") throw notFound();
94
+ const exists = await withSession(Model.exists({ _id: id }), session);
95
+ if (exists) throw scopeDenied();
96
+ throw notFound();
97
+ }
98
+
99
+ async function listPaginated({
100
+ pageIndex = 0,
101
+ pageSize = 20,
102
+ searchTerm,
103
+ sort,
104
+ filtros = {},
105
+ accessContext,
106
+ } = {}) {
70
107
  const page = Math.max(0, Number(pageIndex) || 0);
71
108
  const limit = Math.min(200, Math.max(1, Number(pageSize) || 20));
72
109
 
73
- const query = { ...cleanFilters(filtros) };
110
+ const query = mergeScopedFilter(entry, cleanFilters(filtros), accessContext);
74
111
  if (searchTerm && searchableFields.length) {
75
112
  const rx = new RegExp(escapeRegex(searchTerm), "i");
76
113
  query.$or = searchableFields.map((f) => ({ [f]: rx }));
77
114
  }
78
115
 
79
116
  const sortSpec = parseSort(sort);
117
+ const populateSpecs = referencePopulateSpecs(entry, accessContext);
80
118
  const listQuery = populateQuery(
81
119
  Model.find(query).sort(sortSpec).skip(page * limit).limit(limit),
82
120
  populateSpecs
@@ -98,32 +136,48 @@ function createCrudService(entry) {
98
136
  }
99
137
 
100
138
  async function getById(id, options = {}) {
101
- const query = withSession(Model.findById(id), options.session);
139
+ const populateSpecs = referencePopulateSpecs(entry, options.accessContext);
140
+ const query = withSession(
141
+ Model.findOne(scopedIdFilter(entry, id, options.accessContext)),
142
+ options.session,
143
+ );
102
144
  const doc = await resolvePopulated(query, populateSpecs);
103
- if (!doc) throw new GenericError("Registro não encontrado.", { statusCode: 404 });
145
+ if (!doc) await assertScopedMiss(id, options.accessContext, options.session);
104
146
  return doc;
105
147
  }
106
148
 
107
149
  async function create(data, options = {}) {
108
- const doc = new Model(data);
150
+ const scopedData = scopeMutation(entry, data, options.accessContext);
151
+ const doc = new Model(scopedData);
109
152
  await doc.save({ session: options.session || undefined });
110
- return resolvePopulated(doc, populateSpecs);
153
+ return resolvePopulated(doc, referencePopulateSpecs(entry, options.accessContext));
111
154
  }
112
155
 
113
156
  async function update(id, data, options = {}) {
114
- const query = Model.findByIdAndUpdate(id, data, {
115
- new: true,
116
- runValidators: true,
117
- session: options.session || undefined,
118
- });
119
- const doc = await resolvePopulated(query, populateSpecs);
120
- if (!doc) throw new GenericError("Registro não encontrado.", { statusCode: 404 });
157
+ const scopedData = scopeMutation(entry, data, options.accessContext);
158
+ const query = Model.findOneAndUpdate(
159
+ scopedIdFilter(entry, id, options.accessContext),
160
+ scopedData,
161
+ {
162
+ new: true,
163
+ runValidators: true,
164
+ session: options.session || undefined,
165
+ },
166
+ );
167
+ const doc = await resolvePopulated(
168
+ query,
169
+ referencePopulateSpecs(entry, options.accessContext),
170
+ );
171
+ if (!doc) await assertScopedMiss(id, options.accessContext, options.session);
121
172
  return doc;
122
173
  }
123
174
 
124
175
  async function remove(id, options = {}) {
125
- const doc = await withSession(Model.findByIdAndDelete(id), options.session);
126
- if (!doc) throw new GenericError("Registro não encontrado.", { statusCode: 404 });
176
+ const doc = await withSession(
177
+ Model.findOneAndDelete(scopedIdFilter(entry, id, options.accessContext)),
178
+ options.session,
179
+ );
180
+ if (!doc) await assertScopedMiss(id, options.accessContext, options.session);
127
181
  return doc;
128
182
  }
129
183
 
@@ -131,18 +185,38 @@ function createCrudService(entry) {
131
185
  if (!Array.isArray(rows)) {
132
186
  throw new GenericError("Esperado um array de registros.", { statusCode: 400 });
133
187
  }
134
- const docs = await Model.insertMany(rows, {
188
+ const scopedRows = rows.map((row) => scopeMutation(entry, row, options.accessContext));
189
+ const docs = await Model.insertMany(scopedRows, {
135
190
  ordered: false,
136
191
  session: options.session || undefined,
137
192
  });
138
193
  return { inserted: docs.length };
139
194
  }
140
195
 
141
- async function exportAll(filtros = {}) {
142
- return Model.find(cleanFilters(filtros)).lean();
196
+ async function exportAll(filtros = {}, options = {}) {
197
+ const query = mergeScopedFilter(entry, cleanFilters(filtros), options.accessContext);
198
+ return Model.find(query).lean();
199
+ }
200
+
201
+ function scopeQuery(filter = {}, accessContext) {
202
+ return mergeScopedFilter(entry, filter, accessContext);
203
+ }
204
+
205
+ function scopedData(data = {}, accessContext) {
206
+ return scopeMutation(entry, data, accessContext);
143
207
  }
144
208
 
145
- return { listPaginated, getById, create, update, remove, importMany, exportAll };
209
+ return {
210
+ listPaginated,
211
+ getById,
212
+ create,
213
+ update,
214
+ remove,
215
+ importMany,
216
+ exportAll,
217
+ scopeQuery,
218
+ scopedData,
219
+ };
146
220
  }
147
221
 
148
222
  function cleanFilters(filtros) {
@@ -163,4 +237,9 @@ function escapeRegex(str) {
163
237
  return String(str).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
164
238
  }
165
239
 
166
- module.exports = { createCrudService, referencePopulateSpecs, resolvePopulated };
240
+ module.exports = {
241
+ createCrudService,
242
+ referencePopulateSpecs,
243
+ resolvePopulated,
244
+ cleanFilters,
245
+ };
@@ -8,10 +8,16 @@ const {
8
8
  getTimeout,
9
9
  } = require("../utils/activationProvider");
10
10
  const { resolveUserAccess } = require("../rbac/storage");
11
+ const {
12
+ TENANT_HEADER,
13
+ readRequestedTenantId,
14
+ createAccessContext,
15
+ } = require("../tenancy");
11
16
 
12
17
  async function defaultVerifyToken(token, { req } = {}) {
13
18
  const baseUrl = getBaseUrl(req);
14
19
  const appCode = getAppCode();
20
+ const requestedTenantId = readRequestedTenantId(req);
15
21
 
16
22
  let response;
17
23
  try {
@@ -21,6 +27,7 @@ async function defaultVerifyToken(token, { req } = {}) {
21
27
  headers: {
22
28
  Authorization: `Bearer ${token}`,
23
29
  "x-app-code": appCode,
30
+ ...(requestedTenantId ? { [TENANT_HEADER]: requestedTenantId } : {}),
24
31
  },
25
32
  });
26
33
  } catch (error) {
@@ -28,6 +35,7 @@ async function defaultVerifyToken(token, { req } = {}) {
28
35
  error.response?.data?.message || error.message || "Token inválido ou erro na autenticação.",
29
36
  );
30
37
  err.statusCode = error.response?.status || error.statusCode || 401;
38
+ err.code = error.response?.data?.code || error.code;
31
39
  throw err;
32
40
  }
33
41
 
@@ -41,6 +49,12 @@ async function defaultVerifyToken(token, { req } = {}) {
41
49
  const tipoAcesso = data.aplicativo?.tipoAcesso || data.tipoAcesso || data.tipo || data.perfil;
42
50
  return {
43
51
  ...data,
52
+ acesso: response.data.acesso || data.acesso,
53
+ tenant: response.data.tenant || data.tenant,
54
+ tenancyModel:
55
+ response.data.acesso?.app?.tenancyModel ||
56
+ data.aplicativo?.tenancyModel ||
57
+ data.tenancyModel,
44
58
  tipo: tipoAcesso === "master" ? "admin" : tipoAcesso,
45
59
  perfil: data.perfil || tipoAcesso,
46
60
  nome: data.nome,
@@ -48,18 +62,25 @@ async function defaultVerifyToken(token, { req } = {}) {
48
62
  };
49
63
  }
50
64
 
51
- function localDevelopmentVerifyToken(token) {
65
+ function localDevelopmentVerifyToken(token, { req } = {}) {
52
66
  const expected = String(process.env.DEV_TOKEN || "");
53
67
  if (process.env.NODE_ENV !== "development" || !expected || token !== expected) {
54
68
  const error = new Error("Token local de desenvolvimento inválido.");
55
69
  error.statusCode = 401;
56
70
  throw error;
57
71
  }
72
+ const requestedTenantId = readRequestedTenantId(req);
58
73
  return {
74
+ id: process.env.DEV_USER_ID || "dev-local",
75
+ usuarioId: process.env.DEV_USER_ID || "dev-local",
59
76
  tipo: process.env.DEV_USER_ROLE || "admin",
60
77
  perfil: process.env.DEV_USER_ROLE || "admin",
78
+ roleCode: process.env.DEV_USER_ROLE || "admin",
61
79
  nome: process.env.DEV_USER_NAME || "Dev Local",
62
80
  email: process.env.DEV_USER_EMAIL || "dev@local",
81
+ tenantId: requestedTenantId || process.env.DEV_TENANT_ID,
82
+ appCode: process.env.APP_CODE,
83
+ tenancyModel: process.env.DEV_TENANCY_MODEL || ctx.config.tenancyModel || "single_tenant",
63
84
  capabilities: ctx.config.capabilities || [],
64
85
  };
65
86
  }
@@ -101,14 +122,28 @@ const authMiddleware = async (req, res, next) => {
101
122
  }
102
123
 
103
124
  try {
104
- const providerUser = await resolveVerifyToken()(token, { req });
125
+ const requestedTenantId = readRequestedTenantId(req);
126
+ const providerUser = await resolveVerifyToken()(token, { req, requestedTenantId });
105
127
  if (!providerUser) {
106
128
  const error = new Error("Usuário não resolvido.");
107
129
  error.statusCode = 401;
108
130
  throw error;
109
131
  }
110
132
  const usuario = await applyEffectiveAccess(providerUser);
111
- req.usuario = usuario;
133
+ const accessContext = createAccessContext(usuario, {
134
+ requestedTenantId,
135
+ tenancyModel: ctx.config.tenancyModel || ctx.config.app?.tenancyModel,
136
+ appCode: getAppCode(),
137
+ });
138
+
139
+ req.usuario = {
140
+ ...usuario,
141
+ tenantId: accessContext.tenantId,
142
+ appCode: accessContext.appCode || usuario.appCode,
143
+ };
144
+ // Contexto imutável e validado. Controllers e rotas customizadas devem
145
+ // consumir req.accessContext em vez de tenantId vindo de query/body.
146
+ req.accessContext = accessContext;
112
147
 
113
148
  if (usuario.mustChangePassword && !isFirstAccessRoute(req)) {
114
149
  return Helpers.sendErrorResponse({
@@ -0,0 +1,200 @@
1
+ const GenericError = require("./errors/GenericError");
2
+
3
+ const TENANT_HEADER = "x-oon-tenant-id";
4
+ const TENANCY_MODELS = Object.freeze(["none", "single_tenant", "multi_tenant"]);
5
+ const DATA_SCOPES = Object.freeze(["legacy", "system", "tenant", "user"]);
6
+
7
+ function stringId(value) {
8
+ if (value === undefined || value === null || value === "") return undefined;
9
+ if (typeof value === "object") {
10
+ if (value.id !== undefined) return stringId(value.id);
11
+ if (value._id !== undefined) return stringId(value._id);
12
+ }
13
+ return String(value);
14
+ }
15
+
16
+ function forbidden(code, message) {
17
+ return new GenericError(message, { statusCode: 403, code });
18
+ }
19
+
20
+ function readRequestedTenantId(req) {
21
+ return stringId(req?.headers?.[TENANT_HEADER]);
22
+ }
23
+
24
+ function inferVerifiedTenantId(usuario) {
25
+ return stringId(
26
+ usuario?.tenantId ||
27
+ usuario?.tenant?.id ||
28
+ usuario?.tenant?._id ||
29
+ usuario?.acesso?.tenant?.id ||
30
+ usuario?.acesso?.tenant?._id ||
31
+ usuario?.licenca?.tenantId ||
32
+ usuario?.aplicativo?.tenantId,
33
+ );
34
+ }
35
+
36
+ function inferTenancyModel(usuario, fallback = "single_tenant") {
37
+ const value =
38
+ usuario?.tenancyModel ||
39
+ usuario?.app?.tenancyModel ||
40
+ usuario?.aplicativo?.tenancyModel ||
41
+ usuario?.acesso?.app?.tenancyModel ||
42
+ fallback;
43
+ return TENANCY_MODELS.includes(value) ? value : fallback;
44
+ }
45
+
46
+ function inferPermissions(usuario) {
47
+ const raw = usuario?.permissions || usuario?.permissoes || [];
48
+ if (Array.isArray(raw)) return [...new Set(raw.map(String))];
49
+ return [];
50
+ }
51
+
52
+ function createAccessContext(usuario, options = {}) {
53
+ const requestedTenantId = stringId(options.requestedTenantId);
54
+ const verifiedTenantId = inferVerifiedTenantId(usuario);
55
+ const tenancyModel = inferTenancyModel(usuario, options.tenancyModel || "single_tenant");
56
+
57
+ // O header seleciona um tenant, mas nunca é autoridade. A autoridade é o
58
+ // tenant devolvido pelo provedor de identidade/Control Plane após validar o
59
+ // vínculo do usuário com o app e o tenant solicitado.
60
+ if (requestedTenantId && verifiedTenantId && requestedTenantId !== verifiedTenantId) {
61
+ throw forbidden(
62
+ "TENANT_CONTEXT_MISMATCH",
63
+ "O tenant solicitado não corresponde ao contexto autorizado.",
64
+ );
65
+ }
66
+
67
+ if (tenancyModel === "multi_tenant" && !verifiedTenantId) {
68
+ throw forbidden(
69
+ "TENANT_CONTEXT_REQUIRED",
70
+ "Selecione um tenant autorizado para acessar este aplicativo.",
71
+ );
72
+ }
73
+
74
+ if (tenancyModel === "none" && requestedTenantId) {
75
+ throw forbidden(
76
+ "TENANT_CONTEXT_NOT_ALLOWED",
77
+ "Este aplicativo não aceita contexto de tenant.",
78
+ );
79
+ }
80
+
81
+ const roleCode = String(
82
+ usuario?.roleCode || usuario?.perfil || usuario?.tipo || "",
83
+ ).trim() || undefined;
84
+
85
+ return Object.freeze({
86
+ userId: stringId(usuario?.usuarioId || usuario?.id || usuario?._id || usuario?.sub),
87
+ tenantId: tenancyModel === "none" ? undefined : verifiedTenantId,
88
+ appCode: String(
89
+ usuario?.appCode ||
90
+ usuario?.app?.codigo ||
91
+ usuario?.aplicativo?.codigo ||
92
+ usuario?.acesso?.app?.codigo ||
93
+ options.appCode ||
94
+ "",
95
+ ).trim().toLowerCase() || undefined,
96
+ roleCode,
97
+ permissions: Object.freeze(inferPermissions(usuario)),
98
+ tenancyModel,
99
+ });
100
+ }
101
+
102
+ function dataScope(entryOrDefinition) {
103
+ const definition = entryOrDefinition?.definition || entryOrDefinition || {};
104
+ const value = definition.scope || definition.tenancy?.scope || "legacy";
105
+ return DATA_SCOPES.includes(value) ? value : "legacy";
106
+ }
107
+
108
+ function tenantField(entryOrDefinition) {
109
+ const definition = entryOrDefinition?.definition || entryOrDefinition || {};
110
+ return definition.tenancy?.tenantField || "tenantId";
111
+ }
112
+
113
+ function userField(entryOrDefinition) {
114
+ const definition = entryOrDefinition?.definition || entryOrDefinition || {};
115
+ return definition.tenancy?.userField || "usuarioId";
116
+ }
117
+
118
+ function requireContextValue(value, code, message) {
119
+ if (!value) throw forbidden(code, message);
120
+ return value;
121
+ }
122
+
123
+ function scopeFilter(entryOrDefinition, accessContext) {
124
+ const scope = dataScope(entryOrDefinition);
125
+ if (scope === "legacy" || scope === "system") return {};
126
+
127
+ const tenantId = requireContextValue(
128
+ accessContext?.tenantId,
129
+ "TENANT_CONTEXT_REQUIRED",
130
+ "Contexto de tenant obrigatório para este recurso.",
131
+ );
132
+ const filter = { [tenantField(entryOrDefinition)]: tenantId };
133
+
134
+ if (scope === "user") {
135
+ filter[userField(entryOrDefinition)] = requireContextValue(
136
+ accessContext?.userId,
137
+ "USER_CONTEXT_REQUIRED",
138
+ "Contexto de usuário obrigatório para este recurso.",
139
+ );
140
+ }
141
+
142
+ return filter;
143
+ }
144
+
145
+ function mergeScopedFilter(entryOrDefinition, filter = {}, accessContext) {
146
+ const clean = { ...(filter || {}) };
147
+ const enforced = scopeFilter(entryOrDefinition, accessContext);
148
+
149
+ // Remove tentativas de substituir escopo via query/body. O valor confiável
150
+ // sempre vence e é derivado de req.accessContext.
151
+ for (const [key, value] of Object.entries(enforced)) {
152
+ clean[key] = value;
153
+ }
154
+ return clean;
155
+ }
156
+
157
+ function scopeMutation(entryOrDefinition, data = {}, accessContext) {
158
+ const result = data && typeof data === "object" && !Array.isArray(data)
159
+ ? { ...data }
160
+ : data;
161
+ if (!result || typeof result !== "object" || Array.isArray(result)) return result;
162
+
163
+ const scope = dataScope(entryOrDefinition);
164
+ if (scope === "legacy" || scope === "system") return result;
165
+
166
+ const enforced = scopeFilter(entryOrDefinition, accessContext);
167
+ Object.assign(result, enforced);
168
+ return result;
169
+ }
170
+
171
+ function scopedIdFilter(entryOrDefinition, id, accessContext) {
172
+ return mergeScopedFilter(entryOrDefinition, { _id: id }, accessContext);
173
+ }
174
+
175
+ function assertScopedEntry(entryOrDefinition) {
176
+ const scope = dataScope(entryOrDefinition);
177
+ if (scope === "legacy") return;
178
+ if (!DATA_SCOPES.includes(scope)) {
179
+ throw new Error(`Escopo de dados inválido: ${scope}`);
180
+ }
181
+ }
182
+
183
+ module.exports = {
184
+ TENANT_HEADER,
185
+ TENANCY_MODELS,
186
+ DATA_SCOPES,
187
+ stringId,
188
+ readRequestedTenantId,
189
+ inferVerifiedTenantId,
190
+ inferTenancyModel,
191
+ createAccessContext,
192
+ dataScope,
193
+ tenantField,
194
+ userField,
195
+ scopeFilter,
196
+ mergeScopedFilter,
197
+ scopeMutation,
198
+ scopedIdFilter,
199
+ assertScopedEntry,
200
+ };
package/src/index.js CHANGED
@@ -6,6 +6,7 @@ const { activate } = require("./boot/activation");
6
6
  const define = require("./core/define");
7
7
  const fields = require("./core/fields");
8
8
  const registry = require("./core/registry");
9
+ const tenancy = require("./core/tenancy");
9
10
  const GenericError = require("./core/errors/GenericError");
10
11
  const { requirePermission } = require("./core/middlewares/permissions");
11
12
  const rbacPolicy = require("./core/rbac/policy");
@@ -38,6 +39,16 @@ module.exports = {
38
39
  roleByCode: rbacPolicy.roleByCode,
39
40
  permissionGranted: rbacPolicy.granted,
40
41
 
42
+ TENANT_HEADER: tenancy.TENANT_HEADER,
43
+ TENANCY_MODELS: tenancy.TENANCY_MODELS,
44
+ DATA_SCOPES: tenancy.DATA_SCOPES,
45
+ createAccessContext: tenancy.createAccessContext,
46
+ readRequestedTenantId: tenancy.readRequestedTenantId,
47
+ scopeFilter: tenancy.scopeFilter,
48
+ mergeScopedFilter: tenancy.mergeScopedFilter,
49
+ scopeMutation: tenancy.scopeMutation,
50
+ scopedIdFilter: tenancy.scopedIdFilter,
51
+
41
52
  APP_MANIFEST_FILENAME: appManifest.APP_MANIFEST_FILENAME,
42
53
  APP_MANIFEST_SCHEMA_VERSION: appManifest.APP_MANIFEST_SCHEMA_VERSION,
43
54
  SUPPORTED_APP_KINDS: appManifest.SUPPORTED_APP_KINDS,