@friggframework/core 2.0.0--canary.680.daae24e.0 → 2.0.0--canary.686.e94de4d.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/CHANGELOG.md +0 -11
- package/CLAUDE.md +28 -0
- package/README.md +105 -0
- package/application/commands/integration-commands.js +2 -2
- package/application/commands/scheduler-commands.js +11 -7
- package/application/commands/user-commands.js +17 -5
- package/core/Worker.js +31 -7
- package/core/create-handler.js +9 -0
- package/credential/use-cases/credential-operations-for-user.js +110 -25
- package/credential/use-cases/reauthorize-credential-for-user.js +15 -8
- package/database/config.js +48 -24
- package/database/documentdb-utils.js +6 -1
- package/database/encryption/README.md +0 -2
- package/database/encryption/encryption-config.js +10 -31
- package/database/encryption/encryption-schema-registry.js +99 -47
- package/database/encryption/field-encryption-service.js +5 -69
- package/database/organization-where.js +18 -0
- package/database/prisma.js +4 -2
- package/database/unique-violation.js +25 -0
- package/extensions/bootstrap-runner.js +43 -0
- package/extensions/extension-loader.js +187 -0
- package/extensions/index.js +41 -0
- package/extensions/initialize-app.js +123 -0
- package/extensions/route-mounter.js +103 -0
- package/extensions/schema-composer.js +188 -0
- package/generated/prisma-mongodb/edge.js +27 -4
- package/generated/prisma-mongodb/index-browser.js +24 -1
- package/generated/prisma-mongodb/index.d.ts +3227 -47
- package/generated/prisma-mongodb/index.js +27 -4
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +65 -0
- package/generated/prisma-mongodb/wasm.js +27 -4
- package/generated/prisma-postgresql/edge.js +27 -4
- package/generated/prisma-postgresql/index-browser.js +24 -1
- package/generated/prisma-postgresql/index.d.ts +3519 -67
- package/generated/prisma-postgresql/index.js +27 -4
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +61 -0
- package/generated/prisma-postgresql/wasm.js +27 -4
- package/handlers/app-definition-loader.js +103 -12
- package/handlers/routers/auth.js +40 -12
- package/handlers/routers/health.js +4 -2
- package/handlers/routers/integration-defined-routers.js +144 -92
- package/handlers/routers/integration-webhook-routers.js +102 -64
- package/index.js +63 -1
- package/infrastructure/scheduler/index.js +19 -6
- package/infrastructure/scheduler/netlify-scheduler-adapter.js +216 -0
- package/infrastructure/scheduler/scheduled-job-repository.js +118 -0
- package/infrastructure/scheduler/scheduler-service-factory.js +55 -7
- package/integrations/index.js +5 -0
- package/integrations/integration-router.js +115 -73
- package/integrations/repositories/integration-not-found.js +14 -0
- package/integrations/repositories/integration-repository-documentdb.js +35 -3
- package/integrations/repositories/integration-repository-interface.js +17 -1
- package/integrations/repositories/integration-repository-mongo.js +59 -76
- package/integrations/repositories/integration-repository-postgres.js +62 -76
- package/integrations/schedule-config.js +80 -0
- package/integrations/tests/doubles/test-integration-repository.js +12 -1
- package/integrations/use-cases/create-integration.js +55 -20
- package/integrations/use-cases/dispatch-ongoing-syncs.js +100 -0
- package/integrations/use-cases/get-integrations-for-user.js +80 -40
- package/integrations/use-cases/index.js +7 -1
- package/integrations/use-cases/integration-operations-for-user.js +55 -24
- package/integrations/use-cases/resolve-integration-ownership.js +177 -0
- package/integrations/utils/map-integration-dto.js +44 -1
- package/management-api/management-api-config.js +71 -2
- package/management-api/meta-router.js +21 -2
- package/management-api/openapi/components.js +40 -0
- package/management-api/openapi/v2-operations.js +16 -4
- package/management-api/route-registry.js +123 -6
- package/management-api/v2/dependencies.js +40 -15
- package/management-api/v2/dto.js +20 -0
- package/management-api/v2/handlers/authorization.js +1 -0
- package/management-api/v2/handlers/credentials.js +6 -2
- package/management-api/v2/handlers/entities.js +10 -7
- package/management-api/v2/handlers/integrations.js +6 -3
- package/management-api/v2/request.js +32 -1
- package/modules/entity-scope.js +15 -0
- package/modules/index.js +8 -0
- package/modules/module-factory.js +12 -0
- package/modules/module.js +53 -2
- package/modules/repositories/entity-not-found.js +18 -0
- package/modules/repositories/module-repository-documentdb.js +51 -6
- package/modules/repositories/module-repository-interface.js +19 -2
- package/modules/repositories/module-repository-mongo.js +67 -74
- package/modules/repositories/module-repository-postgres.js +66 -74
- package/modules/repositories/module-repository.js +2 -1
- package/modules/scope-config.js +192 -0
- package/modules/scope-policy.js +112 -0
- package/modules/scope-resolution.js +288 -0
- package/modules/scope-visibility.js +247 -0
- package/modules/tests/doubles/ownership-matching.js +18 -0
- package/modules/use-cases/authorize-entity-for-user.js +61 -0
- package/modules/use-cases/create-entity-from-credential.js +113 -0
- package/modules/use-cases/entity-operations-for-user.js +50 -13
- package/modules/use-cases/entity-types.js +31 -7
- package/modules/use-cases/execute-entity-proxy-request.js +50 -3
- package/modules/use-cases/get-authorization-requirements.js +2 -2
- package/modules/use-cases/get-entities-for-user.js +26 -13
- package/modules/use-cases/get-entity-module-for-user.js +34 -7
- package/modules/use-cases/get-entity-options-by-id.js +10 -4
- package/modules/use-cases/get-entity-options-by-type.js +1 -1
- package/modules/use-cases/get-module-instance-from-type.js +1 -1
- package/modules/use-cases/get-module.js +10 -4
- package/modules/use-cases/manage-access.js +21 -0
- package/modules/use-cases/multi-step-authorization.js +40 -13
- package/modules/use-cases/process-authorization-callback.js +53 -19
- package/modules/use-cases/refresh-entity-options.js +10 -4
- package/modules/use-cases/test-module-auth.js +10 -4
- package/package.json +6 -5
- package/prisma-mongodb/schema.prisma +65 -0
- package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +26 -0
- package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
- package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +28 -0
- package/prisma-postgresql/schema.prisma +61 -0
- package/providers/index.js +23 -0
- package/providers/resolve-provider.js +93 -0
- package/providers/runtime-detection.js +124 -0
- package/queues/index.js +25 -0
- package/queues/providers/index.js +11 -0
- package/queues/providers/netlify-background-provider.js +150 -0
- package/queues/providers/qstash-queue-provider.js +219 -0
- package/queues/providers/sqs-queue-provider.js +70 -0
- package/queues/queue-client-interface.js +55 -0
- package/queues/queue-message-auth.js +193 -0
- package/queues/queue-provider-factory.js +101 -0
- package/queues/queue-provider.js +65 -0
- package/queues/sqs-queue-client.js +81 -0
- package/types/index.d.ts +1 -0
- package/types/modules/index.d.ts +139 -0
- package/types/user/index.d.ts +134 -0
- package/user/adopter-jwt-config.js +177 -0
- package/user/adopter-jwt-errors.js +59 -0
- package/user/adopter-token-verifier.js +122 -0
- package/user/app-user-helpers.js +97 -0
- package/user/create-user-authentication.js +64 -0
- package/user/membership.js +51 -0
- package/user/repositories/organization-membership-record.js +19 -0
- package/user/repositories/organization-membership-repository-documentdb.js +106 -0
- package/user/repositories/organization-membership-repository-factory.js +42 -0
- package/user/repositories/organization-membership-repository-interface.js +57 -0
- package/user/repositories/organization-membership-repository-mongo.js +13 -0
- package/user/repositories/organization-membership-repository-postgres.js +29 -0
- package/user/repositories/organization-membership-repository-prisma.js +81 -0
- package/user/repositories/user-repository-documentdb.js +27 -2
- package/user/repositories/user-repository-interface.js +18 -0
- package/user/repositories/user-repository-mongo.js +24 -0
- package/user/repositories/user-repository-postgres.js +18 -0
- package/user/tests/doubles/adopter-jwt-fixtures.js +220 -0
- package/user/use-cases/authenticate-user.js +7 -1
- package/user/use-cases/ensure-organization-membership.js +92 -0
- package/user/use-cases/find-individual-users-by-organization.js +54 -0
- package/user/use-cases/get-user-from-adopter-jwt.js +95 -124
- package/user/use-cases/get-user-from-x-frigg-headers.js +83 -80
- package/user/user.js +64 -1
- package/database/encryption/test-support/fake-ciphertext.js +0 -15
|
@@ -51,22 +51,27 @@ const {
|
|
|
51
51
|
const {
|
|
52
52
|
createUserRepository,
|
|
53
53
|
} = require('../user/repositories/user-repository-factory');
|
|
54
|
-
const {
|
|
55
|
-
GetUserFromBearerToken,
|
|
56
|
-
} = require('../user/use-cases/get-user-from-bearer-token');
|
|
57
|
-
const {
|
|
58
|
-
GetUserFromXFriggHeaders,
|
|
59
|
-
} = require('../user/use-cases/get-user-from-x-frigg-headers');
|
|
60
|
-
const {
|
|
61
|
-
GetUserFromAdopterJwt,
|
|
62
|
-
} = require('../user/use-cases/get-user-from-adopter-jwt');
|
|
63
54
|
const {
|
|
64
55
|
AuthenticateWithSharedSecret,
|
|
65
56
|
} = require('../user/use-cases/authenticate-with-shared-secret');
|
|
66
57
|
const { AuthenticateUser } = require('../user/use-cases/authenticate-user');
|
|
58
|
+
const {
|
|
59
|
+
buildUserAuthentication,
|
|
60
|
+
} = require('../user/create-user-authentication');
|
|
67
61
|
const {
|
|
68
62
|
ProcessAuthorizationCallback,
|
|
69
63
|
} = require('../modules/use-cases/process-authorization-callback');
|
|
64
|
+
const { createScopePolicy } = require('../modules/scope-policy');
|
|
65
|
+
const {
|
|
66
|
+
AuthorizeEntityForUser,
|
|
67
|
+
} = require('../modules/use-cases/authorize-entity-for-user');
|
|
68
|
+
const {
|
|
69
|
+
CreateEntityFromCredential,
|
|
70
|
+
} = require('../modules/use-cases/create-entity-from-credential');
|
|
71
|
+
const {
|
|
72
|
+
ResolveIntegrationOwnership,
|
|
73
|
+
} = require('./use-cases/resolve-integration-ownership');
|
|
74
|
+
const { GetOwnedIntegrationRecord } = require('./use-cases/integration-operations-for-user');
|
|
70
75
|
|
|
71
76
|
const log = getLogger('frigg.integrations');
|
|
72
77
|
|
|
@@ -77,26 +82,20 @@ function createIntegrationRouter() {
|
|
|
77
82
|
integrations: integrationClasses,
|
|
78
83
|
userConfig,
|
|
79
84
|
moduleDefinitions,
|
|
85
|
+
ownership,
|
|
86
|
+
entityTypeRegistry,
|
|
80
87
|
} = loadAppDefinition();
|
|
88
|
+
// Ownership by scope (ADR-056 §3): one object every use case asks.
|
|
89
|
+
const scopePolicy = createScopePolicy({ ownership, entityTypeRegistry });
|
|
81
90
|
const moduleRepository = createModuleRepository();
|
|
82
91
|
const integrationRepository = createIntegrationRepository();
|
|
83
92
|
const credentialRepository = createCredentialRepository();
|
|
84
93
|
const userRepository = createUserRepository();
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
const getUserFromXFriggHeaders = new GetUserFromXFriggHeaders({
|
|
92
|
-
userRepository,
|
|
93
|
-
userConfig,
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
const getUserFromAdopterJwt = new GetUserFromAdopterJwt({
|
|
97
|
-
userRepository,
|
|
98
|
-
userConfig,
|
|
99
|
-
});
|
|
94
|
+
const {
|
|
95
|
+
getUserFromBearerToken,
|
|
96
|
+
getUserFromXFriggHeaders,
|
|
97
|
+
getUserFromAdopterJwt,
|
|
98
|
+
} = buildUserAuthentication({ userConfig, userRepository });
|
|
100
99
|
|
|
101
100
|
const authenticateWithSharedSecret = new AuthenticateWithSharedSecret();
|
|
102
101
|
|
|
@@ -123,6 +122,19 @@ function createIntegrationRouter() {
|
|
|
123
122
|
integrationClasses,
|
|
124
123
|
moduleFactory,
|
|
125
124
|
moduleRepository,
|
|
125
|
+
scopePolicy,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Decides who owns a new integration and which entities the caller may bind
|
|
129
|
+
// (the v1 create used to check neither), and who may reach an existing one.
|
|
130
|
+
const resolveIntegrationOwnership = new ResolveIntegrationOwnership({
|
|
131
|
+
moduleRepository,
|
|
132
|
+
integrationClasses,
|
|
133
|
+
scopePolicy,
|
|
134
|
+
});
|
|
135
|
+
const getOwnedIntegrationRecord = new GetOwnedIntegrationRecord({
|
|
136
|
+
integrationRepository,
|
|
137
|
+
scopePolicy,
|
|
126
138
|
});
|
|
127
139
|
|
|
128
140
|
const getCredentialForUser = new GetCredentialForUser({
|
|
@@ -138,6 +150,7 @@ function createIntegrationRouter() {
|
|
|
138
150
|
const getEntitiesForUser = new GetEntitiesForUser({
|
|
139
151
|
moduleRepository,
|
|
140
152
|
moduleDefinitions,
|
|
153
|
+
scopePolicy,
|
|
141
154
|
});
|
|
142
155
|
|
|
143
156
|
const getIntegrationInstance = new GetIntegrationInstance({
|
|
@@ -163,37 +176,55 @@ function createIntegrationRouter() {
|
|
|
163
176
|
const testModuleAuth = new TestModuleAuth({
|
|
164
177
|
moduleRepository,
|
|
165
178
|
moduleDefinitions,
|
|
179
|
+
scopePolicy,
|
|
166
180
|
});
|
|
167
181
|
|
|
168
182
|
const getModule = new GetModule({
|
|
169
183
|
moduleRepository,
|
|
170
184
|
moduleDefinitions,
|
|
185
|
+
scopePolicy,
|
|
171
186
|
});
|
|
172
187
|
|
|
173
188
|
const getEntityOptionsById = new GetEntityOptionsById({
|
|
174
189
|
moduleRepository,
|
|
175
190
|
moduleDefinitions,
|
|
191
|
+
scopePolicy,
|
|
176
192
|
});
|
|
177
193
|
|
|
178
194
|
const refreshEntityOptions = new RefreshEntityOptions({
|
|
179
195
|
moduleRepository,
|
|
180
196
|
moduleDefinitions,
|
|
197
|
+
scopePolicy,
|
|
181
198
|
});
|
|
182
199
|
|
|
183
200
|
const getPossibleIntegrations = new GetPossibleIntegrations({
|
|
184
201
|
integrationClasses,
|
|
185
202
|
});
|
|
186
203
|
|
|
204
|
+
// The owner of what an authorization creates follows its scope (ADR-056 §3.2).
|
|
187
205
|
const processAuthorizationCallback = new ProcessAuthorizationCallback({
|
|
188
206
|
moduleRepository,
|
|
189
207
|
credentialRepository,
|
|
190
208
|
integrationRepository,
|
|
191
209
|
moduleDefinitions,
|
|
192
210
|
});
|
|
211
|
+
const authorizeEntityForUser = new AuthorizeEntityForUser({
|
|
212
|
+
scopePolicy,
|
|
213
|
+
processAuthorizationCallback,
|
|
214
|
+
});
|
|
215
|
+
// v1 POST /api/entity: same owner resolution as /api/authorize.
|
|
216
|
+
const createEntityFromCredential = new CreateEntityFromCredential({
|
|
217
|
+
authorizeEntityForUser,
|
|
218
|
+
processAuthorizationCallback,
|
|
219
|
+
credentialRepository,
|
|
220
|
+
moduleDefinitions,
|
|
221
|
+
});
|
|
193
222
|
|
|
194
223
|
const router = express();
|
|
195
224
|
|
|
196
225
|
setIntegrationRoutes(router, authenticateUser, {
|
|
226
|
+
resolveIntegrationOwnership,
|
|
227
|
+
getOwnedIntegrationRecord,
|
|
197
228
|
createIntegration,
|
|
198
229
|
deleteIntegrationForUser,
|
|
199
230
|
getIntegrationsForUser,
|
|
@@ -210,7 +241,8 @@ function createIntegrationRouter() {
|
|
|
210
241
|
getModule,
|
|
211
242
|
getEntityOptionsById,
|
|
212
243
|
refreshEntityOptions,
|
|
213
|
-
|
|
244
|
+
authorizeEntityForUser,
|
|
245
|
+
createEntityFromCredential,
|
|
214
246
|
});
|
|
215
247
|
return router;
|
|
216
248
|
}
|
|
@@ -247,6 +279,8 @@ function checkRequiredParams(params, requiredKeys) {
|
|
|
247
279
|
*/
|
|
248
280
|
function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
249
281
|
const {
|
|
282
|
+
resolveIntegrationOwnership,
|
|
283
|
+
getOwnedIntegrationRecord,
|
|
250
284
|
createIntegration,
|
|
251
285
|
deleteIntegrationForUser,
|
|
252
286
|
getIntegrationsForUser,
|
|
@@ -255,15 +289,22 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
255
289
|
updateIntegration,
|
|
256
290
|
getPossibleIntegrations,
|
|
257
291
|
} = useCases;
|
|
292
|
+
|
|
293
|
+
// The owner of an integration the caller may reach (404 otherwise), which is
|
|
294
|
+
// what the id-and-owner use cases below expect. `manage: true` is for
|
|
295
|
+
// changing or deleting: an organization's integration needs an admin (403).
|
|
296
|
+
const ownerOf = async (integrationId, user, options) =>
|
|
297
|
+
(await getOwnedIntegrationRecord.execute(integrationId, user, options))
|
|
298
|
+
.userId;
|
|
299
|
+
|
|
258
300
|
router.route('/api/integrations').get(
|
|
259
301
|
catchAsyncError(async (req, res) => {
|
|
260
302
|
const user = await authenticateUser.execute(req);
|
|
261
|
-
const
|
|
262
|
-
const integrations = await getIntegrationsForUser.execute(userId);
|
|
303
|
+
const integrations = await getIntegrationsForUser.execute(user);
|
|
263
304
|
const results = {
|
|
264
305
|
entities: {
|
|
265
306
|
options: await getPossibleIntegrations.execute(),
|
|
266
|
-
authorized: await getEntitiesForUser.execute(
|
|
307
|
+
authorized: await getEntitiesForUser.execute(user),
|
|
267
308
|
},
|
|
268
309
|
integrations: integrations,
|
|
269
310
|
};
|
|
@@ -275,7 +316,6 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
275
316
|
router.route('/api/integrations').post(
|
|
276
317
|
catchAsyncError(async (req, res) => {
|
|
277
318
|
const user = await authenticateUser.execute(req);
|
|
278
|
-
const userId = user.getId();
|
|
279
319
|
const params = checkRequiredParams(req.body, [
|
|
280
320
|
'entities',
|
|
281
321
|
'config',
|
|
@@ -283,10 +323,17 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
283
323
|
|
|
284
324
|
get(params.config, 'type');
|
|
285
325
|
|
|
326
|
+
const { ownerId, organizationId, adoptUnscoped } =
|
|
327
|
+
await resolveIntegrationOwnership.execute({
|
|
328
|
+
entityIds: params.entities,
|
|
329
|
+
config: params.config,
|
|
330
|
+
user,
|
|
331
|
+
});
|
|
286
332
|
const integration = await createIntegration.execute(
|
|
287
333
|
params.entities,
|
|
288
|
-
|
|
289
|
-
params.config
|
|
334
|
+
ownerId,
|
|
335
|
+
params.config,
|
|
336
|
+
{ organizationId, adoptUnscoped }
|
|
290
337
|
);
|
|
291
338
|
|
|
292
339
|
res.status(201).json(integration);
|
|
@@ -296,12 +343,11 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
296
343
|
router.route('/api/integrations/:integrationId').patch(
|
|
297
344
|
catchAsyncError(async (req, res) => {
|
|
298
345
|
const user = await authenticateUser.execute(req);
|
|
299
|
-
const userId = user.getId();
|
|
300
346
|
const params = checkRequiredParams(req.body, ['config']);
|
|
301
347
|
|
|
302
348
|
const integration = await updateIntegration.execute(
|
|
303
349
|
req.params.integrationId,
|
|
304
|
-
|
|
350
|
+
await ownerOf(req.params.integrationId, user, { manage: true }),
|
|
305
351
|
params.config
|
|
306
352
|
);
|
|
307
353
|
res.json(integration);
|
|
@@ -314,7 +360,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
314
360
|
const params = checkRequiredParams(req.params, ['integrationId']);
|
|
315
361
|
await deleteIntegrationForUser.execute(
|
|
316
362
|
params.integrationId,
|
|
317
|
-
user
|
|
363
|
+
await ownerOf(params.integrationId, user, { manage: true })
|
|
318
364
|
);
|
|
319
365
|
res.status(204).json({});
|
|
320
366
|
})
|
|
@@ -326,7 +372,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
326
372
|
const params = checkRequiredParams(req.params, ['integrationId']);
|
|
327
373
|
const integration = await getIntegrationInstance.execute(
|
|
328
374
|
params.integrationId,
|
|
329
|
-
user
|
|
375
|
+
await ownerOf(params.integrationId, user)
|
|
330
376
|
);
|
|
331
377
|
res.json(await integration.send('GET_CONFIG_OPTIONS'));
|
|
332
378
|
})
|
|
@@ -342,7 +388,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
342
388
|
]);
|
|
343
389
|
const integration = await getIntegrationInstance.execute(
|
|
344
390
|
params.integrationId,
|
|
345
|
-
user
|
|
391
|
+
await ownerOf(params.integrationId, user)
|
|
346
392
|
);
|
|
347
393
|
|
|
348
394
|
res.json(
|
|
@@ -356,7 +402,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
356
402
|
const params = checkRequiredParams(req.params, ['integrationId']);
|
|
357
403
|
const integration = await getIntegrationInstance.execute(
|
|
358
404
|
params.integrationId,
|
|
359
|
-
user
|
|
405
|
+
await ownerOf(params.integrationId, user)
|
|
360
406
|
);
|
|
361
407
|
res.json(await integration.send('GET_USER_ACTIONS', req.body));
|
|
362
408
|
})
|
|
@@ -373,7 +419,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
373
419
|
]);
|
|
374
420
|
const integration = await getIntegrationInstance.execute(
|
|
375
421
|
params.integrationId,
|
|
376
|
-
user
|
|
422
|
+
await ownerOf(params.integrationId, user)
|
|
377
423
|
);
|
|
378
424
|
|
|
379
425
|
res.json(
|
|
@@ -398,7 +444,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
398
444
|
]);
|
|
399
445
|
const integration = await getIntegrationInstance.execute(
|
|
400
446
|
params.integrationId,
|
|
401
|
-
user
|
|
447
|
+
await ownerOf(params.integrationId, user)
|
|
402
448
|
);
|
|
403
449
|
|
|
404
450
|
res.json(
|
|
@@ -419,7 +465,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
419
465
|
]);
|
|
420
466
|
const integration = await getIntegrationInstance.execute(
|
|
421
467
|
params.integrationId,
|
|
422
|
-
user
|
|
468
|
+
await ownerOf(params.integrationId, user)
|
|
423
469
|
);
|
|
424
470
|
res.json(await integration.send(params.actionId, req.body));
|
|
425
471
|
})
|
|
@@ -436,7 +482,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
436
482
|
const params = checkRequiredParams(req.params, ['integrationId']);
|
|
437
483
|
const integration = await getIntegrationInstance.execute(
|
|
438
484
|
params.integrationId,
|
|
439
|
-
user
|
|
485
|
+
await ownerOf(params.integrationId, user)
|
|
440
486
|
);
|
|
441
487
|
|
|
442
488
|
// We could perhaps augment router with dynamic options? Haven't decided yet, but here may be the place
|
|
@@ -456,7 +502,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
456
502
|
const params = checkRequiredParams(req.params, ['integrationId']);
|
|
457
503
|
const instance = await getIntegrationInstance.execute(
|
|
458
504
|
params.integrationId,
|
|
459
|
-
user
|
|
505
|
+
await ownerOf(params.integrationId, user)
|
|
460
506
|
);
|
|
461
507
|
|
|
462
508
|
if (!instance) {
|
|
@@ -494,16 +540,21 @@ function setEntityRoutes(router, authenticateUser, useCases) {
|
|
|
494
540
|
getModule,
|
|
495
541
|
getEntityOptionsById,
|
|
496
542
|
refreshEntityOptions,
|
|
497
|
-
|
|
543
|
+
authorizeEntityForUser,
|
|
544
|
+
createEntityFromCredential,
|
|
498
545
|
} = useCases;
|
|
499
546
|
|
|
500
547
|
router.route('/api/authorize').get(
|
|
501
548
|
catchAsyncError(async (req, res) => {
|
|
502
549
|
const user = await authenticateUser.execute(req);
|
|
503
|
-
const userId = user.getId();
|
|
504
550
|
const params = checkRequiredParams(req.query, ['entityType']);
|
|
551
|
+
// The owner POST stores under, so GET and POST describe one connection.
|
|
552
|
+
const { ownerId } = authorizeEntityForUser.resolveOwnership({
|
|
553
|
+
user,
|
|
554
|
+
entityType: params.entityType,
|
|
555
|
+
});
|
|
505
556
|
const module = await getModuleInstanceFromType.execute(
|
|
506
|
-
|
|
557
|
+
ownerId,
|
|
507
558
|
params.entityType,
|
|
508
559
|
{ state: req.query.state }
|
|
509
560
|
);
|
|
@@ -522,11 +573,15 @@ function setEntityRoutes(router, authenticateUser, useCases) {
|
|
|
522
573
|
router.route('/api/authorize').post(
|
|
523
574
|
catchAsyncError(async (req, res) => {
|
|
524
575
|
const user = await authenticateUser.execute(req);
|
|
525
|
-
const userId = user.getId();
|
|
526
576
|
const params = checkRequiredParams(req.body, [
|
|
527
577
|
'entityType',
|
|
528
578
|
'data',
|
|
529
579
|
]);
|
|
580
|
+
const ownership = authorizeEntityForUser.resolveOwnership({
|
|
581
|
+
user,
|
|
582
|
+
entityType: params.entityType,
|
|
583
|
+
});
|
|
584
|
+
const userId = ownership.ownerId;
|
|
530
585
|
|
|
531
586
|
const dataKeys =
|
|
532
587
|
params.data && typeof params.data === 'object'
|
|
@@ -538,11 +593,13 @@ function setEntityRoutes(router, authenticateUser, useCases) {
|
|
|
538
593
|
dataKeys,
|
|
539
594
|
});
|
|
540
595
|
|
|
541
|
-
const entityDetails =
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
596
|
+
const { result: entityDetails } =
|
|
597
|
+
await authorizeEntityForUser.execute({
|
|
598
|
+
user,
|
|
599
|
+
entityType: params.entityType,
|
|
600
|
+
data: params.data,
|
|
601
|
+
ownership,
|
|
602
|
+
});
|
|
546
603
|
|
|
547
604
|
log.info('Entity authorized', {
|
|
548
605
|
eventName: 'frigg.integrations.authorized',
|
|
@@ -559,35 +616,20 @@ function setEntityRoutes(router, authenticateUser, useCases) {
|
|
|
559
616
|
router.route('/api/entity').post(
|
|
560
617
|
catchAsyncError(async (req, res) => {
|
|
561
618
|
const user = await authenticateUser.execute(req);
|
|
562
|
-
const userId = user.getId();
|
|
563
619
|
const params = checkRequiredParams(req.body, [
|
|
564
620
|
'entityType',
|
|
565
621
|
'data',
|
|
566
622
|
]);
|
|
567
623
|
checkRequiredParams(req.body.data, ['credential_id']);
|
|
568
624
|
|
|
569
|
-
//
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
throw Boom.badRequest('Invalid credential ID');
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
const module = await getModuleInstanceFromType.execute(
|
|
580
|
-
userId,
|
|
581
|
-
params.entityType
|
|
582
|
-
);
|
|
583
|
-
const entityDetails = await module.getEntityDetails(
|
|
584
|
-
module.api,
|
|
585
|
-
null,
|
|
586
|
-
null,
|
|
587
|
-
userId
|
|
625
|
+
// The owner follows the entity type's scope, as /api/authorize.
|
|
626
|
+
res.json(
|
|
627
|
+
await createEntityFromCredential.execute({
|
|
628
|
+
user,
|
|
629
|
+
entityType: params.entityType,
|
|
630
|
+
credentialId: params.data.credential_id,
|
|
631
|
+
})
|
|
588
632
|
);
|
|
589
|
-
|
|
590
|
-
res.json(await module.findOrCreateEntity(entityDetails));
|
|
591
633
|
})
|
|
592
634
|
);
|
|
593
635
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The error `findIntegrationById` throws for an id with no row, and the one
|
|
3
|
+
* predicate that recognises it. The adapters build the error here and the
|
|
4
|
+
* use cases match it here, so the message is written once and a missing
|
|
5
|
+
* integration can be told from any other failure without widening a regex.
|
|
6
|
+
*/
|
|
7
|
+
const MESSAGE = /^Integration with id .+ not found$/;
|
|
8
|
+
|
|
9
|
+
const integrationNotFoundError = (id) =>
|
|
10
|
+
new Error(`Integration with id ${id} not found`);
|
|
11
|
+
|
|
12
|
+
const isIntegrationNotFound = (error) => MESSAGE.test(error?.message || '');
|
|
13
|
+
|
|
14
|
+
module.exports = { integrationNotFoundError, isIntegrationNotFound };
|
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
IntegrationRepositoryInterface,
|
|
15
15
|
} = require('./integration-repository-interface');
|
|
16
16
|
const { validateConfigPatch } = require('./config-patch-shared');
|
|
17
|
+
const { integrationNotFoundError } = require('./integration-not-found');
|
|
17
18
|
|
|
18
19
|
class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
|
|
19
20
|
constructor() {
|
|
@@ -28,6 +29,28 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
|
|
|
28
29
|
return records.map((doc) => this._mapIntegration(doc));
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Find integrations matching any of the owner/organization pairs, in one
|
|
34
|
+
* query. `organizationId: null` matches rows with no organization (a raw
|
|
35
|
+
* MongoDB `null` also matches a missing field). The caller (a use case)
|
|
36
|
+
* decides which pairs; this only runs them.
|
|
37
|
+
*
|
|
38
|
+
* @param {Array<{userId: string, organizationId: (string|null)}>} clauses
|
|
39
|
+
* @returns {Promise<Array>}
|
|
40
|
+
*/
|
|
41
|
+
async findIntegrationsByOwnership(clauses) {
|
|
42
|
+
if (!Array.isArray(clauses) || clauses.length === 0) return [];
|
|
43
|
+
// Drained: a list can hold more than the first cursor batch (~101).
|
|
44
|
+
const records = await findManyDrained(this.prisma, 'Integration', {
|
|
45
|
+
$or: clauses.map(({ userId, organizationId }) => ({
|
|
46
|
+
userId: toObjectId(userId),
|
|
47
|
+
organizationId:
|
|
48
|
+
organizationId === null ? null : toObjectId(organizationId),
|
|
49
|
+
})),
|
|
50
|
+
});
|
|
51
|
+
return records.map((doc) => this._mapIntegration(doc));
|
|
52
|
+
}
|
|
53
|
+
|
|
31
54
|
async findIntegrations({ type, status } = {}) {
|
|
32
55
|
const filter = {};
|
|
33
56
|
if (type) {
|
|
@@ -68,11 +91,11 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
|
|
|
68
91
|
async findIntegrationById(id) {
|
|
69
92
|
const objectId = toObjectId(id);
|
|
70
93
|
if (!objectId) {
|
|
71
|
-
throw
|
|
94
|
+
throw integrationNotFoundError(id);
|
|
72
95
|
}
|
|
73
96
|
const doc = await findOne(this.prisma, 'Integration', { _id: objectId });
|
|
74
97
|
if (!doc) {
|
|
75
|
-
throw
|
|
98
|
+
throw integrationNotFoundError(id);
|
|
76
99
|
}
|
|
77
100
|
return this._mapIntegration(doc);
|
|
78
101
|
}
|
|
@@ -132,10 +155,18 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
|
|
|
132
155
|
return true;
|
|
133
156
|
}
|
|
134
157
|
|
|
135
|
-
|
|
158
|
+
/**
|
|
159
|
+
* @param {Array<string>} entities - entity ids
|
|
160
|
+
* @param {string} userId - owner (the person, or the organization's user)
|
|
161
|
+
* @param {Object} config - Integration configuration
|
|
162
|
+
* @param {{organizationId?: (string|null)}} [tenancy] - the organization the
|
|
163
|
+
* integration is created in (ADR-056 §3.6)
|
|
164
|
+
*/
|
|
165
|
+
async createIntegration(entities, userId, config, tenancy = {}) {
|
|
136
166
|
const now = new Date();
|
|
137
167
|
const document = {
|
|
138
168
|
userId: toObjectId(userId) || null,
|
|
169
|
+
organizationId: toObjectId(tenancy.organizationId) || null,
|
|
139
170
|
config,
|
|
140
171
|
version: '0.0.0',
|
|
141
172
|
status: 'IN_CREATION',
|
|
@@ -298,6 +329,7 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
|
|
|
298
329
|
id: fromObjectId(doc?._id),
|
|
299
330
|
entitiesIds: (doc?.entityIds || []).map((value) => fromObjectId(value)),
|
|
300
331
|
userId: fromObjectId(doc?.userId),
|
|
332
|
+
organizationId: fromObjectId(doc?.organizationId) ?? null,
|
|
301
333
|
config: doc?.config ?? null,
|
|
302
334
|
version: doc?.version ?? null,
|
|
303
335
|
status: doc?.status ?? null,
|
|
@@ -10,6 +10,20 @@
|
|
|
10
10
|
* @abstract
|
|
11
11
|
*/
|
|
12
12
|
class IntegrationRepositoryInterface {
|
|
13
|
+
/**
|
|
14
|
+
* Find integrations matching any of the owner/organization pairs, in one
|
|
15
|
+
* query (ADR-056 §3.6). `organizationId: null` matches rows with no
|
|
16
|
+
* organization. Which pairs is decided by the caller
|
|
17
|
+
* (modules/entity-scope.js), not here.
|
|
18
|
+
*
|
|
19
|
+
* @param {Array<{userId: string|number, organizationId: (string|number|null)}>} clauses
|
|
20
|
+
* @returns {Promise<Array>} Array of integration objects
|
|
21
|
+
* @abstract
|
|
22
|
+
*/
|
|
23
|
+
async findIntegrationsByOwnership(clauses) {
|
|
24
|
+
throw new Error('Method findIntegrationsByOwnership must be implemented by subclass');
|
|
25
|
+
}
|
|
26
|
+
|
|
13
27
|
/**
|
|
14
28
|
* Find all integrations for a user
|
|
15
29
|
*
|
|
@@ -123,10 +137,12 @@ class IntegrationRepositoryInterface {
|
|
|
123
137
|
* @param {Array<string|number>} entities - Array of entity IDs
|
|
124
138
|
* @param {string|number} userId - User ID
|
|
125
139
|
* @param {Object} config - Integration configuration
|
|
140
|
+
* @param {{organizationId?: (string|number|null)}} [tenancy] - the organization the
|
|
141
|
+
* integration is created in (ADR-056 §3.6)
|
|
126
142
|
* @returns {Promise<Object>} Created integration object
|
|
127
143
|
* @abstract
|
|
128
144
|
*/
|
|
129
|
-
async createIntegration(entities, userId, config) {
|
|
145
|
+
async createIntegration(entities, userId, config, tenancy) {
|
|
130
146
|
throw new Error('Method createIntegration must be implemented by subclass');
|
|
131
147
|
}
|
|
132
148
|
|