@friggframework/core 2.0.0--canary.681.d970b9e.0 → 2.0.0--canary.684.9d19f43.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/README.md +37 -1
- package/application/commands/integration-commands.js +20 -10
- package/application/index.js +6 -2
- package/credential/use-cases/credential-operations-for-user.js +110 -25
- package/credential/use-cases/reauthorize-credential-for-user.js +15 -8
- package/database/encryption/encryption-schema-registry.js +7 -0
- package/database/organization-where.js +18 -0
- package/generated/prisma-mongodb/edge.js +5 -3
- package/generated/prisma-mongodb/index-browser.js +2 -0
- package/generated/prisma-mongodb/index.d.ts +115 -2
- package/generated/prisma-mongodb/index.js +5 -3
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +15 -0
- package/generated/prisma-mongodb/wasm.js +5 -3
- package/generated/prisma-postgresql/edge.js +5 -3
- package/generated/prisma-postgresql/index-browser.js +2 -0
- package/generated/prisma-postgresql/index.d.ts +131 -2
- package/generated/prisma-postgresql/index.js +5 -3
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +15 -0
- package/generated/prisma-postgresql/wasm.js +5 -3
- package/handlers/app-definition-loader.js +25 -8
- package/handlers/backend-utils.js +31 -25
- package/handlers/routers/health.js +1 -4
- package/handlers/workers/integration-defined-workers.js +5 -2
- package/index.js +10 -2
- package/integrations/index.js +0 -4
- package/integrations/integration-router.js +101 -52
- package/integrations/repositories/integration-not-found.js +14 -0
- package/integrations/repositories/integration-repository-documentdb.js +34 -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/tests/doubles/test-integration-repository.js +12 -1
- package/integrations/use-cases/create-integration.js +55 -20
- package/integrations/use-cases/get-integrations-for-user.js +80 -40
- package/integrations/use-cases/index.js +5 -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 +1 -15
- package/management-api/management-api-config.js +3 -0
- package/management-api/openapi/components.js +47 -1
- package/management-api/openapi/v2-operations.js +17 -1
- package/management-api/v2/dependencies.js +58 -14
- 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/entity-type-registry.js +311 -0
- package/modules/index.js +14 -0
- package/modules/repositories/module-repository-documentdb.js +41 -1
- package/modules/repositories/module-repository-interface.js +19 -2
- package/modules/repositories/module-repository-mongo.js +64 -73
- package/modules/repositories/module-repository-postgres.js +63 -73
- package/modules/scope-config.js +177 -0
- package/modules/scope-policy.js +111 -0
- package/modules/scope-resolution.js +288 -0
- package/modules/scope-visibility.js +212 -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/entity-operations-for-user.js +50 -13
- package/modules/use-cases/entity-types.js +62 -14
- package/modules/use-cases/execute-entity-proxy-request.js +15 -0
- package/modules/use-cases/get-entities-for-user.js +16 -5
- package/modules/use-cases/get-entity-module-for-user.js +23 -3
- package/modules/use-cases/get-entity-options-by-id.js +5 -2
- package/modules/use-cases/get-module.js +5 -2
- package/modules/use-cases/manage-access.js +21 -0
- package/modules/use-cases/multi-step-authorization.js +39 -12
- package/modules/use-cases/process-authorization-callback.js +52 -18
- package/modules/use-cases/refresh-entity-options.js +5 -2
- package/modules/use-cases/test-module-auth.js +5 -2
- package/package.json +5 -5
- package/prisma-mongodb/schema.prisma +15 -0
- package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
- package/prisma-postgresql/schema.prisma +15 -0
- package/types/index.d.ts +1 -0
- package/types/modules/index.d.ts +145 -0
- package/types/user/index.d.ts +7 -0
- package/user/use-cases/authenticate-user.js +7 -1
- package/user/use-cases/get-user-from-x-frigg-headers.js +19 -5
- package/user/user.js +24 -1
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). A token with no role is a member for permissions too: it cannot create or change organization-level entities or integrations; `user.orgAdminRoles` (default `['ADMIN']`) names the roles that can. |
|
|
397
397
|
|
|
398
398
|
Rules worth knowing:
|
|
399
399
|
|
|
@@ -421,6 +421,42 @@ Rules worth knowing:
|
|
|
421
421
|
- Two first logins for the same new person or organization, even in different organizations, both
|
|
422
422
|
succeed: a create that loses to a unique constraint finds the winner's row.
|
|
423
423
|
|
|
424
|
+
#### Entity scopes and ownership (ADR-056 §3)
|
|
425
|
+
|
|
426
|
+
Who owns an entity (a connected account) or an integration follows its **scope**, not `user.primary`:
|
|
427
|
+
|
|
428
|
+
| Scope | Owned by | Created with |
|
|
429
|
+
|---|---|---|
|
|
430
|
+
| `individual` | the person (`individualUser`) | the active organization (`organizationId`), or none under `individualAcross: 'all'` |
|
|
431
|
+
| `organization` | the active organization's user | that organization |
|
|
432
|
+
| `global` | nobody (the app owner) | none; refused by the tenant API (admin routes only) |
|
|
433
|
+
|
|
434
|
+
The scope of a new connection is the first of: the request (`scope` on `POST /api/v2/authorize`, only from
|
|
435
|
+
the deciding level's `allow` list), the integration binding (`Definition.modules[key].scope` or
|
|
436
|
+
`Definition.entities[key].scope`), `Definition.ownership.scope`, the entity type
|
|
437
|
+
(`appDefinition.entities[name].scope`), `appDefinition.ownership.scope`, then `organization` for an
|
|
438
|
+
organization-primary app and `individual` otherwise. A setting is `'organization'` or
|
|
439
|
+
`{ default: 'individual', allow: ['individual', 'organization'] }`.
|
|
440
|
+
|
|
441
|
+
```javascript
|
|
442
|
+
const appDefinition = {
|
|
443
|
+
user: { primary: 'individual', authModes: { adopterJwt: true } },
|
|
444
|
+
ownership: { scope: 'individual', individualAcross: 'organization' }, // the defaults
|
|
445
|
+
entities: {
|
|
446
|
+
google: { definition: google, scope: 'individual' },
|
|
447
|
+
xero: { definition: xero, scope: { default: 'organization', allow: ['organization', 'individual'] } },
|
|
448
|
+
},
|
|
449
|
+
integrations: [Sync], // Sync.Definition.ownership = { owner: 'derive' }
|
|
450
|
+
};
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
`GET /api/v2/entities` and `/integrations` return, for the active organization, its own
|
|
454
|
+
rows and the caller's, each with `scope`, `owner`, `organizationId` and `access`, and take `?scope=` and
|
|
455
|
+
`?owner=me|org`. An individual entity is only visible and usable in the organization it was connected in
|
|
456
|
+
(or in all of them, with `individualAcross: 'all'`). Rows written before this existed keep working: see
|
|
457
|
+
[Entity scopes](../../docs/guides/entity-scopes.md), which also covers the schema (`organizationId` on
|
|
458
|
+
`Entity` and `Integration`, no `scope` column) and what is deferred.
|
|
459
|
+
|
|
424
460
|
### 8. Lambda Utilities (`/lambda`)
|
|
425
461
|
|
|
426
462
|
AWS Lambda-specific utilities and helpers.
|
|
@@ -18,7 +18,7 @@ const {
|
|
|
18
18
|
ListIntegrationsByEntityExternalIdUseCase,
|
|
19
19
|
} = require('../../integrations/use-cases/list-integrations-by-entity-external-id');
|
|
20
20
|
const {
|
|
21
|
-
|
|
21
|
+
GetIntegrationsByUserId,
|
|
22
22
|
} = require('../../integrations/use-cases/get-integrations-for-user');
|
|
23
23
|
const {
|
|
24
24
|
CreateIntegration,
|
|
@@ -30,8 +30,9 @@ const {
|
|
|
30
30
|
PatchIntegrationConfig,
|
|
31
31
|
} = require('../../integrations/use-cases/patch-integration-config');
|
|
32
32
|
const {
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
getEntityTypeRegistry,
|
|
34
|
+
getEntityTypeDefinitions,
|
|
35
|
+
} = require('../../modules/entity-type-registry');
|
|
35
36
|
|
|
36
37
|
const ERROR_CODE_MAP = {
|
|
37
38
|
ENTITY_NOT_FOUND: 401,
|
|
@@ -51,7 +52,16 @@ function mapErrorToResponse(error) {
|
|
|
51
52
|
};
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
/**
|
|
56
|
+
* @param {Object} [params]
|
|
57
|
+
* @param {Function} [params.integrationClass]
|
|
58
|
+
* @param {Array<object>} [params.moduleDefinitions] - the app's offered module
|
|
59
|
+
* definitions (`loadAppDefinition().moduleDefinitions`). Integration code
|
|
60
|
+
* calls this from a constructor with no app definition in hand, so when it
|
|
61
|
+
* is omitted the commands hydrate with this integration's own modules only
|
|
62
|
+
* (a registry built from `{ integrations: [integrationClass] }`).
|
|
63
|
+
*/
|
|
64
|
+
function createIntegrationCommands({ integrationClass, moduleDefinitions } = {}) {
|
|
55
65
|
// Always use Frigg's default repositories and use cases
|
|
56
66
|
const integrationRepository = createIntegrationRepository();
|
|
57
67
|
|
|
@@ -108,13 +118,13 @@ function createIntegrationCommands({ integrationClass } = {}) {
|
|
|
108
118
|
|
|
109
119
|
const moduleRepository = createModuleRepository();
|
|
110
120
|
|
|
111
|
-
const moduleDefinitions = getModulesDefinitionFromIntegrationClasses([
|
|
112
|
-
integrationClass,
|
|
113
|
-
]);
|
|
114
|
-
|
|
115
121
|
const moduleFactory = new ModuleFactory({
|
|
116
122
|
moduleRepository,
|
|
117
|
-
moduleDefinitions
|
|
123
|
+
moduleDefinitions:
|
|
124
|
+
moduleDefinitions ??
|
|
125
|
+
getEntityTypeDefinitions(
|
|
126
|
+
getEntityTypeRegistry({ integrations: [integrationClass] })
|
|
127
|
+
),
|
|
118
128
|
});
|
|
119
129
|
|
|
120
130
|
const loadIntegrationContextUseCase = new LoadIntegrationContextUseCase({
|
|
@@ -142,7 +152,7 @@ function createIntegrationCommands({ integrationClass } = {}) {
|
|
|
142
152
|
moduleRepository,
|
|
143
153
|
});
|
|
144
154
|
|
|
145
|
-
const getIntegrationsForUserUseCase = new
|
|
155
|
+
const getIntegrationsForUserUseCase = new GetIntegrationsByUserId({
|
|
146
156
|
integrationRepository,
|
|
147
157
|
integrationClasses: [integrationClass],
|
|
148
158
|
moduleFactory,
|
package/application/index.js
CHANGED
|
@@ -17,6 +17,7 @@ const { createUsageCommands } = require('./commands/usage-commands');
|
|
|
17
17
|
*
|
|
18
18
|
* @param {Object} params
|
|
19
19
|
* @param {Object} params.integrationClass - Integration class (required)
|
|
20
|
+
* @param {Array<object>} [params.moduleDefinitions] - the app's offered module definitions; defaults to the integration's own modules
|
|
20
21
|
* @returns {Object} Unified commands object with all CRUD operations
|
|
21
22
|
*
|
|
22
23
|
* @example
|
|
@@ -24,9 +25,12 @@ const { createUsageCommands } = require('./commands/usage-commands');
|
|
|
24
25
|
* const user = await commands.createUser({ username: 'user@example.com' });
|
|
25
26
|
* const credential = await commands.createCredential({ userId: user.id, ... });
|
|
26
27
|
*/
|
|
27
|
-
function createFriggCommands({ integrationClass }) {
|
|
28
|
+
function createFriggCommands({ integrationClass, moduleDefinitions }) {
|
|
28
29
|
// All commands use Frigg's default repositories and use cases
|
|
29
|
-
const integrationCommands = createIntegrationCommands({
|
|
30
|
+
const integrationCommands = createIntegrationCommands({
|
|
31
|
+
integrationClass,
|
|
32
|
+
moduleDefinitions,
|
|
33
|
+
});
|
|
30
34
|
|
|
31
35
|
const userCommands = createUserCommands();
|
|
32
36
|
|
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
const Boom = require('@hapi/boom');
|
|
2
|
-
const {
|
|
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');
|
|
3
10
|
|
|
4
11
|
/**
|
|
5
|
-
* Management API v2 credential operations. A credential
|
|
6
|
-
*
|
|
7
|
-
*
|
|
12
|
+
* Management API v2 credential operations (ADR-056 §3.4, §3.6). A credential
|
|
13
|
+
* has no module name and no organization of its own:
|
|
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.
|
|
8
26
|
*/
|
|
9
27
|
|
|
10
28
|
const credentialIdOf = (entity) => {
|
|
@@ -37,31 +55,82 @@ function withEntityUsage(credentials, entities) {
|
|
|
37
55
|
});
|
|
38
56
|
}
|
|
39
57
|
|
|
40
|
-
/**
|
|
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
|
+
*/
|
|
41
88
|
class ListCredentialsForUser {
|
|
42
|
-
constructor({ credentialRepository, moduleRepository }) {
|
|
89
|
+
constructor({ credentialRepository, moduleRepository, scopePolicy }) {
|
|
43
90
|
this.credentialRepository = credentialRepository;
|
|
44
91
|
this.moduleRepository = moduleRepository;
|
|
92
|
+
this.scopePolicy = scopePolicy || createScopePolicy();
|
|
45
93
|
}
|
|
46
94
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
95
|
+
/** @param {{scope?: string, owner?: string}} [filters] */
|
|
96
|
+
async execute(user, filters = {}) {
|
|
97
|
+
const ctx = this.scopePolicy.accessContext(user);
|
|
98
|
+
const owners = [...new Set([ctx.callerId, ctx.activeOrgId].filter(Boolean))];
|
|
99
|
+
const [found, perOwner, owned] = await Promise.all([
|
|
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))),
|
|
52
105
|
]);
|
|
53
|
-
|
|
106
|
+
const visible = visibleOf(found, ctx);
|
|
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;
|
|
54
118
|
}
|
|
55
119
|
}
|
|
56
120
|
|
|
57
|
-
/** One credential the caller
|
|
121
|
+
/** One credential the caller may see: see the header. 404 otherwise. */
|
|
58
122
|
class GetCredentialForUserV2 {
|
|
59
|
-
constructor({ credentialRepository, moduleRepository }) {
|
|
123
|
+
constructor({ credentialRepository, moduleRepository, scopePolicy }) {
|
|
60
124
|
this.credentialRepository = credentialRepository;
|
|
61
125
|
this.moduleRepository = moduleRepository;
|
|
126
|
+
this.scopePolicy = scopePolicy || createScopePolicy();
|
|
62
127
|
}
|
|
63
128
|
|
|
64
129
|
async execute(credentialId, user) {
|
|
130
|
+
const notFound = () =>
|
|
131
|
+
Boom.notFound(`Credential ${credentialId} not found`, {
|
|
132
|
+
code: 'CREDENTIAL_NOT_FOUND',
|
|
133
|
+
});
|
|
65
134
|
let credential = null;
|
|
66
135
|
try {
|
|
67
136
|
credential = await this.credentialRepository.findCredentialById(
|
|
@@ -72,22 +141,29 @@ class GetCredentialForUserV2 {
|
|
|
72
141
|
throw error;
|
|
73
142
|
}
|
|
74
143
|
}
|
|
75
|
-
if (!credential
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
);
|
|
83
|
-
|
|
144
|
+
if (!credential) throw notFound();
|
|
145
|
+
const ctx = this.scopePolicy.accessContext(user);
|
|
146
|
+
|
|
147
|
+
const referencing =
|
|
148
|
+
(await this.moduleRepository.findEntities({ credentialId: credential.id })) || [];
|
|
149
|
+
const visible = visibleOf(referencing, ctx);
|
|
150
|
+
if (referencing.length > 0 && visible.length === 0) throw notFound();
|
|
151
|
+
const tenancy = credentialTenancy(credential, visible, ctx);
|
|
152
|
+
if (!tenancy) throw notFound();
|
|
153
|
+
return { ...withEntityUsage([credential], visible)[0], tenancy };
|
|
84
154
|
}
|
|
85
155
|
}
|
|
86
156
|
|
|
87
157
|
/**
|
|
88
|
-
* Deletes a credential the caller
|
|
89
|
-
* their credential unset, so they show as needing re-authorization rather
|
|
158
|
+
* Deletes a credential the caller may manage. The entities that used it stay,
|
|
159
|
+
* with their credential unset, so they show as needing re-authorization rather
|
|
90
160
|
* 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.
|
|
91
167
|
*/
|
|
92
168
|
class DeleteCredentialForUser {
|
|
93
169
|
constructor({ credentialRepository, moduleRepository, getCredentialForUser }) {
|
|
@@ -101,6 +177,15 @@ class DeleteCredentialForUser {
|
|
|
101
177
|
credentialId,
|
|
102
178
|
user
|
|
103
179
|
);
|
|
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
|
+
}
|
|
104
189
|
for (const entityId of credential.entityIds) {
|
|
105
190
|
await this.moduleRepository.unsetCredential(entityId);
|
|
106
191
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const Boom = require('@hapi/boom');
|
|
2
|
+
const { assertCanManage } = require('../../modules/use-cases/manage-access');
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Re-authorization of a credential the caller owns (Management API v2). It
|
|
@@ -14,7 +15,13 @@ async function credentialEntityType(getCredentialForUser, credentialId, user) {
|
|
|
14
15
|
{ code: 'CREDENTIAL_TYPE_UNKNOWN' }
|
|
15
16
|
);
|
|
16
17
|
}
|
|
17
|
-
return {
|
|
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
|
+
};
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
/** GET /api/v2/credentials/:credentialId/reauthorize */
|
|
@@ -25,14 +32,12 @@ class GetCredentialReauthorization {
|
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
async execute(credentialId, user, { step = 1, sessionId, state } = {}) {
|
|
28
|
-
const
|
|
29
|
-
this.getCredentialForUser,
|
|
30
|
-
credentialId,
|
|
31
|
-
user
|
|
32
|
-
);
|
|
35
|
+
const { entityType, credentialId: boundCredentialId } =
|
|
36
|
+
await credentialEntityType(this.getCredentialForUser, credentialId, user);
|
|
33
37
|
return this.getAuthorizationStep.execute({
|
|
34
38
|
user,
|
|
35
|
-
|
|
39
|
+
entityType,
|
|
40
|
+
credentialId: boundCredentialId,
|
|
36
41
|
step,
|
|
37
42
|
sessionId,
|
|
38
43
|
state,
|
|
@@ -48,11 +53,13 @@ class ReauthorizeCredentialForUser {
|
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
async execute(credentialId, user, { data, step = 1, sessionId } = {}) {
|
|
51
|
-
const binding = await credentialEntityType(
|
|
56
|
+
const { access, ...binding } = await credentialEntityType(
|
|
52
57
|
this.getCredentialForUser,
|
|
53
58
|
credentialId,
|
|
54
59
|
user
|
|
55
60
|
);
|
|
61
|
+
// A credential an organization owns: the organization's admins only.
|
|
62
|
+
assertCanManage(access, `credential ${credentialId}`);
|
|
56
63
|
return this.submitAuthorizationStep.execute({
|
|
57
64
|
user,
|
|
58
65
|
...binding,
|
|
@@ -180,6 +180,13 @@ function loadModuleEncryptionSchemas(integrations) {
|
|
|
180
180
|
return;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
// NOTE: this path does not exist ('../integrations' resolves to
|
|
184
|
+
// database/integrations), so today this function throws MODULE_NOT_FOUND,
|
|
185
|
+
// loadCustomEncryptionSchema swallows it, and module credentialFields are
|
|
186
|
+
// effectively NOT registered. Fixing it changes which fields are encrypted
|
|
187
|
+
// and needs a decrypt that tolerates plaintext, so it ships separately:
|
|
188
|
+
// branch claude/internal-mcp-03b-module-credential-encryption. Left as is
|
|
189
|
+
// here on purpose (ADR-059 must not change what gets encrypted).
|
|
183
190
|
const { getModulesDefinitionFromIntegrationClasses } = require('../integrations/utils/map-integration-dto');
|
|
184
191
|
|
|
185
192
|
const moduleDefinitions = getModulesDefinitionFromIntegrationClasses(integrations);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Prisma `where` fragment for an `organizationId` on MongoDB (ADR-056
|
|
3
|
+
* §3.6), shared by the entity and integration adapters.
|
|
4
|
+
*
|
|
5
|
+
* Prisma on MongoDB: `null` matches only an explicit null and `isSet: false`
|
|
6
|
+
* only a missing field. A row written before the column existed is the common
|
|
7
|
+
* "no organization" case, so `null` here means both.
|
|
8
|
+
*/
|
|
9
|
+
function organizationWhere(organizationId) {
|
|
10
|
+
if (organizationId === null) {
|
|
11
|
+
return {
|
|
12
|
+
OR: [{ organizationId: null }, { organizationId: { isSet: false } }],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
return { organizationId };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { organizationWhere };
|