@friggframework/core 2.0.0--canary.663.8de669c.0 → 2.0.0--canary.690.3fe2b27.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 +11 -0
- package/CLAUDE.md +28 -0
- package/README.md +105 -0
- package/application/commands/integration-commands.js +20 -10
- package/application/commands/scheduler-commands.js +11 -7
- package/application/commands/user-commands.js +17 -5
- package/application/index.js +6 -2
- package/core/Worker.js +31 -7
- package/core/create-handler.js +9 -0
- package/credential/repositories/credential-repository-documentdb.js +34 -0
- package/credential/repositories/credential-repository-interface.js +15 -0
- package/credential/repositories/credential-repository-mongo.js +23 -0
- package/credential/repositories/credential-repository-postgres.js +28 -0
- package/credential/use-cases/credential-operations-for-user.js +202 -0
- package/credential/use-cases/reauthorize-credential-for-user.js +73 -0
- package/database/config.js +48 -24
- package/database/documentdb-utils.js +6 -1
- package/database/encryption/README.md +2 -0
- package/database/encryption/encryption-config.js +10 -31
- package/database/encryption/encryption-schema-registry.js +121 -24
- package/database/encryption/field-encryption-service.js +69 -5
- package/database/encryption/test-support/fake-ciphertext.js +15 -0
- 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 +43 -4
- package/generated/prisma-mongodb/index-browser.js +40 -1
- package/generated/prisma-mongodb/index.d.ts +4761 -133
- package/generated/prisma-mongodb/index.js +43 -4
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +92 -0
- package/generated/prisma-mongodb/wasm.js +43 -4
- package/generated/prisma-postgresql/edge.js +43 -4
- package/generated/prisma-postgresql/index-browser.js +40 -1
- package/generated/prisma-postgresql/index.d.ts +4961 -28
- package/generated/prisma-postgresql/index.js +43 -4
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +87 -0
- package/generated/prisma-postgresql/wasm.js +43 -4
- package/handlers/app-definition-loader.js +128 -19
- package/handlers/app-handler-helpers.js +25 -3
- package/handlers/backend-utils.js +31 -25
- package/handlers/middleware/admin-auth.js +14 -1
- package/handlers/routers/auth.js +46 -10
- package/handlers/routers/health.js +13 -4
- package/handlers/routers/integration-defined-routers.js +144 -92
- package/handlers/routers/integration-webhook-routers.js +102 -64
- package/handlers/workers/integration-defined-workers.js +5 -2
- package/index.js +65 -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 +1 -0
- package/integrations/integration-router.js +131 -96
- 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 +256 -0
- package/integrations/use-cases/resolve-integration-ownership.js +177 -0
- package/integrations/utils/map-integration-dto.js +43 -14
- package/management-api/create-management-api-router.js +49 -0
- package/management-api/error-response.js +116 -0
- package/management-api/management-api-config.js +151 -0
- package/management-api/meta-router.js +76 -0
- package/management-api/openapi/build-openapi.js +144 -0
- package/management-api/openapi/components.js +415 -0
- package/management-api/openapi/v1-operations.js +83 -0
- package/management-api/openapi/v2-operations.js +264 -0
- package/management-api/route-registry.js +411 -0
- package/management-api/test-support/http-client.js +39 -0
- package/management-api/use-cases/get-management-api-meta.js +57 -0
- package/management-api/v1-deprecation.js +120 -0
- package/management-api/v2/create-v2-router.js +85 -0
- package/management-api/v2/dependencies.js +281 -0
- package/management-api/v2/dto.js +143 -0
- package/management-api/v2/handlers/authorization.js +94 -0
- package/management-api/v2/handlers/credentials.js +33 -0
- package/management-api/v2/handlers/entities.js +77 -0
- package/management-api/v2/handlers/integrations.js +128 -0
- package/management-api/v2/handlers/proxy.js +34 -0
- package/management-api/v2/request.js +85 -0
- package/management-api/v2/test-support/fake-dependencies.js +103 -0
- package/modules/domain/authorization-session.js +144 -0
- package/modules/entity-scope.js +15 -0
- package/modules/entity-type-registry.js +311 -0
- package/modules/index.js +14 -0
- package/modules/module-factory.js +12 -0
- package/modules/module.js +53 -2
- package/modules/proxy/proxy-bases.js +169 -0
- package/modules/proxy/proxy-errors.js +31 -0
- package/modules/proxy/proxy-path.js +183 -0
- package/modules/proxy/proxy-policy.js +277 -0
- package/modules/repositories/authorization-session-repository-documentdb.js +112 -0
- package/modules/repositories/authorization-session-repository-factory.js +31 -0
- package/modules/repositories/authorization-session-repository-interface.js +47 -0
- package/modules/repositories/authorization-session-repository-prisma.js +87 -0
- 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/requester/proxy-request.js +245 -0
- package/modules/requester/requester.js +35 -5
- 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/in-memory-authorization-session-repository.js +45 -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 +162 -0
- package/modules/use-cases/entity-types.js +139 -0
- package/modules/use-cases/execute-entity-proxy-request.js +331 -0
- package/modules/use-cases/get-authorization-requirements.js +100 -0
- package/modules/use-cases/get-entities-for-user.js +26 -13
- package/modules/use-cases/get-entity-module-for-user.js +113 -0
- 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 +329 -0
- 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 +92 -0
- package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +28 -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 +87 -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 +2 -0
- package/types/modules/index.d.ts +214 -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
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scope resolution and ownership on create (ADR-056 §3.1, §3.2, §3.6).
|
|
3
|
+
*
|
|
4
|
+
* No I/O: everything here is a function of what it is handed (settings, a
|
|
5
|
+
* request's identity context, stored rows). This file also holds the shared
|
|
6
|
+
* constants and `OwnershipConfigError`, so `scope-config.js` (which validates
|
|
7
|
+
* app definitions) and `scope-visibility.js` can import them without a cycle.
|
|
8
|
+
*
|
|
9
|
+
* There is no `scope` column. The owner's user type already says it: no owner
|
|
10
|
+
* is `global`, the active organization's user is `organization`, the caller's
|
|
11
|
+
* individual user is `individual` (docs/guides/entity-scopes.md).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const Boom = require('@hapi/boom');
|
|
15
|
+
const { BaseError } = require('../errors/base-error');
|
|
16
|
+
const { isPresent, sameId } = require('../user/membership');
|
|
17
|
+
|
|
18
|
+
const SCOPES = Object.freeze({
|
|
19
|
+
INDIVIDUAL: 'individual',
|
|
20
|
+
ORGANIZATION: 'organization',
|
|
21
|
+
GLOBAL: 'global',
|
|
22
|
+
});
|
|
23
|
+
const SCOPE_VALUES = Object.freeze(Object.values(SCOPES));
|
|
24
|
+
const INDIVIDUAL_ACROSS_VALUES = Object.freeze(['organization', 'all']);
|
|
25
|
+
const OWNER_RULES = Object.freeze(['derive', 'individual', 'organization']);
|
|
26
|
+
const ACCESS = Object.freeze({ MANAGE: 'manage', USE: 'use', VIEW: 'view' });
|
|
27
|
+
|
|
28
|
+
/** The organization roles that may manage organization-level things unless `user.orgAdminRoles` says. */
|
|
29
|
+
const DEFAULT_ORG_ADMIN_ROLES = Object.freeze(['ADMIN']);
|
|
30
|
+
|
|
31
|
+
/** Raised at app-definition load for an invalid ownership setting. */
|
|
32
|
+
class OwnershipConfigError extends BaseError {
|
|
33
|
+
constructor(message, options) {
|
|
34
|
+
super(message, options);
|
|
35
|
+
this.code = 'OWNERSHIP_CONFIG_INVALID';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const isPlainObject = (value) =>
|
|
40
|
+
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
41
|
+
|
|
42
|
+
const configError = (where, message) =>
|
|
43
|
+
new OwnershipConfigError(`${where}: ${message}`);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* One level's setting: a scope (`'organization'`) or a default plus what a
|
|
47
|
+
* request may choose (`{ default, allow }`). Returns `{ default, allow }`, or
|
|
48
|
+
* null when the level sets nothing. Idempotent on its own output.
|
|
49
|
+
*/
|
|
50
|
+
function parseScopeSetting(setting, where) {
|
|
51
|
+
if (setting === undefined || setting === null) return null;
|
|
52
|
+
if (typeof setting === 'string') {
|
|
53
|
+
if (!SCOPE_VALUES.includes(setting)) {
|
|
54
|
+
throw configError(
|
|
55
|
+
where,
|
|
56
|
+
`scope '${setting}' is not one of ${SCOPE_VALUES.join(', ')}`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return { default: setting, allow: [setting] };
|
|
60
|
+
}
|
|
61
|
+
if (!isPlainObject(setting)) {
|
|
62
|
+
throw configError(where, 'scope must be a scope name or { default, allow }');
|
|
63
|
+
}
|
|
64
|
+
const unknown = Object.keys(setting).filter(
|
|
65
|
+
(key) => key !== 'default' && key !== 'allow'
|
|
66
|
+
);
|
|
67
|
+
if (unknown.length) {
|
|
68
|
+
throw configError(where, `unknown scope setting(s): ${unknown.join(', ')}`);
|
|
69
|
+
}
|
|
70
|
+
const { allow, default: defaultScope } = setting;
|
|
71
|
+
if (allow !== undefined) {
|
|
72
|
+
if (
|
|
73
|
+
!Array.isArray(allow) ||
|
|
74
|
+
allow.length === 0 ||
|
|
75
|
+
allow.some((value) => !SCOPE_VALUES.includes(value)) ||
|
|
76
|
+
new Set(allow).size !== allow.length
|
|
77
|
+
) {
|
|
78
|
+
throw configError(
|
|
79
|
+
where,
|
|
80
|
+
`scope.allow must list one or more of ${SCOPE_VALUES.join(', ')} once each`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (defaultScope !== undefined && !SCOPE_VALUES.includes(defaultScope)) {
|
|
85
|
+
throw configError(
|
|
86
|
+
where,
|
|
87
|
+
`scope.default '${defaultScope}' is not one of ${SCOPE_VALUES.join(', ')}`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (defaultScope === undefined && (allow === undefined || allow.length !== 1)) {
|
|
91
|
+
throw configError(where, 'scope needs a default (or an allow list of exactly one)');
|
|
92
|
+
}
|
|
93
|
+
const resolvedDefault = defaultScope ?? allow[0];
|
|
94
|
+
const resolvedAllow = allow ?? [resolvedDefault];
|
|
95
|
+
if (!resolvedAllow.includes(resolvedDefault)) {
|
|
96
|
+
throw configError(
|
|
97
|
+
where,
|
|
98
|
+
`scope.default '${resolvedDefault}' must be in scope.allow`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return { default: resolvedDefault, allow: [...resolvedAllow] };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The level-6 default: `organization` for an organization-primary app. */
|
|
105
|
+
const defaultScopeFor = (primary) =>
|
|
106
|
+
primary === 'organization' ? SCOPES.ORGANIZATION : SCOPES.INDIVIDUAL;
|
|
107
|
+
|
|
108
|
+
const LEVELS = ['binding', 'integration', 'entityType', 'app'];
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The first level that sets a scope (ADR-056 §3.1), each overriding those
|
|
112
|
+
* below it; nothing intersects. Returns `{ default, allow, source }`.
|
|
113
|
+
*/
|
|
114
|
+
function decideLevel({ binding, integration, entityType, app, primary } = {}) {
|
|
115
|
+
const settings = { binding, integration, entityType, app };
|
|
116
|
+
for (const source of LEVELS) {
|
|
117
|
+
const parsed = parseScopeSetting(settings[source], `${source} scope`);
|
|
118
|
+
if (parsed) return { ...parsed, source };
|
|
119
|
+
}
|
|
120
|
+
const fallback = defaultScopeFor(primary);
|
|
121
|
+
return { default: fallback, allow: [fallback], source: 'default' };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* What a request may choose, from the deciding level's `allow` list only. A
|
|
126
|
+
* tenant request is never given `global`; a scope outside the list is refused.
|
|
127
|
+
* Both are the same refusal (403 SCOPE_NOT_ALLOWED): the caller asked for
|
|
128
|
+
* something this app does not offer.
|
|
129
|
+
*/
|
|
130
|
+
function applyRequest(decided, request) {
|
|
131
|
+
const result = {
|
|
132
|
+
scope: decided.default,
|
|
133
|
+
source: decided.source,
|
|
134
|
+
decidedBy: decided.source,
|
|
135
|
+
allow: decided.allow,
|
|
136
|
+
};
|
|
137
|
+
if (request === undefined || request === null) return result;
|
|
138
|
+
if (!SCOPE_VALUES.includes(request)) {
|
|
139
|
+
throw Boom.badRequest(
|
|
140
|
+
`scope must be one of ${SCOPE_VALUES.filter((s) => s !== SCOPES.GLOBAL).join(', ')}`,
|
|
141
|
+
{ code: 'SCOPE_INVALID', details: { field: 'scope' } }
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (request === SCOPES.GLOBAL) {
|
|
145
|
+
throw Boom.forbidden(
|
|
146
|
+
'global entities are created by the app owner through admin routes, not by a tenant request',
|
|
147
|
+
{ code: 'SCOPE_NOT_ALLOWED' }
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
if (!decided.allow.includes(request)) {
|
|
151
|
+
throw Boom.forbidden(
|
|
152
|
+
`scope '${request}' is not allowed here; allowed: ${decided.allow.join(', ')}`,
|
|
153
|
+
{
|
|
154
|
+
code: 'SCOPE_NOT_ALLOWED',
|
|
155
|
+
details: { field: 'scope', allowed: decided.allow },
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return { ...result, scope: request, source: 'request' };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The scope a new connection gets: decideLevel then applyRequest.
|
|
164
|
+
*
|
|
165
|
+
* @param {Object} levels
|
|
166
|
+
* @param {string} [levels.request] - `scope` on the request
|
|
167
|
+
* @param {*} [levels.binding] - `use(module, { scope })` / `Definition.entities[key].scope`
|
|
168
|
+
* @param {*} [levels.integration] - `Definition.ownership.scope`
|
|
169
|
+
* @param {*} [levels.entityType] - registry `settings.scope` (ADR-059)
|
|
170
|
+
* @param {*} [levels.app] - `appDefinition.ownership.scope`
|
|
171
|
+
* @param {string} [levels.primary] - `user.primary`
|
|
172
|
+
* @returns {{scope: string, source: string, decidedBy: string, allow: string[]}}
|
|
173
|
+
*/
|
|
174
|
+
function resolveScope({ request, ...levels } = {}) {
|
|
175
|
+
return applyRequest(decideLevel(levels), request);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const MISSING_INDIVIDUAL =
|
|
179
|
+
'this request has no individual user; send x-frigg-appuserid (or sign in with a token that names the person)';
|
|
180
|
+
const MISSING_ORGANIZATION =
|
|
181
|
+
'this request has no active organization; send x-frigg-apporgid (or sign in with a token that names the organization)';
|
|
182
|
+
|
|
183
|
+
const missing = (message, field) =>
|
|
184
|
+
Boom.badRequest(message, { code: 'IDENTITY_REQUIRED', details: { field } });
|
|
185
|
+
|
|
186
|
+
const adminRequired = (what) =>
|
|
187
|
+
Boom.forbidden(`only an organization admin can ${what}`, {
|
|
188
|
+
code: 'ORG_ADMIN_REQUIRED',
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The owner and organization for a new entity of `scope` (§3.2):
|
|
193
|
+
*
|
|
194
|
+
* individual -> the person; the active organization (null under 'all')
|
|
195
|
+
* organization -> the active organization's user, in that organization
|
|
196
|
+
* global -> refused here: the tenant API never creates global entities
|
|
197
|
+
*
|
|
198
|
+
* `ctx` is an access context (scope-visibility.accessContext).
|
|
199
|
+
*/
|
|
200
|
+
function resolveOwnerForScope(scope, ctx) {
|
|
201
|
+
if (scope === SCOPES.INDIVIDUAL) {
|
|
202
|
+
if (!ctx.callerId) throw missing(MISSING_INDIVIDUAL, 'x-frigg-appuserid');
|
|
203
|
+
return {
|
|
204
|
+
scope,
|
|
205
|
+
ownerId: ctx.callerId,
|
|
206
|
+
organizationId: ctx.individualAcross === 'all' ? null : ctx.activeOrgId,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
if (scope === SCOPES.ORGANIZATION) {
|
|
210
|
+
if (!ctx.activeOrgId) throw missing(MISSING_ORGANIZATION, 'x-frigg-apporgid');
|
|
211
|
+
if (!ctx.canManageOrg) throw adminRequired('connect an organization-level account');
|
|
212
|
+
return { scope, ownerId: ctx.activeOrgId, organizationId: ctx.activeOrgId };
|
|
213
|
+
}
|
|
214
|
+
throw Boom.forbidden(
|
|
215
|
+
'global entities are created by the app owner through admin routes, not through the tenant API',
|
|
216
|
+
{ code: 'SCOPE_NOT_ALLOWED' }
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The owner kind of an integration (§3 "Integration ownership"): the
|
|
222
|
+
* organization when every entity it uses is organization-scoped, otherwise the
|
|
223
|
+
* person, unless `Definition.ownership.owner` says. ResolveIntegrationOwnership
|
|
224
|
+
* then refuses an integration whose entities are not all of that one scope.
|
|
225
|
+
*/
|
|
226
|
+
function integrationOwnerKind(entityScopes, rule = 'derive') {
|
|
227
|
+
if (rule === 'individual' || rule === 'organization') return rule;
|
|
228
|
+
return entityScopes.length > 0 &&
|
|
229
|
+
entityScopes.every((scope) => scope === SCOPES.ORGANIZATION)
|
|
230
|
+
? SCOPES.ORGANIZATION
|
|
231
|
+
: SCOPES.INDIVIDUAL;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The owner and organization of a new integration of owner kind `kind`
|
|
236
|
+
* ('individual' | 'organization').
|
|
237
|
+
*/
|
|
238
|
+
function resolveIntegrationOwner(kind, ctx) {
|
|
239
|
+
if (kind === SCOPES.ORGANIZATION) {
|
|
240
|
+
if (!ctx.activeOrgId) throw missing(MISSING_ORGANIZATION, 'x-frigg-apporgid');
|
|
241
|
+
if (!ctx.canManageOrg) throw adminRequired('create an organization-level integration');
|
|
242
|
+
return { ownerId: ctx.activeOrgId, organizationId: ctx.activeOrgId };
|
|
243
|
+
}
|
|
244
|
+
if (!ctx.callerId) throw missing(MISSING_INDIVIDUAL, 'x-frigg-appuserid');
|
|
245
|
+
return { ownerId: ctx.callerId, organizationId: ctx.activeOrgId };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The entity an authorization should reuse. The same provider account in two
|
|
250
|
+
* organizations is two entities (§3.6), so an exact organization match wins.
|
|
251
|
+
* A legacy row (organizationId null, written before scoping) is reused when
|
|
252
|
+
* the visibility rule would show it here: an organization-owned one always
|
|
253
|
+
* (the owner is unambiguous), an individual one only in the person's legacy
|
|
254
|
+
* organization.
|
|
255
|
+
*/
|
|
256
|
+
function pickExistingEntity(candidates, { scope, organizationId, legacyOrgId }) {
|
|
257
|
+
const rows = candidates || [];
|
|
258
|
+
const unscoped = (row) => !isPresent(row.organizationId);
|
|
259
|
+
const exact = rows.find((row) =>
|
|
260
|
+
isPresent(organizationId)
|
|
261
|
+
? sameId(row.organizationId, organizationId)
|
|
262
|
+
: unscoped(row)
|
|
263
|
+
);
|
|
264
|
+
if (exact) return exact;
|
|
265
|
+
const legacyApplies =
|
|
266
|
+
scope === SCOPES.ORGANIZATION || sameId(legacyOrgId, organizationId);
|
|
267
|
+
return legacyApplies ? rows.find(unscoped) ?? null : null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
module.exports = {
|
|
271
|
+
SCOPES,
|
|
272
|
+
SCOPE_VALUES,
|
|
273
|
+
INDIVIDUAL_ACROSS_VALUES,
|
|
274
|
+
OWNER_RULES,
|
|
275
|
+
ACCESS,
|
|
276
|
+
DEFAULT_ORG_ADMIN_ROLES,
|
|
277
|
+
OwnershipConfigError,
|
|
278
|
+
configError,
|
|
279
|
+
isPlainObject,
|
|
280
|
+
parseScopeSetting,
|
|
281
|
+
decideLevel,
|
|
282
|
+
applyRequest,
|
|
283
|
+
resolveScope,
|
|
284
|
+
resolveOwnerForScope,
|
|
285
|
+
integrationOwnerKind,
|
|
286
|
+
resolveIntegrationOwner,
|
|
287
|
+
pickExistingEntity,
|
|
288
|
+
};
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which stored rows a caller may see and use (ADR-056 §3.4, §3.6).
|
|
3
|
+
*
|
|
4
|
+
* No I/O. One clause list (`ctx.clauses`) is what a repository is asked for and
|
|
5
|
+
* what `describeRecord` checks every returned row against, so a list and an
|
|
6
|
+
* access check cannot drift apart. The clauses are built once per request, when
|
|
7
|
+
* the access context is.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { isPresent, idString, sameId } = require('../user/membership');
|
|
11
|
+
const { AUTH_SOURCES } = require('../user/user');
|
|
12
|
+
const { SCOPES, ACCESS, DEFAULT_ORG_ADMIN_ROLES } = require('./scope-resolution');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One owner/organization pair a repository ORs into a single query.
|
|
16
|
+
* `userId` is the owner's user id, and for an organization-owned row that is
|
|
17
|
+
* the ORGANIZATION user's id, not a person's. `organizationId: null` means "has
|
|
18
|
+
* no organization" (unset or null); otherwise the id of the organization.
|
|
19
|
+
*
|
|
20
|
+
* @typedef {Object} OwnershipClause
|
|
21
|
+
* @property {string} userId
|
|
22
|
+
* @property {(string|null)} organizationId
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Whether the caller's own rows with no organization belong to the context this
|
|
27
|
+
* request acts in: always when it acts in no organization (today's behaviour),
|
|
28
|
+
* always for an entity under `individualAcross: 'all'`, and otherwise only in
|
|
29
|
+
* the person's legacy organization.
|
|
30
|
+
*/
|
|
31
|
+
function unscopedRowsBelongHere(base, kind) {
|
|
32
|
+
return (
|
|
33
|
+
!base.activeOrgId ||
|
|
34
|
+
(kind === 'entity' && base.individualAcross === 'all') ||
|
|
35
|
+
sameId(base.legacyOrgId, base.activeOrgId)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* What a caller may see, as owner/organization pairs:
|
|
41
|
+
*
|
|
42
|
+
* - the active organization's own rows (owner = the organization), whether
|
|
43
|
+
* they carry its id or, written before scoping, none;
|
|
44
|
+
* - the caller's rows in the active organization;
|
|
45
|
+
* - the caller's rows with no organization, when `unscopedRowsBelongHere`.
|
|
46
|
+
*
|
|
47
|
+
* `kind` is 'entity' or 'integration'; only entities may be
|
|
48
|
+
* organization-independent under 'all' (an integration always belongs to the
|
|
49
|
+
* organization it was created in).
|
|
50
|
+
*
|
|
51
|
+
* @returns {OwnershipClause[]}
|
|
52
|
+
*/
|
|
53
|
+
function buildClauses(base, kind) {
|
|
54
|
+
const clauses = [];
|
|
55
|
+
if (base.activeOrgId) {
|
|
56
|
+
clauses.push({ userId: base.activeOrgId, organizationId: base.activeOrgId });
|
|
57
|
+
clauses.push({ userId: base.activeOrgId, organizationId: null });
|
|
58
|
+
}
|
|
59
|
+
if (base.callerId) {
|
|
60
|
+
if (base.activeOrgId) {
|
|
61
|
+
clauses.push({ userId: base.callerId, organizationId: base.activeOrgId });
|
|
62
|
+
}
|
|
63
|
+
if (unscopedRowsBelongHere(base, kind)) {
|
|
64
|
+
clauses.push({ userId: base.callerId, organizationId: null });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return clauses;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Whether the caller may manage organization-level things (create, change,
|
|
72
|
+
* delete), ADR-056 §3.6:
|
|
73
|
+
*
|
|
74
|
+
* - A shared-secret caller is the app's own backend and may.
|
|
75
|
+
* - A Frigg bearer token (`/user/login`) predates organization roles and
|
|
76
|
+
* carries none. It keeps its pre-roles behaviour and may, unless the app
|
|
77
|
+
* sets `user.orgAdminGateBearerTokens: true`; then it is judged like any
|
|
78
|
+
* other caller below.
|
|
79
|
+
* - Otherwise the role is the one the token carries; when it carries none,
|
|
80
|
+
* the role stored on the caller's membership of the active organization;
|
|
81
|
+
* when there is none, `MEMBER`. A missing role never means admin. An
|
|
82
|
+
* unknown source (a `User` built outside AuthenticateUser) is judged this
|
|
83
|
+
* way too.
|
|
84
|
+
*
|
|
85
|
+
* @param {{authSource: (string|null), orgRole: (string|null), membershipRole: (string|null)}} who
|
|
86
|
+
* @param {string[]} adminRoles - `user.orgAdminRoles`, compared case-insensitively
|
|
87
|
+
* @param {{gateBearerTokens?: boolean}} [options]
|
|
88
|
+
*/
|
|
89
|
+
function canManageOrganization(
|
|
90
|
+
{ authSource, orgRole, membershipRole },
|
|
91
|
+
adminRoles = DEFAULT_ORG_ADMIN_ROLES,
|
|
92
|
+
{ gateBearerTokens = false } = {}
|
|
93
|
+
) {
|
|
94
|
+
if (authSource === AUTH_SOURCES.SHARED_SECRET) return true;
|
|
95
|
+
if (authSource === AUTH_SOURCES.BEARER_TOKEN && !gateBearerTokens) return true;
|
|
96
|
+
let role = 'MEMBER';
|
|
97
|
+
if (isPresent(orgRole)) role = orgRole;
|
|
98
|
+
else if (isPresent(membershipRole)) role = membershipRole;
|
|
99
|
+
role = String(role).toUpperCase();
|
|
100
|
+
return adminRoles.some((admin) => String(admin).toUpperCase() === role);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Who the caller is, for ownership and visibility: their individual user, the
|
|
105
|
+
* organization this request acts in, the legacy `organizationId` pointer,
|
|
106
|
+
* whether they may manage organization-level things, and the clauses that
|
|
107
|
+
* follow from them (computed once).
|
|
108
|
+
*
|
|
109
|
+
* @param {Object} user - the authenticated `User` (or a `{ getId }` double)
|
|
110
|
+
* @param {{individualAcross?: string, orgAdminRoles?: string[], gateBearerTokens?: boolean}} [options]
|
|
111
|
+
*/
|
|
112
|
+
function accessContext(
|
|
113
|
+
user,
|
|
114
|
+
{
|
|
115
|
+
individualAcross = 'organization',
|
|
116
|
+
orgAdminRoles = DEFAULT_ORG_ADMIN_ROLES,
|
|
117
|
+
gateBearerTokens = false,
|
|
118
|
+
} = {}
|
|
119
|
+
) {
|
|
120
|
+
const hasUserModel = typeof user?.getIndividualUser === 'function';
|
|
121
|
+
const individual = hasUserModel ? user.getIndividualUser() : null;
|
|
122
|
+
const base = {
|
|
123
|
+
callerId: hasUserModel ? idString(individual?.id) : idString(user?.getId?.()),
|
|
124
|
+
activeOrgId:
|
|
125
|
+
typeof user?.getActiveOrganizationId === 'function'
|
|
126
|
+
? idString(user.getActiveOrganizationId())
|
|
127
|
+
: null,
|
|
128
|
+
legacyOrgId: idString(individual?.organizationId),
|
|
129
|
+
individualAcross,
|
|
130
|
+
};
|
|
131
|
+
const who = {
|
|
132
|
+
authSource:
|
|
133
|
+
typeof user?.getAuthSource === 'function' ? user.getAuthSource() : null,
|
|
134
|
+
orgRole: typeof user?.getOrgRole === 'function' ? user.getOrgRole() : null,
|
|
135
|
+
membershipRole:
|
|
136
|
+
typeof user?.getMembershipRole === 'function'
|
|
137
|
+
? user.getMembershipRole()
|
|
138
|
+
: null,
|
|
139
|
+
};
|
|
140
|
+
return {
|
|
141
|
+
...base,
|
|
142
|
+
canManageOrg: canManageOrganization(who, orgAdminRoles, {
|
|
143
|
+
gateBearerTokens,
|
|
144
|
+
}),
|
|
145
|
+
clauses: {
|
|
146
|
+
entity: buildClauses(base, 'entity'),
|
|
147
|
+
integration: buildClauses(base, 'integration'),
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The clauses for `kind` ('entity' | 'integration'): what to ask the repository. */
|
|
153
|
+
const visibilityClauses = (ctx, kind) => ctx.clauses[kind];
|
|
154
|
+
|
|
155
|
+
const matchesClause = (record, clause) =>
|
|
156
|
+
sameId(record.userId, clause.userId) &&
|
|
157
|
+
(clause.organizationId === null
|
|
158
|
+
? !isPresent(record.organizationId)
|
|
159
|
+
: sameId(record.organizationId, clause.organizationId));
|
|
160
|
+
|
|
161
|
+
/** The scope a stored row has for this caller, from its owner. */
|
|
162
|
+
function scopeOf(record, ctx) {
|
|
163
|
+
if (!isPresent(record?.userId)) return SCOPES.GLOBAL;
|
|
164
|
+
if (sameId(record.userId, ctx.activeOrgId)) return SCOPES.ORGANIZATION;
|
|
165
|
+
if (sameId(record.userId, ctx.callerId)) return SCOPES.INDIVIDUAL;
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** manage for the person's own; for the organization's, only an admin; use otherwise. */
|
|
170
|
+
const accessFor = (scope, ctx) => {
|
|
171
|
+
if (scope === SCOPES.INDIVIDUAL) return ACCESS.MANAGE;
|
|
172
|
+
if (scope === SCOPES.ORGANIZATION) {
|
|
173
|
+
return ctx.canManageOrg ? ACCESS.MANAGE : ACCESS.USE;
|
|
174
|
+
}
|
|
175
|
+
return ACCESS.USE;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Whether an access level lets the caller use a record: bind it, proxy through
|
|
180
|
+
* it (§3.4). `manage` includes `use`; `view` (metadata only) does not, and
|
|
181
|
+
* neither does anything unrecognised.
|
|
182
|
+
*/
|
|
183
|
+
const grantsUse = (access) => access === ACCESS.USE || access === ACCESS.MANAGE;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* What the caller may do with a record owned by `ownerId` that carries no
|
|
187
|
+
* organization of its own (a credential): manage the person's own, manage the
|
|
188
|
+
* active organization's only as an admin, nothing otherwise. Null when the
|
|
189
|
+
* record is not theirs.
|
|
190
|
+
*/
|
|
191
|
+
function accessToOwner(ownerId, ctx) {
|
|
192
|
+
const scope = scopeOf({ userId: ownerId }, ctx);
|
|
193
|
+
if (scope !== SCOPES.INDIVIDUAL && scope !== SCOPES.ORGANIZATION) return null;
|
|
194
|
+
return accessFor(scope, ctx);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* What a row is to this caller, or null when they may not see it.
|
|
199
|
+
* `{ scope, owner: {type, id}, organizationId, access }` (§3.4). A `global`
|
|
200
|
+
* row is never described: it is not the tenant's to see.
|
|
201
|
+
*/
|
|
202
|
+
function describeRecord(record, ctx, kind) {
|
|
203
|
+
if (!record) return null;
|
|
204
|
+
const scope = scopeOf(record, ctx);
|
|
205
|
+
if (scope === null || scope === SCOPES.GLOBAL) return null;
|
|
206
|
+
if (!ctx.clauses[kind].some((clause) => matchesClause(record, clause))) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
scope,
|
|
211
|
+
owner: { type: scope, id: idString(record.userId) },
|
|
212
|
+
organizationId:
|
|
213
|
+
scope === SCOPES.ORGANIZATION
|
|
214
|
+
? idString(record.userId)
|
|
215
|
+
: idString(record.organizationId),
|
|
216
|
+
access: accessFor(scope, ctx),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** `?owner=` values and the scope each one means. */
|
|
221
|
+
const LIST_OWNERS = Object.freeze({
|
|
222
|
+
me: SCOPES.INDIVIDUAL,
|
|
223
|
+
org: SCOPES.ORGANIZATION,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
/** Whether a described row passes `?scope=` and `?owner=`. */
|
|
227
|
+
function matchesListFilters(description, { scope, owner } = {}) {
|
|
228
|
+
if (scope !== undefined && description.scope !== scope) return false;
|
|
229
|
+
if (owner !== undefined && description.owner.type !== LIST_OWNERS[owner]) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = {
|
|
236
|
+
canManageOrganization,
|
|
237
|
+
accessToOwner,
|
|
238
|
+
accessContext,
|
|
239
|
+
unscopedRowsBelongHere,
|
|
240
|
+
visibilityClauses,
|
|
241
|
+
matchesClause,
|
|
242
|
+
scopeOf,
|
|
243
|
+
describeRecord,
|
|
244
|
+
grantsUse,
|
|
245
|
+
LIST_OWNERS,
|
|
246
|
+
matchesListFilters,
|
|
247
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const { AuthorizationSession } = require('../../domain/authorization-session');
|
|
2
|
+
|
|
3
|
+
/** In-memory AuthorizationSession repository for tests. */
|
|
4
|
+
class InMemoryAuthorizationSessionRepository {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.rows = new Map();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
_copy(session) {
|
|
10
|
+
return new AuthorizationSession({ ...session, stepData: { ...session.stepData } });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async create(session) {
|
|
14
|
+
this.rows.set(session.sessionId, this._copy(session));
|
|
15
|
+
return this._copy(session);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async findBySessionId(sessionId) {
|
|
19
|
+
const row = this.rows.get(sessionId);
|
|
20
|
+
if (!row || row.isExpired()) return null;
|
|
21
|
+
return this._copy(row);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async update(session) {
|
|
25
|
+
this.rows.set(session.sessionId, this._copy(session));
|
|
26
|
+
return this._copy(session);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async deleteBySessionId(sessionId) {
|
|
30
|
+
return this.rows.delete(sessionId) ? 1 : 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async deleteExpired(now = new Date()) {
|
|
34
|
+
let removed = 0;
|
|
35
|
+
for (const [id, row] of this.rows) {
|
|
36
|
+
if (row.isExpired(now)) {
|
|
37
|
+
this.rows.delete(id);
|
|
38
|
+
removed++;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return removed;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { InMemoryAuthorizationSessionRepository };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test double helper: answers an owner/organization clause the way a database
|
|
3
|
+
* does, so in-memory repositories run the clauses a use case hands them.
|
|
4
|
+
* `clause.organizationId === null` matches a row with no organization (unset
|
|
5
|
+
* or null).
|
|
6
|
+
*/
|
|
7
|
+
const same = (a, b) =>
|
|
8
|
+
a !== undefined && a !== null && b !== undefined && b !== null && String(a) === String(b);
|
|
9
|
+
|
|
10
|
+
const matchesClause = (row, clause) =>
|
|
11
|
+
same(row.userId, clause.userId) &&
|
|
12
|
+
(clause.organizationId === null
|
|
13
|
+
? row.organizationId === null || row.organizationId === undefined
|
|
14
|
+
: same(row.organizationId, clause.organizationId));
|
|
15
|
+
|
|
16
|
+
const matchesAnyClause = (row, clauses) => clauses.some((clause) => matchesClause(row, clause));
|
|
17
|
+
|
|
18
|
+
module.exports = { same, matchesClause, matchesAnyClause };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authorizes an entity for a caller: decides who will own it and in which
|
|
3
|
+
* organization (ADR-056 §3.2, §3.6), then runs the module's authorization.
|
|
4
|
+
* Shared by the v1 `POST /api/authorize` and the v2 authorize flow, so the
|
|
5
|
+
* owner is decided in one place and never by `user.primary`.
|
|
6
|
+
*/
|
|
7
|
+
class AuthorizeEntityForUser {
|
|
8
|
+
/**
|
|
9
|
+
* @param {Object} params
|
|
10
|
+
* @param {Object} params.scopePolicy - from createScopePolicy
|
|
11
|
+
* @param {import('./process-authorization-callback').ProcessAuthorizationCallback} params.processAuthorizationCallback
|
|
12
|
+
*/
|
|
13
|
+
constructor({ scopePolicy, processAuthorizationCallback }) {
|
|
14
|
+
this.scopePolicy = scopePolicy;
|
|
15
|
+
this.processAuthorizationCallback = processAuthorizationCallback;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The scope, owner and organization a new entity of `entityType` gets for
|
|
20
|
+
* this caller. Throws a 400/403 when the request cannot have one.
|
|
21
|
+
*
|
|
22
|
+
* @param {{user: Object, entityType: string, scope?: string, fixedScope?: string}} params
|
|
23
|
+
* `fixedScope`: a re-authorization keeps the scope its connection has
|
|
24
|
+
*/
|
|
25
|
+
resolveOwnership({ user, entityType, scope, fixedScope }) {
|
|
26
|
+
if (fixedScope) return this.scopePolicy.ownershipForScope({ scope: fixedScope, user });
|
|
27
|
+
return this.scopePolicy.resolveOwnership({
|
|
28
|
+
entityType,
|
|
29
|
+
requestScope: scope,
|
|
30
|
+
user,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {Object} params
|
|
36
|
+
* @param {Object} params.user - authenticated User
|
|
37
|
+
* @param {string} params.entityType
|
|
38
|
+
* @param {Object} params.data - what the module's authorization needs
|
|
39
|
+
* @param {string} [params.scope] - the scope the request asks for
|
|
40
|
+
* @param {Object} [params.ownership] - from `resolveOwnership`, when the
|
|
41
|
+
* caller needed it earlier (a multi-step flow does)
|
|
42
|
+
* @returns {Promise<{result: Object, ownership: Object}>} the callback's
|
|
43
|
+
* `{ credential_id, entity_id, type }` and the ownership it ran with
|
|
44
|
+
*/
|
|
45
|
+
async execute({ user, entityType, data, scope, ownership }) {
|
|
46
|
+
const resolved = ownership ?? this.resolveOwnership({ user, entityType, scope });
|
|
47
|
+
const result = await this.processAuthorizationCallback.execute(
|
|
48
|
+
resolved.ownerId,
|
|
49
|
+
entityType,
|
|
50
|
+
data,
|
|
51
|
+
{
|
|
52
|
+
scope: resolved.scope,
|
|
53
|
+
organizationId: resolved.organizationId,
|
|
54
|
+
legacyOrgId: resolved.legacyOrgId,
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
return { result, ownership: resolved };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { AuthorizeEntityForUser };
|