@friggframework/core 2.0.0--canary.685.022c3c3.0 → 2.0.0--canary.682.d8626c4.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 -23
- package/README.md +1 -1
- package/credential/use-cases/credential-operations-for-user.js +25 -110
- package/credential/use-cases/reauthorize-credential-for-user.js +8 -15
- package/handlers/app-definition-loader.js +0 -9
- package/index.js +3 -4
- package/integrations/index.js +5 -0
- package/integrations/integration-router.js +31 -35
- package/integrations/repositories/integration-repository-documentdb.js +2 -1
- package/integrations/use-cases/integration-operations-for-user.js +6 -16
- package/integrations/utils/map-integration-dto.js +44 -1
- package/management-api/management-api-config.js +4 -70
- package/management-api/openapi/components.js +0 -1
- package/management-api/openapi/v2-operations.js +3 -6
- package/management-api/v2/dependencies.js +0 -2
- package/management-api/v2/dto.js +0 -3
- package/management-api/v2/handlers/credentials.js +2 -6
- package/modules/repositories/module-repository-documentdb.js +8 -6
- package/modules/repositories/module-repository-mongo.js +1 -2
- package/modules/repositories/module-repository-postgres.js +1 -2
- package/modules/repositories/module-repository.js +1 -2
- package/modules/scope-config.js +1 -23
- package/modules/scope-policy.js +3 -34
- package/modules/scope-resolution.js +0 -11
- package/modules/scope-visibility.js +8 -63
- package/modules/use-cases/authorize-entity-for-user.js +2 -4
- package/modules/use-cases/create-entity-from-credential.js +113 -0
- package/modules/use-cases/entity-operations-for-user.js +4 -9
- package/modules/use-cases/execute-entity-proxy-request.js +18 -50
- package/modules/use-cases/get-entity-module-for-user.js +2 -9
- package/modules/use-cases/get-entity-options-by-id.js +4 -1
- package/modules/use-cases/get-module.js +4 -1
- package/modules/use-cases/multi-step-authorization.js +1 -4
- package/modules/use-cases/refresh-entity-options.js +4 -1
- package/modules/use-cases/test-module-auth.js +4 -1
- package/package.json +5 -5
- package/types/modules/index.d.ts +9 -41
- package/types/user/index.d.ts +0 -7
- package/user/use-cases/authenticate-user.js +1 -7
- package/user/user.js +1 -24
- package/modules/repositories/entity-not-found.js +0 -18
- package/modules/use-cases/manage-access.js +0 -21
package/CLAUDE.md
CHANGED
|
@@ -502,29 +502,6 @@ The single source of the max receive count is
|
|
|
502
502
|
devtools integration builder uses it for the queue's `RedrivePolicy` and sets
|
|
503
503
|
it as `FRIGG_QUEUE_MAX_RECEIVE_COUNT` on the queue worker function.
|
|
504
504
|
|
|
505
|
-
### Management API v2 notes (`/management-api`)
|
|
506
|
-
|
|
507
|
-
- Handlers in `management-api/v2/handlers/` call use cases only; dependencies are wired in
|
|
508
|
-
`management-api/v2/dependencies.js`. Errors are `{ error: { code, message, details? } }`.
|
|
509
|
-
- `appDefinition.managementApi` is resolved by `resolveManagementApiConfig`, which `loadAppDefinition`
|
|
510
|
-
also calls so a bad value stops the app at load. Numeric proxy limits fall back to their defaults;
|
|
511
|
-
`proxy.scopes` (ADR-056 §3.5, default `['individual']`) never does: an unknown scope, `'global'`, a
|
|
512
|
-
non-list, an empty list or a repeated scope throws `ManagementApiConfigError`
|
|
513
|
-
(`MANAGEMENT_API_CONFIG_INVALID`).
|
|
514
|
-
- **Entity proxy** (`ExecuteEntityProxyRequest`): visibility in the active organization comes from
|
|
515
|
-
`GetEntityModuleForUser` (404), then `assertProxyable` reads the same `{ scope, access }` description
|
|
516
|
-
the v2 lists return: `grantsUse(access)` (`use` or `manage`, from `modules/scope-visibility.js`) or 403
|
|
517
|
-
`PROXY_NOT_ALLOWED` `reason: 'access'`, and the scope must be in `proxy.scopes` or 403 `reason: 'scope'`.
|
|
518
|
-
Do not add a second ownership rule for the proxy; change the description instead.
|
|
519
|
-
- **Not visible is 404, and so is missing.** Module repositories throw `entityNotFoundError(id)`
|
|
520
|
-
(`modules/repositories/entity-not-found.js`) for a missing row and use cases match it with
|
|
521
|
-
`isEntityNotFound`. A 500 for a missing id next to a 404 for another tenant's id tells a caller which
|
|
522
|
-
ids exist. A test double that resolves `findEntityById` to `null` hides this: no adapter does that.
|
|
523
|
-
- **Tenant isolation on real PostgreSQL**: `management-api/v2/tenant-isolation.postgres.test.js`, run with
|
|
524
|
-
`FRIGG_TEST_POSTGRES_URL=<a database it may wipe>`. It applies the migrations, truncates every table
|
|
525
|
-
before each test, and drives real adopter-JWT auth through the real router and Prisma repositories.
|
|
526
|
-
Without the variable it is skipped (and says so), not passed. See `docs/guides/entity-scopes.md`.
|
|
527
|
-
|
|
528
505
|
### 8. Error Handling (`/errors`)
|
|
529
506
|
|
|
530
507
|
**Purpose**: Standardized error types with proper HTTP semantics.
|
package/README.md
CHANGED
|
@@ -393,7 +393,7 @@ value that is not a three-part JWT is not offered to the verifier at all; it fal
|
|
|
393
393
|
| `clockToleranceSec` | `30` | Skew allowed on `exp` and `nbf`. |
|
|
394
394
|
| `userIdClaim` | `sub` | App user id (required in the token). |
|
|
395
395
|
| `orgIdClaim` | `org_id` | App organization id (optional). |
|
|
396
|
-
| `roleClaim` | `org_role` | Exposed as `user.getOrgRole()` for the request, and stored as the `orgRole` of a person's membership the first time they join that organization (`MEMBER` when the token names none).
|
|
396
|
+
| `roleClaim` | `org_role` | Exposed as `user.getOrgRole()` for the request, and stored as the `orgRole` of a person's membership the first time they join that organization (`MEMBER` when the token names none). |
|
|
397
397
|
|
|
398
398
|
Rules worth knowing:
|
|
399
399
|
|
|
@@ -1,28 +1,10 @@
|
|
|
1
1
|
const Boom = require('@hapi/boom');
|
|
2
|
-
const {
|
|
3
|
-
createScopePolicy,
|
|
4
|
-
describeRecord,
|
|
5
|
-
visibilityClauses,
|
|
6
|
-
matchesListFilters,
|
|
7
|
-
SCOPES,
|
|
8
|
-
} = require('../../modules/entity-scope');
|
|
9
|
-
const { assertCanManage } = require('../../modules/use-cases/manage-access');
|
|
2
|
+
const { userOwns } = require('../../modules/use-cases/get-entity-module-for-user');
|
|
10
3
|
|
|
11
4
|
/**
|
|
12
|
-
* Management API v2 credential operations
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* - its `type` and `entityIds` come from the entities that use it that the
|
|
16
|
-
* caller may see, so the same person's account connected in two
|
|
17
|
-
* organizations (one credential, two entities) lists in each organization
|
|
18
|
-
* with that organization's entity only;
|
|
19
|
-
* - who it belongs to is its OWNER (`credential.userId`), never the entities:
|
|
20
|
-
* a credential an individual entity and an organization entity both point at
|
|
21
|
-
* (possible only through legacy data) is described by whoever owns the
|
|
22
|
-
* credential, so the answer cannot change with the order the entities load;
|
|
23
|
-
* - a credential whose entities are all out of sight (another organization's)
|
|
24
|
-
* is a 404, and one with no entities at all (its entity was deleted) is
|
|
25
|
-
* visible to its owner only, as it was before scoping.
|
|
5
|
+
* Management API v2 credential operations. A credential has no module name
|
|
6
|
+
* of its own; its `type` and the entities that use it come from the
|
|
7
|
+
* caller's entities. A missing or foreign credential is a 404.
|
|
26
8
|
*/
|
|
27
9
|
|
|
28
10
|
const credentialIdOf = (entity) => {
|
|
@@ -55,82 +37,31 @@ function withEntityUsage(credentials, entities) {
|
|
|
55
37
|
});
|
|
56
38
|
}
|
|
57
39
|
|
|
58
|
-
/**
|
|
59
|
-
* `{scope, owner, organizationId, access}` of a credential for this caller, or
|
|
60
|
-
* null when its owner is neither the caller nor the active organization. Built
|
|
61
|
-
* by `describeRecord` over the row a connection by this owner has: the
|
|
62
|
-
* organization of a visible entity, else the one a new entity would get.
|
|
63
|
-
*/
|
|
64
|
-
function credentialTenancy(credential, visibleEntities, ctx) {
|
|
65
|
-
const entityOrg = visibleEntities.find(
|
|
66
|
-
(entity) => entity.organizationId !== undefined && entity.organizationId !== null
|
|
67
|
-
)?.organizationId;
|
|
68
|
-
const organizationId =
|
|
69
|
-
entityOrg ?? (ctx.individualAcross === 'all' ? null : ctx.activeOrgId);
|
|
70
|
-
return describeRecord({ userId: credential.userId, organizationId }, ctx, 'entity');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** The entities of `entities` the caller may see, each with its `tenancy`. */
|
|
74
|
-
function visibleOf(entities, ctx) {
|
|
75
|
-
const visible = [];
|
|
76
|
-
for (const entity of entities || []) {
|
|
77
|
-
const tenancy = describeRecord(entity, ctx, 'entity');
|
|
78
|
-
if (tenancy) visible.push({ ...entity, tenancy });
|
|
79
|
-
}
|
|
80
|
-
return visible;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* The caller's credentials with their type, the entities of them they may see
|
|
85
|
-
* and `tenancy`. Credentials with no entity at all are listed too, for the
|
|
86
|
-
* person and the active organization that own them.
|
|
87
|
-
*/
|
|
40
|
+
/** The caller's credentials with their type and entity usage. */
|
|
88
41
|
class ListCredentialsForUser {
|
|
89
|
-
constructor({ credentialRepository, moduleRepository
|
|
42
|
+
constructor({ credentialRepository, moduleRepository }) {
|
|
90
43
|
this.credentialRepository = credentialRepository;
|
|
91
44
|
this.moduleRepository = moduleRepository;
|
|
92
|
-
this.scopePolicy = scopePolicy || createScopePolicy();
|
|
93
45
|
}
|
|
94
46
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
this.moduleRepository.findEntitiesByOwnership(visibilityClauses(ctx, 'entity')),
|
|
101
|
-
Promise.all(owners.map((id) => this.credentialRepository.findCredentialsByUserId(id))),
|
|
102
|
-
// Every entity of those owners, in any organization: only to tell a credential
|
|
103
|
-
// with no entity at all from one used elsewhere.
|
|
104
|
-
Promise.all(owners.map((id) => this.moduleRepository.findEntitiesByUserId(id))),
|
|
47
|
+
async execute(user) {
|
|
48
|
+
const userId = user.getId();
|
|
49
|
+
const [credentials, entities] = await Promise.all([
|
|
50
|
+
this.credentialRepository.findCredentialsByUserId(userId),
|
|
51
|
+
this.moduleRepository.findEntitiesByUserId(userId),
|
|
105
52
|
]);
|
|
106
|
-
|
|
107
|
-
const usedAnywhere = new Set(owned.flat().map(credentialIdOf).filter(Boolean));
|
|
108
|
-
const listed = [];
|
|
109
|
-
for (const credential of perOwner.flat().filter(Boolean)) {
|
|
110
|
-
const id = String(credential.id);
|
|
111
|
-
const referencing = visible.filter((entity) => credentialIdOf(entity) === id);
|
|
112
|
-
if (referencing.length === 0 && usedAnywhere.has(id)) continue;
|
|
113
|
-
const tenancy = credentialTenancy(credential, referencing, ctx);
|
|
114
|
-
if (!tenancy || !matchesListFilters(tenancy, filters)) continue;
|
|
115
|
-
listed.push({ ...withEntityUsage([credential], referencing)[0], tenancy });
|
|
116
|
-
}
|
|
117
|
-
return listed;
|
|
53
|
+
return withEntityUsage(credentials || [], entities);
|
|
118
54
|
}
|
|
119
55
|
}
|
|
120
56
|
|
|
121
|
-
/** One credential the caller
|
|
57
|
+
/** One credential the caller owns, with its type and entity usage. */
|
|
122
58
|
class GetCredentialForUserV2 {
|
|
123
|
-
constructor({ credentialRepository, moduleRepository
|
|
59
|
+
constructor({ credentialRepository, moduleRepository }) {
|
|
124
60
|
this.credentialRepository = credentialRepository;
|
|
125
61
|
this.moduleRepository = moduleRepository;
|
|
126
|
-
this.scopePolicy = scopePolicy || createScopePolicy();
|
|
127
62
|
}
|
|
128
63
|
|
|
129
64
|
async execute(credentialId, user) {
|
|
130
|
-
const notFound = () =>
|
|
131
|
-
Boom.notFound(`Credential ${credentialId} not found`, {
|
|
132
|
-
code: 'CREDENTIAL_NOT_FOUND',
|
|
133
|
-
});
|
|
134
65
|
let credential = null;
|
|
135
66
|
try {
|
|
136
67
|
credential = await this.credentialRepository.findCredentialById(
|
|
@@ -141,29 +72,22 @@ class GetCredentialForUserV2 {
|
|
|
141
72
|
throw error;
|
|
142
73
|
}
|
|
143
74
|
}
|
|
144
|
-
if (!credential
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
return { ...withEntityUsage([credential], visible)[0], tenancy };
|
|
75
|
+
if (!credential || !userOwns(user, credential.userId)) {
|
|
76
|
+
throw Boom.notFound(`Credential ${credentialId} not found`, {
|
|
77
|
+
code: 'CREDENTIAL_NOT_FOUND',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const entities = await this.moduleRepository.findEntitiesByUserId(
|
|
81
|
+
credential.userId
|
|
82
|
+
);
|
|
83
|
+
return withEntityUsage([credential], entities)[0];
|
|
154
84
|
}
|
|
155
85
|
}
|
|
156
86
|
|
|
157
87
|
/**
|
|
158
|
-
* Deletes a credential the caller
|
|
159
|
-
*
|
|
88
|
+
* Deletes a credential the caller owns. The entities that used it stay, with
|
|
89
|
+
* their credential unset, so they show as needing re-authorization rather
|
|
160
90
|
* than disappearing from integrations.
|
|
161
|
-
*
|
|
162
|
-
* Refuses with 409 `CREDENTIAL_SHARED` while an entity the caller cannot see
|
|
163
|
-
* still uses it: one credential row serves the same provider account in every
|
|
164
|
-
* organization, and deleting it from one would break the other. The message
|
|
165
|
-
* says nothing about the other connections (not even how many): remove the
|
|
166
|
-
* entities that use it, and it goes with the last one.
|
|
167
91
|
*/
|
|
168
92
|
class DeleteCredentialForUser {
|
|
169
93
|
constructor({ credentialRepository, moduleRepository, getCredentialForUser }) {
|
|
@@ -177,15 +101,6 @@ class DeleteCredentialForUser {
|
|
|
177
101
|
credentialId,
|
|
178
102
|
user
|
|
179
103
|
);
|
|
180
|
-
assertCanManage(credential.tenancy.access, `credential ${credential.id}`);
|
|
181
|
-
const usedBy =
|
|
182
|
-
(await this.moduleRepository.findEntities({ credentialId: credential.id })) || [];
|
|
183
|
-
if (usedBy.length > credential.entityIds.length) {
|
|
184
|
-
throw Boom.conflict(
|
|
185
|
-
`Credential ${credential.id} is still in use by other connections and cannot be deleted from here`,
|
|
186
|
-
{ code: 'CREDENTIAL_SHARED' }
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
104
|
for (const entityId of credential.entityIds) {
|
|
190
105
|
await this.moduleRepository.unsetCredential(entityId);
|
|
191
106
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const Boom = require('@hapi/boom');
|
|
2
|
-
const { assertCanManage } = require('../../modules/use-cases/manage-access');
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Re-authorization of a credential the caller owns (Management API v2). It
|
|
@@ -15,13 +14,7 @@ async function credentialEntityType(getCredentialForUser, credentialId, user) {
|
|
|
15
14
|
{ code: 'CREDENTIAL_TYPE_UNKNOWN' }
|
|
16
15
|
);
|
|
17
16
|
}
|
|
18
|
-
return {
|
|
19
|
-
entityType: credential.type,
|
|
20
|
-
credentialId: String(credential.id),
|
|
21
|
-
// A re-authorization keeps the scope the connection already has.
|
|
22
|
-
fixedScope: credential.tenancy?.scope,
|
|
23
|
-
access: credential.tenancy?.access,
|
|
24
|
-
};
|
|
17
|
+
return { entityType: credential.type, credentialId: String(credential.id) };
|
|
25
18
|
}
|
|
26
19
|
|
|
27
20
|
/** GET /api/v2/credentials/:credentialId/reauthorize */
|
|
@@ -32,12 +25,14 @@ class GetCredentialReauthorization {
|
|
|
32
25
|
}
|
|
33
26
|
|
|
34
27
|
async execute(credentialId, user, { step = 1, sessionId, state } = {}) {
|
|
35
|
-
const
|
|
36
|
-
|
|
28
|
+
const binding = await credentialEntityType(
|
|
29
|
+
this.getCredentialForUser,
|
|
30
|
+
credentialId,
|
|
31
|
+
user
|
|
32
|
+
);
|
|
37
33
|
return this.getAuthorizationStep.execute({
|
|
38
34
|
user,
|
|
39
|
-
|
|
40
|
-
credentialId: boundCredentialId,
|
|
35
|
+
...binding,
|
|
41
36
|
step,
|
|
42
37
|
sessionId,
|
|
43
38
|
state,
|
|
@@ -53,13 +48,11 @@ class ReauthorizeCredentialForUser {
|
|
|
53
48
|
}
|
|
54
49
|
|
|
55
50
|
async execute(credentialId, user, { data, step = 1, sessionId } = {}) {
|
|
56
|
-
const
|
|
51
|
+
const binding = await credentialEntityType(
|
|
57
52
|
this.getCredentialForUser,
|
|
58
53
|
credentialId,
|
|
59
54
|
user
|
|
60
55
|
);
|
|
61
|
-
// A credential an organization owns: the organization's admins only.
|
|
62
|
-
assertCanManage(access, `credential ${credentialId}`);
|
|
63
56
|
return this.submitAuthorizationStep.execute({
|
|
64
57
|
user,
|
|
65
58
|
...binding,
|
|
@@ -12,9 +12,6 @@ const {
|
|
|
12
12
|
getEntityTypeDefinitions,
|
|
13
13
|
} = require('../modules/entity-type-registry');
|
|
14
14
|
const { resolveOwnershipConfig } = require('../modules/entity-scope');
|
|
15
|
-
const {
|
|
16
|
-
resolveManagementApiConfig,
|
|
17
|
-
} = require('../management-api/management-api-config');
|
|
18
15
|
|
|
19
16
|
// Handlers without a database never load the encryption registry, so the
|
|
20
17
|
// credential leaf keys are registered for redaction here too.
|
|
@@ -42,7 +39,6 @@ function registerCredentialLogKeys(appDefinition, moduleDefinitions) {
|
|
|
42
39
|
* @throws {Error} Throws error if index.js file cannot be found in the backend directory.
|
|
43
40
|
* @throws {Error} Throws error if user.authModes.adopterJwt is enabled without a usable user.jwtConfig.
|
|
44
41
|
* @throws {OwnershipConfigError} Throws if an ownership or scope setting is invalid (ADR-056 §3.1).
|
|
45
|
-
* @throws {ManagementApiConfigError} Throws if `managementApi.proxy.scopes` is invalid (ADR-056 §3.5).
|
|
46
42
|
* @example
|
|
47
43
|
* const { integrations, userConfig, telemetry } = loadAppDefinition();
|
|
48
44
|
* console.log(`Found ${integrations.length} integrations`);
|
|
@@ -88,11 +84,6 @@ function loadAppDefinition() {
|
|
|
88
84
|
// individual entity stops the app at load.
|
|
89
85
|
const ownership = resolveOwnershipConfig(appDefinition, entityTypeRegistry);
|
|
90
86
|
|
|
91
|
-
// And for the Management API: `managementApi.proxy.scopes` decides whose
|
|
92
|
-
// credentials the entity proxy may act through (ADR-056 §3.5), so an
|
|
93
|
-
// unknown scope stops the app here rather than being read as a default.
|
|
94
|
-
resolveManagementApiConfig(managementApi);
|
|
95
|
-
|
|
96
87
|
registerCredentialLogKeys(appDefinition, moduleDefinitions);
|
|
97
88
|
|
|
98
89
|
// Degrade consistently: an invalid telemetry block must never take down a
|
package/index.js
CHANGED
|
@@ -71,6 +71,7 @@ const {
|
|
|
71
71
|
Options,
|
|
72
72
|
createIntegrationRouter,
|
|
73
73
|
checkRequiredParams,
|
|
74
|
+
getModulesDefinitionFromIntegrationClasses,
|
|
74
75
|
LoadIntegrationContextUseCase,
|
|
75
76
|
} = require('./integrations/index');
|
|
76
77
|
const {
|
|
@@ -112,9 +113,6 @@ const {
|
|
|
112
113
|
listBindingScopes,
|
|
113
114
|
OwnershipConfigError,
|
|
114
115
|
} = require('./modules/index');
|
|
115
|
-
const {
|
|
116
|
-
ManagementApiConfigError,
|
|
117
|
-
} = require('./management-api/management-api-config');
|
|
118
116
|
const application = require('./application');
|
|
119
117
|
const utils = require('./utils');
|
|
120
118
|
|
|
@@ -169,6 +167,8 @@ module.exports = {
|
|
|
169
167
|
Options,
|
|
170
168
|
checkRequiredParams,
|
|
171
169
|
createIntegrationRouter,
|
|
170
|
+
// Deprecated (ADR-059): use getEntityTypeRegistry.
|
|
171
|
+
getModulesDefinitionFromIntegrationClasses,
|
|
172
172
|
LoadIntegrationContextUseCase,
|
|
173
173
|
CreateProcess,
|
|
174
174
|
UpdateProcessState,
|
|
@@ -229,7 +229,6 @@ module.exports = {
|
|
|
229
229
|
resolveScope,
|
|
230
230
|
listBindingScopes,
|
|
231
231
|
OwnershipConfigError,
|
|
232
|
-
ManagementApiConfigError,
|
|
233
232
|
// queues
|
|
234
233
|
QueuerUtil,
|
|
235
234
|
|
package/integrations/index.js
CHANGED
|
@@ -4,6 +4,9 @@ const {
|
|
|
4
4
|
createIntegrationRouter,
|
|
5
5
|
checkRequiredParams,
|
|
6
6
|
} = require('./integration-router');
|
|
7
|
+
const {
|
|
8
|
+
getModulesDefinitionFromIntegrationClasses,
|
|
9
|
+
} = require('./utils/map-integration-dto');
|
|
7
10
|
const {
|
|
8
11
|
LoadIntegrationContextUseCase,
|
|
9
12
|
} = require('./use-cases/load-integration-context');
|
|
@@ -18,6 +21,8 @@ module.exports = {
|
|
|
18
21
|
Options,
|
|
19
22
|
createIntegrationRouter,
|
|
20
23
|
checkRequiredParams,
|
|
24
|
+
// Deprecated (ADR-059): a wrapper over getEntityTypeRegistry.
|
|
25
|
+
getModulesDefinitionFromIntegrationClasses,
|
|
21
26
|
LoadIntegrationContextUseCase,
|
|
22
27
|
validateExtensionBinding,
|
|
23
28
|
getExtensionRoutes,
|
|
@@ -65,6 +65,9 @@ const { createScopePolicy } = require('../modules/scope-policy');
|
|
|
65
65
|
const {
|
|
66
66
|
AuthorizeEntityForUser,
|
|
67
67
|
} = require('../modules/use-cases/authorize-entity-for-user');
|
|
68
|
+
const {
|
|
69
|
+
CreateEntityFromCredential,
|
|
70
|
+
} = require('../modules/use-cases/create-entity-from-credential');
|
|
68
71
|
const {
|
|
69
72
|
ResolveIntegrationOwnership,
|
|
70
73
|
} = require('./use-cases/resolve-integration-ownership');
|
|
@@ -199,14 +202,22 @@ function createIntegrationRouter() {
|
|
|
199
202
|
});
|
|
200
203
|
|
|
201
204
|
// The owner of what an authorization creates follows its scope (ADR-056 §3.2).
|
|
205
|
+
const processAuthorizationCallback = new ProcessAuthorizationCallback({
|
|
206
|
+
moduleRepository,
|
|
207
|
+
credentialRepository,
|
|
208
|
+
integrationRepository,
|
|
209
|
+
moduleDefinitions,
|
|
210
|
+
});
|
|
202
211
|
const authorizeEntityForUser = new AuthorizeEntityForUser({
|
|
203
212
|
scopePolicy,
|
|
204
|
-
processAuthorizationCallback
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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,
|
|
210
221
|
});
|
|
211
222
|
|
|
212
223
|
const router = express();
|
|
@@ -231,6 +242,7 @@ function createIntegrationRouter() {
|
|
|
231
242
|
getEntityOptionsById,
|
|
232
243
|
refreshEntityOptions,
|
|
233
244
|
authorizeEntityForUser,
|
|
245
|
+
createEntityFromCredential,
|
|
234
246
|
});
|
|
235
247
|
return router;
|
|
236
248
|
}
|
|
@@ -279,11 +291,9 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
279
291
|
} = useCases;
|
|
280
292
|
|
|
281
293
|
// The owner of an integration the caller may reach (404 otherwise), which is
|
|
282
|
-
// what the id-and-owner use cases below expect.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
(await getOwnedIntegrationRecord.execute(integrationId, user, options))
|
|
286
|
-
.userId;
|
|
294
|
+
// what the id-and-owner use cases below expect.
|
|
295
|
+
const ownerOf = async (integrationId, user) =>
|
|
296
|
+
(await getOwnedIntegrationRecord.execute(integrationId, user)).userId;
|
|
287
297
|
|
|
288
298
|
router.route('/api/integrations').get(
|
|
289
299
|
catchAsyncError(async (req, res) => {
|
|
@@ -335,7 +345,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
335
345
|
|
|
336
346
|
const integration = await updateIntegration.execute(
|
|
337
347
|
req.params.integrationId,
|
|
338
|
-
await ownerOf(req.params.integrationId, user
|
|
348
|
+
await ownerOf(req.params.integrationId, user),
|
|
339
349
|
params.config
|
|
340
350
|
);
|
|
341
351
|
res.json(integration);
|
|
@@ -348,7 +358,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
348
358
|
const params = checkRequiredParams(req.params, ['integrationId']);
|
|
349
359
|
await deleteIntegrationForUser.execute(
|
|
350
360
|
params.integrationId,
|
|
351
|
-
await ownerOf(params.integrationId, user
|
|
361
|
+
await ownerOf(params.integrationId, user)
|
|
352
362
|
);
|
|
353
363
|
res.status(204).json({});
|
|
354
364
|
})
|
|
@@ -529,6 +539,7 @@ function setEntityRoutes(router, authenticateUser, useCases) {
|
|
|
529
539
|
getEntityOptionsById,
|
|
530
540
|
refreshEntityOptions,
|
|
531
541
|
authorizeEntityForUser,
|
|
542
|
+
createEntityFromCredential,
|
|
532
543
|
} = useCases;
|
|
533
544
|
|
|
534
545
|
router.route('/api/authorize').get(
|
|
@@ -603,35 +614,20 @@ function setEntityRoutes(router, authenticateUser, useCases) {
|
|
|
603
614
|
router.route('/api/entity').post(
|
|
604
615
|
catchAsyncError(async (req, res) => {
|
|
605
616
|
const user = await authenticateUser.execute(req);
|
|
606
|
-
const userId = user.getId();
|
|
607
617
|
const params = checkRequiredParams(req.body, [
|
|
608
618
|
'entityType',
|
|
609
619
|
'data',
|
|
610
620
|
]);
|
|
611
621
|
checkRequiredParams(req.body.data, ['credential_id']);
|
|
612
622
|
|
|
613
|
-
//
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
throw Boom.badRequest('Invalid credential ID');
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
const module = await getModuleInstanceFromType.execute(
|
|
624
|
-
userId,
|
|
625
|
-
params.entityType
|
|
626
|
-
);
|
|
627
|
-
const entityDetails = await module.getEntityDetails(
|
|
628
|
-
module.api,
|
|
629
|
-
null,
|
|
630
|
-
null,
|
|
631
|
-
userId
|
|
623
|
+
// The owner follows the entity type's scope, as /api/authorize.
|
|
624
|
+
res.json(
|
|
625
|
+
await createEntityFromCredential.execute({
|
|
626
|
+
user,
|
|
627
|
+
entityType: params.entityType,
|
|
628
|
+
credentialId: params.data.credential_id,
|
|
629
|
+
})
|
|
632
630
|
);
|
|
633
|
-
|
|
634
|
-
res.json(await module.findOrCreateEntity(entityDetails));
|
|
635
631
|
})
|
|
636
632
|
);
|
|
637
633
|
|
|
@@ -40,7 +40,8 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
|
|
|
40
40
|
*/
|
|
41
41
|
async findIntegrationsByOwnership(clauses) {
|
|
42
42
|
if (!Array.isArray(clauses) || clauses.length === 0) return [];
|
|
43
|
-
|
|
43
|
+
// Drained: a list can hold more than the first cursor batch (~101).
|
|
44
|
+
const records = await findManyDrained(this.prisma, 'Integration', {
|
|
44
45
|
$or: clauses.map(({ userId, organizationId }) => ({
|
|
45
46
|
userId: toObjectId(userId),
|
|
46
47
|
organizationId:
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const Boom = require('@hapi/boom');
|
|
2
2
|
const { describeRecord } = require('../../modules/scope-visibility');
|
|
3
3
|
const { DEFAULT_SCOPE_POLICY } = require('../../modules/scope-policy');
|
|
4
|
-
const { assertCanManage } = require('../../modules/use-cases/manage-access');
|
|
5
4
|
const { isIntegrationNotFound } = require('../repositories/integration-not-found');
|
|
6
5
|
|
|
7
6
|
/**
|
|
@@ -33,14 +32,7 @@ class GetOwnedIntegrationRecord {
|
|
|
33
32
|
return describeRecord(record, this.scopePolicy.accessContext(user), 'integration');
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
* @param {string} integrationId
|
|
38
|
-
* @param {Object} user - authenticated User
|
|
39
|
-
* @param {{manage?: boolean}} [options] - `manage: true` for changing or
|
|
40
|
-
* deleting: a person may use an organization's integration but only an
|
|
41
|
-
* admin may change it (403 ORG_ADMIN_REQUIRED)
|
|
42
|
-
*/
|
|
43
|
-
async execute(integrationId, user, { manage = false } = {}) {
|
|
35
|
+
async execute(integrationId, user) {
|
|
44
36
|
let record = null;
|
|
45
37
|
try {
|
|
46
38
|
record = await this.integrationRepository.findIntegrationById(
|
|
@@ -55,9 +47,9 @@ class GetOwnedIntegrationRecord {
|
|
|
55
47
|
}
|
|
56
48
|
throw error;
|
|
57
49
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
50
|
+
if (!record || !this.describe(record, user)) {
|
|
51
|
+
throw notFound(integrationId);
|
|
52
|
+
}
|
|
61
53
|
return record;
|
|
62
54
|
}
|
|
63
55
|
}
|
|
@@ -146,8 +138,7 @@ class UpdateIntegrationForUser {
|
|
|
146
138
|
}
|
|
147
139
|
const record = await this.getOwnedIntegrationRecord.execute(
|
|
148
140
|
integrationId,
|
|
149
|
-
user
|
|
150
|
-
{ manage: true }
|
|
141
|
+
user
|
|
151
142
|
);
|
|
152
143
|
return this.updateIntegration.execute(record.id, record.userId, config);
|
|
153
144
|
}
|
|
@@ -163,8 +154,7 @@ class RemoveIntegrationForUser {
|
|
|
163
154
|
async execute(integrationId, user) {
|
|
164
155
|
const record = await this.getOwnedIntegrationRecord.execute(
|
|
165
156
|
integrationId,
|
|
166
|
-
user
|
|
167
|
-
{ manage: true }
|
|
157
|
+
user
|
|
168
158
|
);
|
|
169
159
|
await this.deleteIntegrationForUser.execute(record.id, record.userId);
|
|
170
160
|
}
|
|
@@ -19,5 +19,48 @@ function mapIntegrationClassToIntegrationDTO(integration) {
|
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
let deprecationLogged = false;
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
/**
|
|
25
|
+
* @deprecated Since ADR-059. Read `entityTypeRegistry` / `moduleDefinitions`
|
|
26
|
+
* from `loadAppDefinition()`, or call `getEntityTypeRegistry(appDefinition)`
|
|
27
|
+
* and read each entry's `definition`.
|
|
28
|
+
*
|
|
29
|
+
* Kept as a wrapper over the entity type registry so callers that imported it
|
|
30
|
+
* keep working. It returns the registry's definitions, so it follows the
|
|
31
|
+
* registry's rules: one definition per entity type name (the first wins when
|
|
32
|
+
* two integrations bind different definitions under one name, as
|
|
33
|
+
* ModuleFactory already picked), and, when `entities` is passed, registry-only
|
|
34
|
+
* types are included and conflicts throw `EntityRegistryError`.
|
|
35
|
+
*
|
|
36
|
+
* @param {Array<Function>} integrationClasses - integration classes
|
|
37
|
+
* @param {Object} [entities] - `appDefinition.entities` (ADR-059)
|
|
38
|
+
* @returns {Array<Object>} API module definitions
|
|
39
|
+
*/
|
|
40
|
+
function getModulesDefinitionFromIntegrationClasses(
|
|
41
|
+
integrationClasses,
|
|
42
|
+
entities
|
|
43
|
+
) {
|
|
44
|
+
// Required here, not at the top: modules/ must not load at require time
|
|
45
|
+
// of every integration use case that only wants the DTO mapper.
|
|
46
|
+
const {
|
|
47
|
+
getEntityTypeRegistry,
|
|
48
|
+
getEntityTypeDefinitions,
|
|
49
|
+
} = require('../../modules/entity-type-registry');
|
|
50
|
+
if (!deprecationLogged) {
|
|
51
|
+
deprecationLogged = true;
|
|
52
|
+
const { getLogger } = require('../../logs');
|
|
53
|
+
getLogger('frigg.integrations').warn(
|
|
54
|
+
'getModulesDefinitionFromIntegrationClasses is deprecated; use getEntityTypeRegistry (ADR-059)',
|
|
55
|
+
{ eventName: 'frigg.integrations.deprecated_module_definitions' }
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return getEntityTypeDefinitions(
|
|
59
|
+
getEntityTypeRegistry({ integrations: integrationClasses, entities })
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
mapIntegrationClassToIntegrationDTO,
|
|
65
|
+
getModulesDefinitionFromIntegrationClasses,
|
|
66
|
+
};
|