@friggframework/core 2.0.0--canary.684.0da024b.0 → 2.0.0--canary.679.a2bcc2f.0
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/CLAUDE.md +0 -2
- package/README.md +0 -105
- package/application/commands/integration-commands.js +2 -2
- package/application/commands/user-commands.js +5 -17
- package/credential/use-cases/credential-operations-for-user.js +25 -110
- package/credential/use-cases/reauthorize-credential-for-user.js +8 -15
- package/database/documentdb-utils.js +1 -6
- package/generated/prisma-mongodb/edge.js +4 -15
- package/generated/prisma-mongodb/index-browser.js +1 -12
- package/generated/prisma-mongodb/index.d.ts +86 -2027
- package/generated/prisma-mongodb/index.js +4 -15
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +0 -47
- package/generated/prisma-mongodb/wasm.js +4 -15
- package/generated/prisma-postgresql/edge.js +4 -15
- package/generated/prisma-postgresql/index-browser.js +1 -12
- package/generated/prisma-postgresql/index.d.ts +63 -2162
- package/generated/prisma-postgresql/index.js +4 -15
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +0 -44
- package/generated/prisma-postgresql/wasm.js +4 -15
- package/handlers/app-definition-loader.js +1 -15
- package/index.js +0 -18
- package/integrations/integration-router.js +73 -115
- package/integrations/repositories/integration-repository-documentdb.js +3 -35
- package/integrations/repositories/integration-repository-interface.js +1 -17
- package/integrations/repositories/integration-repository-mongo.js +76 -59
- package/integrations/repositories/integration-repository-postgres.js +76 -62
- package/integrations/tests/doubles/test-integration-repository.js +1 -12
- package/integrations/use-cases/create-integration.js +20 -55
- package/integrations/use-cases/get-integrations-for-user.js +40 -80
- package/integrations/use-cases/index.js +1 -5
- package/integrations/use-cases/integration-operations-for-user.js +24 -55
- package/management-api/management-api-config.js +0 -3
- package/management-api/openapi/components.js +0 -40
- package/management-api/openapi/v2-operations.js +1 -13
- package/management-api/v2/dependencies.js +15 -40
- package/management-api/v2/dto.js +0 -20
- package/management-api/v2/handlers/authorization.js +0 -1
- package/management-api/v2/handlers/credentials.js +2 -6
- package/management-api/v2/handlers/entities.js +7 -10
- package/management-api/v2/handlers/integrations.js +3 -6
- package/management-api/v2/request.js +1 -32
- package/modules/index.js +0 -8
- package/modules/repositories/module-repository-documentdb.js +4 -48
- package/modules/repositories/module-repository-interface.js +2 -19
- package/modules/repositories/module-repository-mongo.js +73 -65
- package/modules/repositories/module-repository-postgres.js +73 -64
- package/modules/use-cases/entity-operations-for-user.js +13 -50
- package/modules/use-cases/entity-types.js +7 -31
- package/modules/use-cases/execute-entity-proxy-request.js +0 -15
- package/modules/use-cases/get-entities-for-user.js +5 -16
- package/modules/use-cases/get-entity-module-for-user.js +3 -23
- package/modules/use-cases/get-entity-options-by-id.js +3 -9
- package/modules/use-cases/get-module.js +3 -9
- package/modules/use-cases/multi-step-authorization.js +12 -39
- package/modules/use-cases/process-authorization-callback.js +18 -52
- package/modules/use-cases/refresh-entity-options.js +3 -9
- package/modules/use-cases/test-module-auth.js +3 -9
- package/package.json +5 -6
- package/prisma-mongodb/schema.prisma +0 -47
- package/prisma-postgresql/schema.prisma +0 -44
- package/types/index.d.ts +0 -1
- package/types/modules/index.d.ts +0 -87
- package/user/repositories/user-repository-documentdb.js +2 -27
- package/user/repositories/user-repository-interface.js +0 -18
- package/user/repositories/user-repository-mongo.js +0 -24
- package/user/repositories/user-repository-postgres.js +0 -18
- package/user/use-cases/authenticate-user.js +1 -7
- package/user/use-cases/get-user-from-adopter-jwt.js +124 -95
- package/user/use-cases/get-user-from-x-frigg-headers.js +80 -83
- package/user/user.js +1 -64
- package/database/organization-where.js +0 -18
- package/database/unique-violation.js +0 -25
- package/integrations/repositories/integration-not-found.js +0 -14
- package/integrations/use-cases/resolve-integration-ownership.js +0 -177
- package/modules/entity-scope.js +0 -15
- package/modules/scope-config.js +0 -192
- package/modules/scope-policy.js +0 -112
- package/modules/scope-resolution.js +0 -288
- package/modules/scope-visibility.js +0 -239
- package/modules/tests/doubles/ownership-matching.js +0 -18
- package/modules/use-cases/authorize-entity-for-user.js +0 -61
- package/modules/use-cases/create-entity-from-credential.js +0 -113
- package/modules/use-cases/manage-access.js +0 -21
- package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +0 -26
- package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +0 -25
- package/types/user/index.d.ts +0 -134
- package/user/adopter-jwt-config.js +0 -177
- package/user/adopter-jwt-errors.js +0 -59
- package/user/adopter-token-verifier.js +0 -122
- package/user/app-user-helpers.js +0 -97
- package/user/create-user-authentication.js +0 -64
- package/user/membership.js +0 -51
- package/user/repositories/organization-membership-record.js +0 -19
- package/user/repositories/organization-membership-repository-documentdb.js +0 -106
- package/user/repositories/organization-membership-repository-factory.js +0 -42
- package/user/repositories/organization-membership-repository-interface.js +0 -57
- package/user/repositories/organization-membership-repository-mongo.js +0 -13
- package/user/repositories/organization-membership-repository-postgres.js +0 -29
- package/user/repositories/organization-membership-repository-prisma.js +0 -81
- package/user/tests/doubles/adopter-jwt-fixtures.js +0 -220
- package/user/use-cases/ensure-organization-membership.js +0 -92
- package/user/use-cases/find-individual-users-by-organization.js +0 -54
|
@@ -108,26 +108,6 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
108
108
|
return credentialMap;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
/**
|
|
112
|
-
* The one place a stored entity row becomes the entity the application sees.
|
|
113
|
-
* The provider's `data` is spread FIRST and every column is pinned after it:
|
|
114
|
-
* a stored `id`, `userId` or `organizationId` in `data` must not decide who
|
|
115
|
-
* owns the entity or who sees it.
|
|
116
|
-
* @private
|
|
117
|
-
*/
|
|
118
|
-
_mapEntity(row, credential) {
|
|
119
|
-
return {
|
|
120
|
-
...(row.data || {}),
|
|
121
|
-
id: row.id.toString(),
|
|
122
|
-
credential,
|
|
123
|
-
userId: row.userId?.toString(),
|
|
124
|
-
name: row.name,
|
|
125
|
-
externalId: row.externalId,
|
|
126
|
-
moduleName: row.moduleName,
|
|
127
|
-
organizationId: row.organizationId?.toString() ?? null,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
111
|
/**
|
|
132
112
|
* Find entity by ID with credential
|
|
133
113
|
* Replaces: Entity.findById(entityId).populate('credential')
|
|
@@ -149,7 +129,15 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
149
129
|
|
|
150
130
|
const credential = await this._fetchCredential(entity.credentialId);
|
|
151
131
|
|
|
152
|
-
return
|
|
132
|
+
return {
|
|
133
|
+
id: entity.id.toString(),
|
|
134
|
+
credential,
|
|
135
|
+
userId: entity.userId?.toString(),
|
|
136
|
+
name: entity.name,
|
|
137
|
+
externalId: entity.externalId,
|
|
138
|
+
moduleName: entity.moduleName,
|
|
139
|
+
...(entity.data || {}),
|
|
140
|
+
};
|
|
153
141
|
}
|
|
154
142
|
|
|
155
143
|
/**
|
|
@@ -169,7 +157,15 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
169
157
|
const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
|
|
170
158
|
const credentialMap = await this._fetchCredentialsBulk(credentialIds);
|
|
171
159
|
|
|
172
|
-
return entities.map((e) => (
|
|
160
|
+
return entities.map((e) => ({
|
|
161
|
+
id: e.id.toString(),
|
|
162
|
+
credential: credentialMap.get(e.credentialId) || null,
|
|
163
|
+
userId: e.userId?.toString(),
|
|
164
|
+
name: e.name,
|
|
165
|
+
externalId: e.externalId,
|
|
166
|
+
moduleName: e.moduleName,
|
|
167
|
+
...(e.data || {}),
|
|
168
|
+
}));
|
|
173
169
|
}
|
|
174
170
|
|
|
175
171
|
/**
|
|
@@ -189,7 +185,15 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
189
185
|
const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
|
|
190
186
|
const credentialMap = await this._fetchCredentialsBulk(credentialIds);
|
|
191
187
|
|
|
192
|
-
return entities.map((e) => (
|
|
188
|
+
return entities.map((e) => ({
|
|
189
|
+
id: e.id.toString(),
|
|
190
|
+
credential: credentialMap.get(e.credentialId) || null,
|
|
191
|
+
userId: e.userId?.toString(),
|
|
192
|
+
name: e.name,
|
|
193
|
+
externalId: e.externalId,
|
|
194
|
+
moduleName: e.moduleName,
|
|
195
|
+
...(e.data || {}),
|
|
196
|
+
}));
|
|
193
197
|
}
|
|
194
198
|
|
|
195
199
|
/**
|
|
@@ -213,32 +217,15 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
213
217
|
const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
|
|
214
218
|
const credentialMap = await this._fetchCredentialsBulk(credentialIds);
|
|
215
219
|
|
|
216
|
-
return entities.map((e) => (
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
* @returns {Promise<Array>} Array of entity objects with string IDs
|
|
226
|
-
*/
|
|
227
|
-
async findEntitiesByOwnership(clauses) {
|
|
228
|
-
if (!Array.isArray(clauses) || clauses.length === 0) return [];
|
|
229
|
-
const entities = await this.prisma.entity.findMany({
|
|
230
|
-
where: {
|
|
231
|
-
OR: clauses.map(({ userId, organizationId }) => ({
|
|
232
|
-
userId: this._convertId(userId),
|
|
233
|
-
organizationId: this._convertId(organizationId),
|
|
234
|
-
})),
|
|
235
|
-
},
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
|
|
239
|
-
const credentialMap = await this._fetchCredentialsBulk(credentialIds);
|
|
240
|
-
|
|
241
|
-
return entities.map((e) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
|
|
220
|
+
return entities.map((e) => ({
|
|
221
|
+
id: e.id.toString(),
|
|
222
|
+
credential: credentialMap.get(e.credentialId) || null,
|
|
223
|
+
userId: e.userId?.toString(),
|
|
224
|
+
name: e.name,
|
|
225
|
+
externalId: e.externalId,
|
|
226
|
+
moduleName: e.moduleName,
|
|
227
|
+
...(e.data || {}),
|
|
228
|
+
}));
|
|
242
229
|
}
|
|
243
230
|
|
|
244
231
|
/**
|
|
@@ -277,7 +264,15 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
277
264
|
|
|
278
265
|
const credential = await this._fetchCredential(entity.credentialId);
|
|
279
266
|
|
|
280
|
-
return
|
|
267
|
+
return {
|
|
268
|
+
id: entity.id.toString(),
|
|
269
|
+
credential,
|
|
270
|
+
userId: entity.userId?.toString(),
|
|
271
|
+
name: entity.name,
|
|
272
|
+
externalId: entity.externalId,
|
|
273
|
+
moduleName: entity.moduleName,
|
|
274
|
+
...(entity.data || {}),
|
|
275
|
+
};
|
|
281
276
|
}
|
|
282
277
|
|
|
283
278
|
/**
|
|
@@ -297,7 +292,15 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
297
292
|
.filter(Boolean);
|
|
298
293
|
const credentialMap = await this._fetchCredentialsBulk(credentialIds);
|
|
299
294
|
|
|
300
|
-
return entities.map((e) => (
|
|
295
|
+
return entities.map((e) => ({
|
|
296
|
+
id: e.id.toString(),
|
|
297
|
+
credential: credentialMap.get(e.credentialId) || null,
|
|
298
|
+
userId: e.userId?.toString(),
|
|
299
|
+
name: e.name,
|
|
300
|
+
externalId: e.externalId,
|
|
301
|
+
moduleName: e.moduleName,
|
|
302
|
+
...(e.data || {}),
|
|
303
|
+
}));
|
|
301
304
|
}
|
|
302
305
|
|
|
303
306
|
/**
|
|
@@ -305,13 +308,9 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
305
308
|
* Replaces: Entity.create(entityData)
|
|
306
309
|
*
|
|
307
310
|
* @param {Object} entityData - Entity data (with string IDs from application layer)
|
|
308
|
-
* @param {{organizationId?: (string|null)}} [tenancy] - the organization the entity
|
|
309
|
-
* belongs to (ADR-056 §3.6). A separate argument, not a field of `entityData`,
|
|
310
|
-
* because `entityData` carries provider fields and one named `organizationId`
|
|
311
|
-
* must not be taken for ours.
|
|
312
311
|
* @returns {Promise<Object>} Created entity object with string IDs
|
|
313
312
|
*/
|
|
314
|
-
async createEntity(entityData
|
|
313
|
+
async createEntity(entityData) {
|
|
315
314
|
const {
|
|
316
315
|
user,
|
|
317
316
|
userId,
|
|
@@ -329,7 +328,6 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
329
328
|
name,
|
|
330
329
|
moduleName,
|
|
331
330
|
externalId,
|
|
332
|
-
organizationId: this._convertId(tenancy.organizationId ?? null),
|
|
333
331
|
data: dynamicData,
|
|
334
332
|
};
|
|
335
333
|
|
|
@@ -339,7 +337,15 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
339
337
|
|
|
340
338
|
const credentialObj = await this._fetchCredential(entity.credentialId);
|
|
341
339
|
|
|
342
|
-
return
|
|
340
|
+
return {
|
|
341
|
+
id: entity.id.toString(),
|
|
342
|
+
credential: credentialObj,
|
|
343
|
+
userId: entity.userId?.toString(),
|
|
344
|
+
name: entity.name,
|
|
345
|
+
externalId: entity.externalId,
|
|
346
|
+
moduleName: entity.moduleName,
|
|
347
|
+
...(entity.data || {}),
|
|
348
|
+
};
|
|
343
349
|
}
|
|
344
350
|
|
|
345
351
|
/**
|
|
@@ -395,7 +401,15 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
395
401
|
|
|
396
402
|
const credentialObj = await this._fetchCredential(entity.credentialId);
|
|
397
403
|
|
|
398
|
-
return
|
|
404
|
+
return {
|
|
405
|
+
id: entity.id.toString(),
|
|
406
|
+
credential: credentialObj,
|
|
407
|
+
userId: entity.userId?.toString(),
|
|
408
|
+
name: entity.name,
|
|
409
|
+
externalId: entity.externalId,
|
|
410
|
+
moduleName: entity.moduleName,
|
|
411
|
+
...(entity.data || {}),
|
|
412
|
+
};
|
|
399
413
|
} catch (error) {
|
|
400
414
|
if (error.code === 'P2025') {
|
|
401
415
|
return null;
|
|
@@ -460,11 +474,6 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
460
474
|
if (filter.moduleName) where.moduleName = filter.moduleName;
|
|
461
475
|
if (filter.externalId) where.externalId = this._toString(filter.externalId);
|
|
462
476
|
|
|
463
|
-
// `null` means "no organization"; absent means "any".
|
|
464
|
-
if (filter.organizationId !== undefined) {
|
|
465
|
-
where.organizationId = this._convertId(filter.organizationId);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
477
|
return where;
|
|
469
478
|
}
|
|
470
479
|
}
|
|
@@ -1,11 +1,4 @@
|
|
|
1
1
|
const Boom = require('@hapi/boom');
|
|
2
|
-
const {
|
|
3
|
-
describeRecord,
|
|
4
|
-
visibilityClauses,
|
|
5
|
-
matchesListFilters,
|
|
6
|
-
} = require('../scope-visibility');
|
|
7
|
-
const { DEFAULT_SCOPE_POLICY } = require('../scope-policy');
|
|
8
|
-
const { assertCanManage } = require('./manage-access');
|
|
9
2
|
|
|
10
3
|
/**
|
|
11
4
|
* Management API v2 entity operations. Each one starts from
|
|
@@ -13,55 +6,29 @@ const { assertCanManage } = require('./manage-access');
|
|
|
13
6
|
* (missing or foreign entity = 404).
|
|
14
7
|
*/
|
|
15
8
|
|
|
16
|
-
/**
|
|
17
|
-
* The entities the caller may see in the active organization (ADR-056 §3.4,
|
|
18
|
-
* §3.6): the organization's own, and the caller's individual ones by the
|
|
19
|
-
* visibility rule. Each carries `tenancy` ({scope, owner, organizationId,
|
|
20
|
-
* access}); the stored entity is otherwise untouched.
|
|
21
|
-
*/
|
|
9
|
+
/** The caller's connected entities. */
|
|
22
10
|
class ListEntitiesForUser {
|
|
23
|
-
constructor({ moduleRepository
|
|
11
|
+
constructor({ moduleRepository }) {
|
|
24
12
|
this.moduleRepository = moduleRepository;
|
|
25
|
-
this.scopePolicy = scopePolicy || DEFAULT_SCOPE_POLICY;
|
|
26
13
|
}
|
|
27
14
|
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
async execute(user, filters = {}) {
|
|
33
|
-
const ctx = this.scopePolicy.accessContext(user);
|
|
34
|
-
const entities = await this.moduleRepository.findEntitiesByOwnership(
|
|
35
|
-
visibilityClauses(ctx, 'entity')
|
|
15
|
+
/** @param {Object} user - authenticated User */
|
|
16
|
+
async execute(user) {
|
|
17
|
+
const entities = await this.moduleRepository.findEntitiesByUserId(
|
|
18
|
+
user.getId()
|
|
36
19
|
);
|
|
37
|
-
|
|
38
|
-
for (const entity of Array.isArray(entities) ? entities : []) {
|
|
39
|
-
// The query is the filter; this is the same rule applied again,
|
|
40
|
-
// so a repository that returns too much cannot widen the list.
|
|
41
|
-
const tenancy = describeRecord(entity, ctx, 'entity');
|
|
42
|
-
if (tenancy && matchesListFilters(tenancy, filters)) {
|
|
43
|
-
listed.push({ ...entity, tenancy });
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return listed;
|
|
20
|
+
return Array.isArray(entities) ? entities : [];
|
|
47
21
|
}
|
|
48
22
|
}
|
|
49
23
|
|
|
50
|
-
/** One entity the caller
|
|
24
|
+
/** One entity the caller owns. */
|
|
51
25
|
class GetEntityForUser {
|
|
52
26
|
constructor({ getEntityModuleForUser }) {
|
|
53
27
|
this.getEntityModuleForUser = getEntityModuleForUser;
|
|
54
28
|
}
|
|
55
29
|
|
|
56
30
|
async execute(entityId, user) {
|
|
57
|
-
|
|
58
|
-
entityId,
|
|
59
|
-
user
|
|
60
|
-
);
|
|
61
|
-
return {
|
|
62
|
-
...entity,
|
|
63
|
-
tenancy: this.getEntityModuleForUser.describe(entity, user),
|
|
64
|
-
};
|
|
31
|
+
return this.getEntityModuleForUser.loadOwnedEntity(entityId, user);
|
|
65
32
|
}
|
|
66
33
|
}
|
|
67
34
|
|
|
@@ -112,10 +79,10 @@ class RefreshEntityOptionsForUser {
|
|
|
112
79
|
}
|
|
113
80
|
|
|
114
81
|
/**
|
|
115
|
-
* Deletes an entity the caller
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
82
|
+
* Deletes an entity the caller owns. Refuses with 409 while integrations
|
|
83
|
+
* still use it, so an integration never points at a missing entity. The
|
|
84
|
+
* credential is left alone (others may share it); delete it through
|
|
85
|
+
* /api/v2/credentials.
|
|
119
86
|
*/
|
|
120
87
|
class DeleteEntityForUser {
|
|
121
88
|
constructor({ getEntityModuleForUser, moduleRepository, integrationRepository }) {
|
|
@@ -129,10 +96,6 @@ class DeleteEntityForUser {
|
|
|
129
96
|
entityId,
|
|
130
97
|
user
|
|
131
98
|
);
|
|
132
|
-
assertCanManage(
|
|
133
|
-
this.getEntityModuleForUser.describe(entity, user)?.access,
|
|
134
|
-
`entity ${entity.id}`
|
|
135
|
-
);
|
|
136
99
|
const integrations =
|
|
137
100
|
(await this.integrationRepository.findIntegrationsByEntityId(
|
|
138
101
|
entity.id
|
|
@@ -59,31 +59,10 @@ function findDefinition(moduleDefinitions, entityType) {
|
|
|
59
59
|
return definition;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
* With a scope policy the entry also says which scope a new connection gets
|
|
64
|
-
* and which a request may choose (ADR-056 §3.1), so a UI can offer the choice.
|
|
65
|
-
* `global` is never offered: a tenant request cannot create one.
|
|
66
|
-
*/
|
|
67
|
-
const scopeOf = (scopePolicy, name, user) => {
|
|
68
|
-
if (!scopePolicy) return {};
|
|
69
|
-
const { scope, source, allow } = scopePolicy.resolveScope({
|
|
70
|
-
entityType: name,
|
|
71
|
-
user,
|
|
72
|
-
});
|
|
73
|
-
return {
|
|
74
|
-
scope: {
|
|
75
|
-
default: scope,
|
|
76
|
-
allow: allow.filter((value) => value !== 'global'),
|
|
77
|
-
source,
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const describeEntry = (name, entry, scopePolicy, user) => ({
|
|
62
|
+
const describeEntry = (name, entry) => ({
|
|
83
63
|
// The registry key is the name the API uses, whatever the definition spells.
|
|
84
64
|
...describeEntityType(entry.definition, name),
|
|
85
65
|
usedBy: usedByOf(entry),
|
|
86
|
-
...scopeOf(scopePolicy, name, user),
|
|
87
66
|
});
|
|
88
67
|
|
|
89
68
|
function requireRegistry(entityTypeRegistry) {
|
|
@@ -99,34 +78,31 @@ function requireRegistry(entityTypeRegistry) {
|
|
|
99
78
|
* bind each type; it is empty for a registry-only type.
|
|
100
79
|
*/
|
|
101
80
|
class ListEntityTypes {
|
|
102
|
-
constructor({ entityTypeRegistry
|
|
81
|
+
constructor({ entityTypeRegistry }) {
|
|
103
82
|
this.registry = requireRegistry(entityTypeRegistry);
|
|
104
|
-
this.scopePolicy = scopePolicy;
|
|
105
83
|
}
|
|
106
84
|
|
|
107
|
-
|
|
108
|
-
async execute({ user } = {}) {
|
|
85
|
+
async execute() {
|
|
109
86
|
return [...this.registry]
|
|
110
|
-
.map(([name, entry]) => describeEntry(name, entry
|
|
87
|
+
.map(([name, entry]) => describeEntry(name, entry))
|
|
111
88
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
112
89
|
}
|
|
113
90
|
}
|
|
114
91
|
|
|
115
92
|
/** Describes one entity type, looked up by its registry key; 404 when absent. */
|
|
116
93
|
class GetEntityType {
|
|
117
|
-
constructor({ entityTypeRegistry
|
|
94
|
+
constructor({ entityTypeRegistry }) {
|
|
118
95
|
this.registry = requireRegistry(entityTypeRegistry);
|
|
119
|
-
this.scopePolicy = scopePolicy;
|
|
120
96
|
}
|
|
121
97
|
|
|
122
|
-
async execute(entityType
|
|
98
|
+
async execute(entityType) {
|
|
123
99
|
const entry = this.registry.get(entityType);
|
|
124
100
|
if (!entry) {
|
|
125
101
|
throw Boom.notFound(`Entity type '${entityType}' not found`, {
|
|
126
102
|
code: 'ENTITY_TYPE_NOT_FOUND',
|
|
127
103
|
});
|
|
128
104
|
}
|
|
129
|
-
return describeEntry(entityType, entry
|
|
105
|
+
return describeEntry(entityType, entry);
|
|
130
106
|
}
|
|
131
107
|
}
|
|
132
108
|
|
|
@@ -98,24 +98,9 @@ class ExecuteEntityProxyRequest {
|
|
|
98
98
|
audit.requestBytes = validated.requestBytes;
|
|
99
99
|
const path = normalizeProxyPath(validated.path);
|
|
100
100
|
|
|
101
|
-
// Access is `use` in the active organization (ADR-056 §3.5, §3.6): the
|
|
102
|
-
// owner's entity there, or the organization's. A `global` entity is
|
|
103
|
-
// never the tenant's.
|
|
104
101
|
const { entity, definition, module } =
|
|
105
102
|
await this.getEntityModuleForUser.execute(entityId, user);
|
|
106
103
|
audit.module = definition.moduleName;
|
|
107
|
-
// Narrowest by default (§3.5): proxying through a shared credential
|
|
108
|
-
// lets any member act on the organization's account, so only an
|
|
109
|
-
// individual entity may be proxied until `managementApi.proxy.scopes`
|
|
110
|
-
// lets an app opt in to `organization`.
|
|
111
|
-
// TODO(F6): read proxy.scopes here instead of refusing outright.
|
|
112
|
-
if (this.getEntityModuleForUser.describe(entity, user)?.scope !== 'individual') {
|
|
113
|
-
throw new ProxyRequestError(
|
|
114
|
-
'PROXY_NOT_ALLOWED',
|
|
115
|
-
'The entity proxy only accepts individual entities; organization-level entities are not proxied',
|
|
116
|
-
{ module: definition.moduleName, reason: 'scope' }
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
104
|
const credentialRef = entity.credential;
|
|
120
105
|
const credentialId =
|
|
121
106
|
credentialRef && typeof credentialRef === 'object'
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
const { Module } = require('../module');
|
|
2
2
|
const { mapModuleClassToModuleDTO } = require('../utils/map-module-dto');
|
|
3
|
-
const { describeRecord, visibilityClauses } = require('../scope-visibility');
|
|
4
|
-
const { DEFAULT_SCOPE_POLICY } = require('../scope-policy');
|
|
5
3
|
|
|
6
4
|
class GetEntitiesForUser {
|
|
7
|
-
constructor({ moduleRepository, moduleDefinitions
|
|
8
|
-
this.scopePolicy = scopePolicy || DEFAULT_SCOPE_POLICY;
|
|
5
|
+
constructor({ moduleRepository, moduleDefinitions }) {
|
|
9
6
|
this.moduleRepository = moduleRepository;
|
|
10
7
|
|
|
11
8
|
this.definitionMap = new Map();
|
|
@@ -14,24 +11,16 @@ class GetEntitiesForUser {
|
|
|
14
11
|
}
|
|
15
12
|
}
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
*/
|
|
21
|
-
async execute(user) {
|
|
22
|
-
const ctx = this.scopePolicy.accessContext(user);
|
|
23
|
-
const found = await this.moduleRepository.findEntitiesByOwnership(
|
|
24
|
-
visibilityClauses(ctx, 'entity')
|
|
25
|
-
);
|
|
26
|
-
const entities = (found || []).filter((entity) =>
|
|
27
|
-
describeRecord(entity, ctx, 'entity')
|
|
14
|
+
async execute(userId) {
|
|
15
|
+
const entities = await this.moduleRepository.findEntitiesByUserId(
|
|
16
|
+
userId
|
|
28
17
|
);
|
|
29
18
|
|
|
30
19
|
return entities.map((entity) => {
|
|
31
20
|
const definition = this.definitionMap.get(entity.moduleName);
|
|
32
21
|
|
|
33
22
|
const moduleInstance = new Module({
|
|
34
|
-
userId
|
|
23
|
+
userId,
|
|
35
24
|
definition: definition,
|
|
36
25
|
entity: entity,
|
|
37
26
|
});
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
const Boom = require('@hapi/boom');
|
|
2
|
-
const { describeRecord } = require('../scope-visibility');
|
|
3
|
-
const { DEFAULT_SCOPE_POLICY } = require('../scope-policy');
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
4
|
* True when `user` may act on records owned by `ownerId`. Uses the User
|
|
7
5
|
* domain object's rule (individual or organisation user model) when present.
|
|
8
|
-
* This is the owner-only rule: it ignores the organization a record belongs
|
|
9
|
-
* to, so it no longer decides access to an entity or an integration
|
|
10
|
-
* (modules/scope-visibility.js does). Credentials still use it.
|
|
11
6
|
* @param {Object} user - authenticated User (or a plain { getId } double)
|
|
12
7
|
* @param {string|number} ownerId
|
|
13
8
|
*/
|
|
@@ -35,11 +30,8 @@ class GetEntityModuleForUser {
|
|
|
35
30
|
* @param {import('../repositories/module-repository-interface').ModuleRepositoryInterface} params.moduleRepository
|
|
36
31
|
* @param {Array<Object>} params.moduleDefinitions
|
|
37
32
|
* @param {Function} [params.createModule] - builds a Module; injectable for tests.
|
|
38
|
-
* @param {Object} [params.scopePolicy] - from createScopePolicy; with none the
|
|
39
|
-
* app configures nothing (ADR-056 §3.1).
|
|
40
33
|
*/
|
|
41
|
-
constructor({ moduleRepository, moduleDefinitions, createModule
|
|
42
|
-
this.scopePolicy = scopePolicy || DEFAULT_SCOPE_POLICY;
|
|
34
|
+
constructor({ moduleRepository, moduleDefinitions, createModule }) {
|
|
43
35
|
this.moduleRepository = moduleRepository;
|
|
44
36
|
this.moduleDefinitions = moduleDefinitions || [];
|
|
45
37
|
this.createModule = createModule || defaultCreateModule;
|
|
@@ -69,11 +61,7 @@ class GetEntityModuleForUser {
|
|
|
69
61
|
return { entity, definition, module };
|
|
70
62
|
}
|
|
71
63
|
|
|
72
|
-
/**
|
|
73
|
-
* The entity record, or a 404 when it is missing or the caller may not use
|
|
74
|
-
* it: not theirs, or paired with another organization (ADR-056 §3.6). A
|
|
75
|
-
* `global` entity is never the tenant's to load.
|
|
76
|
-
*/
|
|
64
|
+
/** The entity record, or a 404 when it is missing or not the caller's. */
|
|
77
65
|
async loadOwnedEntity(entityId, user) {
|
|
78
66
|
let entity = null;
|
|
79
67
|
try {
|
|
@@ -86,21 +74,13 @@ class GetEntityModuleForUser {
|
|
|
86
74
|
throw error;
|
|
87
75
|
}
|
|
88
76
|
}
|
|
89
|
-
if (!entity || !
|
|
77
|
+
if (!entity || !userOwns(user, entity.userId)) {
|
|
90
78
|
throw Boom.notFound(`Entity ${entityId} not found`, {
|
|
91
79
|
code: 'ENTITY_NOT_FOUND',
|
|
92
80
|
});
|
|
93
81
|
}
|
|
94
82
|
return entity;
|
|
95
83
|
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* `{ scope, owner, organizationId, access }` of an entity to this caller,
|
|
99
|
-
* or null when they may not see it (§3.4).
|
|
100
|
-
*/
|
|
101
|
-
describe(entity, user) {
|
|
102
|
-
return describeRecord(entity, this.scopePolicy.accessContext(user), 'entity');
|
|
103
|
-
}
|
|
104
84
|
}
|
|
105
85
|
|
|
106
86
|
module.exports = { GetEntityModuleForUser, userOwns };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const { Module } = require('../module');
|
|
2
|
-
const { DEFAULT_SCOPE_POLICY } = require('../scope-policy');
|
|
3
2
|
|
|
4
3
|
class GetEntityOptionsById {
|
|
5
4
|
/**
|
|
@@ -7,11 +6,9 @@ class GetEntityOptionsById {
|
|
|
7
6
|
* @param {import('../repositories/module-repository-interface').ModuleRepositoryInterface} params.moduleRepository
|
|
8
7
|
* @param {} params.moduleDefinitions
|
|
9
8
|
*/
|
|
10
|
-
constructor({ moduleRepository, moduleDefinitions
|
|
9
|
+
constructor({ moduleRepository, moduleDefinitions }) {
|
|
11
10
|
this.moduleRepository = moduleRepository;
|
|
12
11
|
this.moduleDefinitions = moduleDefinitions;
|
|
13
|
-
// Who may load an entity follows its scope (ADR-056 §3.6).
|
|
14
|
-
this.scopePolicy = scopePolicy || DEFAULT_SCOPE_POLICY;
|
|
15
12
|
}
|
|
16
13
|
|
|
17
14
|
/**
|
|
@@ -38,7 +35,7 @@ class GetEntityOptionsById {
|
|
|
38
35
|
|
|
39
36
|
// Validate entity ownership
|
|
40
37
|
const isOwned = typeof userIdOrUser === 'object' && userIdOrUser?.ownsUserId
|
|
41
|
-
?
|
|
38
|
+
? userIdOrUser.ownsUserId(entity.userId)
|
|
42
39
|
: entity.userId?.toString() === userId?.toString();
|
|
43
40
|
|
|
44
41
|
if (!isOwned) {
|
|
@@ -60,11 +57,8 @@ class GetEntityOptionsById {
|
|
|
60
57
|
);
|
|
61
58
|
}
|
|
62
59
|
|
|
63
|
-
// Built for the entity's OWNER, not the caller: a caller may use an
|
|
64
|
-
// organization's entity, and a token refresh during this call must
|
|
65
|
-
// update the owner's credential, not create one under the caller.
|
|
66
60
|
const module = new Module({
|
|
67
|
-
userId
|
|
61
|
+
userId,
|
|
68
62
|
entity,
|
|
69
63
|
definition: moduleDefinition,
|
|
70
64
|
});
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
const { Module } = require('../module');
|
|
2
|
-
const { DEFAULT_SCOPE_POLICY } = require('../scope-policy');
|
|
3
2
|
|
|
4
3
|
class GetModule {
|
|
5
|
-
constructor({ moduleRepository, moduleDefinitions
|
|
4
|
+
constructor({ moduleRepository, moduleDefinitions }) {
|
|
6
5
|
this.moduleRepository = moduleRepository;
|
|
7
6
|
this.moduleDefinitions = moduleDefinitions;
|
|
8
|
-
// Who may load an entity follows its scope (ADR-056 §3.6).
|
|
9
|
-
this.scopePolicy = scopePolicy || DEFAULT_SCOPE_POLICY;
|
|
10
7
|
}
|
|
11
8
|
|
|
12
9
|
/**
|
|
@@ -35,7 +32,7 @@ class GetModule {
|
|
|
35
32
|
// If User object provided, use ownsUserId to check linked users
|
|
36
33
|
// Otherwise fall back to simple equality check
|
|
37
34
|
const isOwned = typeof userIdOrUser === 'object' && userIdOrUser?.ownsUserId
|
|
38
|
-
?
|
|
35
|
+
? userIdOrUser.ownsUserId(entity.userId)
|
|
39
36
|
: entity.userId?.toString() === userId?.toString();
|
|
40
37
|
|
|
41
38
|
if (!isOwned) {
|
|
@@ -56,11 +53,8 @@ class GetModule {
|
|
|
56
53
|
);
|
|
57
54
|
}
|
|
58
55
|
|
|
59
|
-
// Built for the entity's OWNER, not the caller: a caller may use an
|
|
60
|
-
// organization's entity, and a token refresh during this call must
|
|
61
|
-
// update the owner's credential, not create one under the caller.
|
|
62
56
|
const module = new Module({
|
|
63
|
-
userId
|
|
57
|
+
userId,
|
|
64
58
|
entity,
|
|
65
59
|
definition: moduleDefinition,
|
|
66
60
|
});
|