@friggframework/core 2.0.0--canary.683.8a84ab1.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/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). For permissions the token's role wins; a token with none uses the role stored on the person's membership of the active organization, and is a member when there is none: it cannot create or change organization-level entities or integrations. `user.orgAdminRoles` (default `['ADMIN']`) names the roles that can. Frigg bearer tokens are not gated unless `user.orgAdminGateBearerTokens: true`. |
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,7 +1,5 @@
1
1
  const Boom = require('@hapi/boom');
2
2
  const { userOwns } = require('../../modules/use-cases/get-entity-module-for-user');
3
- const { DEFAULT_SCOPE_POLICY } = require('../../modules/scope-policy');
4
- const { assertCanManage } = require('../../modules/use-cases/manage-access');
5
3
 
6
4
  /**
7
5
  * Management API v2 credential operations. A credential has no module name
@@ -92,8 +90,7 @@ class GetCredentialForUserV2 {
92
90
  * than disappearing from integrations.
93
91
  */
94
92
  class DeleteCredentialForUser {
95
- constructor({ credentialRepository, moduleRepository, getCredentialForUser, scopePolicy }) {
96
- this.scopePolicy = scopePolicy || DEFAULT_SCOPE_POLICY;
93
+ constructor({ credentialRepository, moduleRepository, getCredentialForUser }) {
97
94
  this.credentialRepository = credentialRepository;
98
95
  this.moduleRepository = moduleRepository;
99
96
  this.getCredentialForUser = getCredentialForUser;
@@ -104,11 +101,6 @@ class DeleteCredentialForUser {
104
101
  credentialId,
105
102
  user
106
103
  );
107
- // A credential an organization owns: the organization's admins only.
108
- // (null: not the person's or the active organization's, which the
109
- // owner check above already decided.)
110
- const access = this.scopePolicy.accessToOwnedBy(user, credential.userId);
111
- if (access !== null) assertCanManage(access, `credential ${credential.id}`);
112
104
  for (const entityId of credential.entityIds) {
113
105
  await this.moduleRepository.unsetCredential(entityId);
114
106
  }
@@ -1,6 +1,4 @@
1
1
  const Boom = require('@hapi/boom');
2
- const { DEFAULT_SCOPE_POLICY } = require('../../modules/scope-policy');
3
- const { assertCanManage } = require('../../modules/use-cases/manage-access');
4
2
 
5
3
  /**
6
4
  * Re-authorization of a credential the caller owns (Management API v2). It
@@ -16,11 +14,7 @@ async function credentialEntityType(getCredentialForUser, credentialId, user) {
16
14
  { code: 'CREDENTIAL_TYPE_UNKNOWN' }
17
15
  );
18
16
  }
19
- return {
20
- entityType: credential.type,
21
- credentialId: String(credential.id),
22
- ownerId: credential.userId,
23
- };
17
+ return { entityType: credential.type, credentialId: String(credential.id) };
24
18
  }
25
19
 
26
20
  /** GET /api/v2/credentials/:credentialId/reauthorize */
@@ -31,7 +25,7 @@ class GetCredentialReauthorization {
31
25
  }
32
26
 
33
27
  async execute(credentialId, user, { step = 1, sessionId, state } = {}) {
34
- const { ownerId, ...binding } = await credentialEntityType(
28
+ const binding = await credentialEntityType(
35
29
  this.getCredentialForUser,
36
30
  credentialId,
37
31
  user
@@ -48,21 +42,17 @@ class GetCredentialReauthorization {
48
42
 
49
43
  /** POST /api/v2/credentials/:credentialId/reauthorize */
50
44
  class ReauthorizeCredentialForUser {
51
- constructor({ getCredentialForUser, submitAuthorizationStep, scopePolicy }) {
52
- this.scopePolicy = scopePolicy || DEFAULT_SCOPE_POLICY;
45
+ constructor({ getCredentialForUser, submitAuthorizationStep }) {
53
46
  this.getCredentialForUser = getCredentialForUser;
54
47
  this.submitAuthorizationStep = submitAuthorizationStep;
55
48
  }
56
49
 
57
50
  async execute(credentialId, user, { data, step = 1, sessionId } = {}) {
58
- const { ownerId, ...binding } = await credentialEntityType(
51
+ const binding = await credentialEntityType(
59
52
  this.getCredentialForUser,
60
53
  credentialId,
61
54
  user
62
55
  );
63
- // A credential an organization owns: the organization's admins only.
64
- const access = this.scopePolicy.accessToOwnedBy(user, ownerId);
65
- if (access !== null) assertCanManage(access, `credential ${credentialId}`);
66
56
  return this.submitAuthorizationStep.execute({
67
57
  user,
68
58
  ...binding,
@@ -291,11 +291,9 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
291
291
  } = useCases;
292
292
 
293
293
  // The owner of an integration the caller may reach (404 otherwise), which is
294
- // what the id-and-owner use cases below expect. `manage: true` is for
295
- // changing or deleting: an organization's integration needs an admin (403).
296
- const ownerOf = async (integrationId, user, options) =>
297
- (await getOwnedIntegrationRecord.execute(integrationId, user, options))
298
- .userId;
294
+ // what the id-and-owner use cases below expect.
295
+ const ownerOf = async (integrationId, user) =>
296
+ (await getOwnedIntegrationRecord.execute(integrationId, user)).userId;
299
297
 
300
298
  router.route('/api/integrations').get(
301
299
  catchAsyncError(async (req, res) => {
@@ -347,7 +345,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
347
345
 
348
346
  const integration = await updateIntegration.execute(
349
347
  req.params.integrationId,
350
- await ownerOf(req.params.integrationId, user, { manage: true }),
348
+ await ownerOf(req.params.integrationId, user),
351
349
  params.config
352
350
  );
353
351
  res.json(integration);
@@ -360,7 +358,7 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
360
358
  const params = checkRequiredParams(req.params, ['integrationId']);
361
359
  await deleteIntegrationForUser.execute(
362
360
  params.integrationId,
363
- await ownerOf(params.integrationId, user, { manage: true })
361
+ await ownerOf(params.integrationId, user)
364
362
  );
365
363
  res.status(204).json({});
366
364
  })
@@ -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
- const tenancy = record && this.describe(record, user);
59
- if (!tenancy) throw notFound(integrationId);
60
- if (manage) assertCanManage(tenancy.access, `integration ${integrationId}`);
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
  }
@@ -252,7 +252,6 @@ function buildV2Dependencies({ appDefinition, config, entityTypeRegistry }) {
252
252
  credentialRepository,
253
253
  moduleRepository,
254
254
  getCredentialForUser,
255
- scopePolicy,
256
255
  }),
257
256
  getCredentialReauthorization: new GetCredentialReauthorization({
258
257
  getCredentialForUser,
@@ -261,7 +260,6 @@ function buildV2Dependencies({ appDefinition, config, entityTypeRegistry }) {
261
260
  reauthorizeCredentialForUser: new ReauthorizeCredentialForUser({
262
261
  getCredentialForUser,
263
262
  submitAuthorizationStep,
264
- scopePolicy,
265
263
  }),
266
264
 
267
265
  // Authorization (multi-step)
@@ -8,7 +8,6 @@ const {
8
8
  SCOPES,
9
9
  INDIVIDUAL_ACROSS_VALUES,
10
10
  OWNER_RULES,
11
- DEFAULT_ORG_ADMIN_ROLES,
12
11
  configError,
13
12
  isPlainObject,
14
13
  parseScopeSetting,
@@ -76,40 +75,6 @@ function readIntegrationOwnership(integrationClass) {
76
75
  return { scope: parseScopeSetting(raw.scope, `${where}.scope`), owner };
77
76
  }
78
77
 
79
- /**
80
- * `user.orgAdminRoles`: the token roles that may manage organization-level
81
- * things. Default `['ADMIN']`. A shared-secret caller needs no role.
82
- */
83
- function readOrgAdminRoles(appDefinition) {
84
- const raw = appDefinition?.user?.orgAdminRoles;
85
- if (raw === undefined || raw === null) return [...DEFAULT_ORG_ADMIN_ROLES];
86
- if (
87
- !Array.isArray(raw) ||
88
- raw.length === 0 ||
89
- raw.some((role) => typeof role !== 'string' || role.trim() === '')
90
- ) {
91
- throw configError(
92
- 'appDefinition.user.orgAdminRoles',
93
- 'must be a non-empty array of role names'
94
- );
95
- }
96
- return [...raw];
97
- }
98
-
99
- /**
100
- * `user.orgAdminGateBearerTokens`: judge Frigg bearer-token callers by role
101
- * like adopter-token callers. Default false: they keep their pre-roles
102
- * behaviour and may manage the organization they act in.
103
- */
104
- function readGateBearerTokens(appDefinition) {
105
- const raw = appDefinition?.user?.orgAdminGateBearerTokens;
106
- if (raw === undefined || raw === null) return false;
107
- if (typeof raw !== 'boolean') {
108
- throw configError('appDefinition.user.orgAdminGateBearerTokens', 'must be a boolean');
109
- }
110
- return raw;
111
- }
112
-
113
78
  /**
114
79
  * Every binding of every integration with its resolved scope and where it came
115
80
  * from, parsing each integration's settings once. Throws OwnershipConfigError
@@ -153,7 +118,7 @@ function listBindingScopes(appDefinition, entityTypeRegistry, primary) {
153
118
  * Validates every ownership setting in one pass and returns the app-level
154
119
  * config the runtime needs.
155
120
  *
156
- * @returns {{scope: ({default: string, allow: string[]}|null), individualAcross: string, orgAdminRoles: string[], gateBearerTokens: boolean}}
121
+ * @returns {{scope: ({default: string, allow: string[]}|null), individualAcross: string}}
157
122
  * @throws {OwnershipConfigError}
158
123
  */
159
124
  function resolveOwnershipConfig(appDefinition, entityTypeRegistry) {
@@ -178,8 +143,6 @@ function resolveOwnershipConfig(appDefinition, entityTypeRegistry) {
178
143
  return Object.freeze({
179
144
  scope: app.scope,
180
145
  individualAcross: app.individualAcross,
181
- orgAdminRoles: Object.freeze(readOrgAdminRoles(appDefinition)),
182
- gateBearerTokens: readGateBearerTokens(appDefinition),
183
146
  });
184
147
  }
185
148
 
@@ -6,21 +6,16 @@
6
6
 
7
7
  const {
8
8
  INDIVIDUAL_ACROSS_VALUES,
9
- DEFAULT_ORG_ADMIN_ROLES,
10
9
  configError,
11
10
  parseScopeSetting,
12
11
  resolveScope,
13
12
  resolveOwnerForScope,
14
13
  } = require('./scope-resolution');
15
- const {
16
- accessContext,
17
- accessToOwner,
18
- describeRecord,
19
- } = require('./scope-visibility');
14
+ const { accessContext, describeRecord } = require('./scope-visibility');
20
15
 
21
16
  /**
22
17
  * @param {Object} [params]
23
- * @param {{scope: *, individualAcross: string, orgAdminRoles?: string[], gateBearerTokens?: boolean}} [params.ownership] - the raw
18
+ * @param {{scope: *, individualAcross: string}} [params.ownership] - the raw
24
19
  * `appDefinition.ownership` or what resolveOwnershipConfig returned (a parsed
25
20
  * `{ default, allow }` parses to itself)
26
21
  * @param {Map} [params.entityTypeRegistry]
@@ -38,11 +33,7 @@ function createScopePolicy({ ownership, entityTypeRegistry } = {}) {
38
33
  );
39
34
  }
40
35
 
41
- const orgAdminRoles = ownership?.orgAdminRoles ?? DEFAULT_ORG_ADMIN_ROLES;
42
- const gateBearerTokens = ownership?.gateBearerTokens === true;
43
-
44
- const contextFor = (user) =>
45
- accessContext(user, { individualAcross, orgAdminRoles, gateBearerTokens });
36
+ const contextFor = (user) => accessContext(user, { individualAcross });
46
37
 
47
38
  /** The scope a new connection of `entityType` gets (levels 1, 4, 5, 6). */
48
39
  const resolveEntityScope = ({ entityType, requestScope, binding, integration, user }) =>
@@ -74,16 +65,8 @@ function createScopePolicy({ ownership, entityTypeRegistry } = {}) {
74
65
  const canUseEntity = (user, entity) =>
75
66
  describeRecord(entity, contextFor(user), 'entity') !== null;
76
67
 
77
- /**
78
- * What `user` may do with a record owned by `ownerId` that has no
79
- * organization of its own (a credential): manage, or null when not theirs.
80
- */
81
- const accessToOwnedBy = (user, ownerId) => accessToOwner(ownerId, contextFor(user));
82
-
83
68
  return Object.freeze({
84
69
  individualAcross,
85
- orgAdminRoles,
86
- accessToOwnedBy,
87
70
  accessContext: contextFor,
88
71
  resolveScope: resolveEntityScope,
89
72
  resolveOwnership,
@@ -25,9 +25,6 @@ const INDIVIDUAL_ACROSS_VALUES = Object.freeze(['organization', 'all']);
25
25
  const OWNER_RULES = Object.freeze(['derive', 'individual', 'organization']);
26
26
  const ACCESS = Object.freeze({ MANAGE: 'manage', USE: 'use', VIEW: 'view' });
27
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
28
  /** Raised at app-definition load for an invalid ownership setting. */
32
29
  class OwnershipConfigError extends BaseError {
33
30
  constructor(message, options) {
@@ -183,11 +180,6 @@ const MISSING_ORGANIZATION =
183
180
  const missing = (message, field) =>
184
181
  Boom.badRequest(message, { code: 'IDENTITY_REQUIRED', details: { field } });
185
182
 
186
- const adminRequired = (what) =>
187
- Boom.forbidden(`only an organization admin can ${what}`, {
188
- code: 'ORG_ADMIN_REQUIRED',
189
- });
190
-
191
183
  /**
192
184
  * The owner and organization for a new entity of `scope` (§3.2):
193
185
  *
@@ -208,7 +200,6 @@ function resolveOwnerForScope(scope, ctx) {
208
200
  }
209
201
  if (scope === SCOPES.ORGANIZATION) {
210
202
  if (!ctx.activeOrgId) throw missing(MISSING_ORGANIZATION, 'x-frigg-apporgid');
211
- if (!ctx.canManageOrg) throw adminRequired('connect an organization-level account');
212
203
  return { scope, ownerId: ctx.activeOrgId, organizationId: ctx.activeOrgId };
213
204
  }
214
205
  throw Boom.forbidden(
@@ -238,7 +229,6 @@ function integrationOwnerKind(entityScopes, rule = 'derive') {
238
229
  function resolveIntegrationOwner(kind, ctx) {
239
230
  if (kind === SCOPES.ORGANIZATION) {
240
231
  if (!ctx.activeOrgId) throw missing(MISSING_ORGANIZATION, 'x-frigg-apporgid');
241
- if (!ctx.canManageOrg) throw adminRequired('create an organization-level integration');
242
232
  return { ownerId: ctx.activeOrgId, organizationId: ctx.activeOrgId };
243
233
  }
244
234
  if (!ctx.callerId) throw missing(MISSING_INDIVIDUAL, 'x-frigg-appuserid');
@@ -273,7 +263,6 @@ module.exports = {
273
263
  INDIVIDUAL_ACROSS_VALUES,
274
264
  OWNER_RULES,
275
265
  ACCESS,
276
- DEFAULT_ORG_ADMIN_ROLES,
277
266
  OwnershipConfigError,
278
267
  configError,
279
268
  isPlainObject,
@@ -8,8 +8,7 @@
8
8
  */
9
9
 
10
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');
11
+ const { SCOPES, ACCESS } = require('./scope-resolution');
13
12
 
14
13
  /**
15
14
  * One owner/organization pair a repository ORs into a single query.
@@ -67,56 +66,15 @@ function buildClauses(base, kind) {
67
66
  return clauses;
68
67
  }
69
68
 
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
69
  /**
104
70
  * 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).
71
+ * organization this request acts in, the legacy `organizationId` pointer, and
72
+ * the clauses that follow from them (computed once).
108
73
  *
109
74
  * @param {Object} user - the authenticated `User` (or a `{ getId }` double)
110
- * @param {{individualAcross?: string, orgAdminRoles?: string[], gateBearerTokens?: boolean}} [options]
75
+ * @param {{individualAcross?: string}} [options]
111
76
  */
112
- function accessContext(
113
- user,
114
- {
115
- individualAcross = 'organization',
116
- orgAdminRoles = DEFAULT_ORG_ADMIN_ROLES,
117
- gateBearerTokens = false,
118
- } = {}
119
- ) {
77
+ function accessContext(user, { individualAcross = 'organization' } = {}) {
120
78
  const hasUserModel = typeof user?.getIndividualUser === 'function';
121
79
  const individual = hasUserModel ? user.getIndividualUser() : null;
122
80
  const base = {
@@ -128,20 +86,8 @@ function accessContext(
128
86
  legacyOrgId: idString(individual?.organizationId),
129
87
  individualAcross,
130
88
  };
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
89
  return {
141
90
  ...base,
142
- canManageOrg: canManageOrganization(who, orgAdminRoles, {
143
- gateBearerTokens,
144
- }),
145
91
  clauses: {
146
92
  entity: buildClauses(base, 'entity'),
147
93
  integration: buildClauses(base, 'integration'),
@@ -166,26 +112,8 @@ function scopeOf(record, ctx) {
166
112
  return null;
167
113
  }
168
114
 
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
- * What the caller may do with a record owned by `ownerId` that carries no
180
- * organization of its own (a credential): manage the person's own, manage the
181
- * active organization's only as an admin, nothing otherwise. Null when the
182
- * record is not theirs.
183
- */
184
- function accessToOwner(ownerId, ctx) {
185
- const scope = scopeOf({ userId: ownerId }, ctx);
186
- if (scope !== SCOPES.INDIVIDUAL && scope !== SCOPES.ORGANIZATION) return null;
187
- return accessFor(scope, ctx);
188
- }
115
+ const accessFor = (scope) =>
116
+ scope === SCOPES.GLOBAL ? ACCESS.USE : ACCESS.MANAGE;
189
117
 
190
118
  /**
191
119
  * What a row is to this caller, or null when they may not see it.
@@ -206,7 +134,7 @@ function describeRecord(record, ctx, kind) {
206
134
  scope === SCOPES.ORGANIZATION
207
135
  ? idString(record.userId)
208
136
  : idString(record.organizationId),
209
- access: accessFor(scope, ctx),
137
+ access: accessFor(scope),
210
138
  };
211
139
  }
212
140
 
@@ -226,8 +154,6 @@ function matchesListFilters(description, { scope, owner } = {}) {
226
154
  }
227
155
 
228
156
  module.exports = {
229
- canManageOrganization,
230
- accessToOwner,
231
157
  accessContext,
232
158
  unscopedRowsBelongHere,
233
159
  visibilityClauses,
@@ -5,7 +5,6 @@ const {
5
5
  matchesListFilters,
6
6
  } = require('../scope-visibility');
7
7
  const { DEFAULT_SCOPE_POLICY } = require('../scope-policy');
8
- const { assertCanManage } = require('./manage-access');
9
8
 
10
9
  /**
11
10
  * Management API v2 entity operations. Each one starts from
@@ -112,10 +111,10 @@ class RefreshEntityOptionsForUser {
112
111
  }
113
112
 
114
113
  /**
115
- * Deletes an entity the caller may manage (the owner; for an organization
116
- * entity, an organization admin). Refuses with 409 while integrations still
117
- * use it, so an integration never points at a missing entity. The credential
118
- * is left alone (others may share it); delete it through /api/v2/credentials.
114
+ * Deletes an entity the caller may use. Refuses with 409 while integrations
115
+ * still use it, so an integration never points at a missing entity. The
116
+ * credential is left alone (others may share it); delete it through
117
+ * /api/v2/credentials.
119
118
  */
120
119
  class DeleteEntityForUser {
121
120
  constructor({ getEntityModuleForUser, moduleRepository, integrationRepository }) {
@@ -129,10 +128,6 @@ class DeleteEntityForUser {
129
128
  entityId,
130
129
  user
131
130
  );
132
- assertCanManage(
133
- this.getEntityModuleForUser.describe(entity, user)?.access,
134
- `entity ${entity.id}`
135
- );
136
131
  const integrations =
137
132
  (await this.integrationRepository.findIntegrationsByEntityId(
138
133
  entity.id
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/core",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.683.8a84ab1.0",
4
+ "version": "2.0.0--canary.682.d8626c4.0",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
7
7
  "@aws-sdk/client-kms": "^3.588.0",
@@ -48,9 +48,9 @@
48
48
  }
49
49
  },
50
50
  "devDependencies": {
51
- "@friggframework/eslint-config": "2.0.0--canary.683.8a84ab1.0",
52
- "@friggframework/prettier-config": "2.0.0--canary.683.8a84ab1.0",
53
- "@friggframework/test": "2.0.0--canary.683.8a84ab1.0",
51
+ "@friggframework/eslint-config": "2.0.0--canary.682.d8626c4.0",
52
+ "@friggframework/prettier-config": "2.0.0--canary.682.d8626c4.0",
53
+ "@friggframework/test": "2.0.0--canary.682.d8626c4.0",
54
54
  "@prisma/client": "^6.19.3",
55
55
  "@types/lodash": "4.17.15",
56
56
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -90,5 +90,5 @@
90
90
  "publishConfig": {
91
91
  "access": "public"
92
92
  },
93
- "gitHead": "8a84ab14e5e56c7e7939b1b6a0b656ed9301dcfd"
93
+ "gitHead": "d8626c44a041edbf421a843c24d6a050cbf8657b"
94
94
  }
@@ -62,20 +62,6 @@ declare module "@friggframework/core/user" {
62
62
  * at startup. `strictMembership` wins when both are set.
63
63
  */
64
64
  strictUserValidation?: boolean;
65
- /**
66
- * Roles that may manage organization-level things (ADR-056 §3). Default
67
- * `['ADMIN']`. The role is the adopter token's role claim, else the stored
68
- * membership `orgRole`, else `MEMBER` (never admin). A shared-secret
69
- * (`x-frigg` header) caller needs no role, nor does a Frigg bearer token
70
- * unless `orgAdminGateBearerTokens`. Compared case-insensitively.
71
- * Validated at load.
72
- */
73
- orgAdminRoles?: string[];
74
- /**
75
- * Judge Frigg bearer-token callers by role like adopter-token callers.
76
- * Default false: they keep their behaviour from before organization roles.
77
- */
78
- orgAdminGateBearerTokens?: boolean;
79
65
  authModes?: UserAuthModes;
80
66
  jwtConfig?: AdopterJwtConfig;
81
67
  }
@@ -127,8 +113,5 @@ declare module "@friggframework/core/user" {
127
113
  /** The organization role from the adopter token for this request, or null. */
128
114
  getOrgRole(): string | null;
129
115
  setOrgRole(orgRole: string | null): void;
130
- /** The stored `orgRole` of the membership of the active organization, or null. Used when the token carries no role. */
131
- getMembershipRole(): string | null;
132
- setMembershipRole(membershipRole: string | null): void;
133
116
  }
134
117
  }
@@ -1,6 +1,5 @@
1
1
  const Boom = require('@hapi/boom');
2
2
  const { getLogger } = require('../../logs');
3
- const { AUTH_SOURCES } = require('../user');
4
3
 
5
4
  const log = getLogger('frigg.user.authentication');
6
5
 
@@ -69,13 +68,10 @@ class AuthenticateUser {
69
68
  // Validate the API key (authentication)
70
69
  await this.authenticateWithSharedSecret.execute(apiKey);
71
70
  // Get user from x-frigg headers (authorization)
72
- const user = await this.getUserFromXFriggHeaders.execute(
71
+ return await this.getUserFromXFriggHeaders.execute(
73
72
  appUserId,
74
73
  appOrgId
75
74
  );
76
- // The app's own backend: trusted to manage organization things.
77
- user.setAuthSource(AUTH_SOURCES.SHARED_SECRET);
78
- return user;
79
75
  }
80
76
  log.debug('No x-frigg-api-key header, skipping shared secret', {
81
77
  eventName: `${log.name}.shared_secret_skipped`,
@@ -94,7 +90,6 @@ class AuthenticateUser {
94
90
  // Detect JWT format (3 parts separated by dots)
95
91
  if (token && token.split('.').length === 3) {
96
92
  const user = await this.getUserFromAdopterJwt.execute(token);
97
- user.setAuthSource(AUTH_SOURCES.ADOPTER_JWT);
98
93
  // Validate x-frigg headers match JWT claims if present
99
94
  if (appUserId || appOrgId) {
100
95
  this.validateUserMatch(user, appUserId, appOrgId);
@@ -111,7 +106,6 @@ class AuthenticateUser {
111
106
  const user = await this.getUserFromBearerToken.execute(
112
107
  req.headers.authorization
113
108
  );
114
- user.setAuthSource(AUTH_SOURCES.BEARER_TOKEN);
115
109
  // Validate x-frigg headers match token user if present
116
110
  if (appUserId || appOrgId) {
117
111
  this.validateUserMatch(user, appUserId, appOrgId);
@@ -74,14 +74,8 @@ class GetUserFromAdopterJwt {
74
74
  const role = claims[roleClaim];
75
75
 
76
76
  const orgRole = typeof role === 'string' && role !== '' ? role : null;
77
- const { user, membership } = await this._findOrCreateUser(
78
- appUserId,
79
- appOrgId,
80
- orgRole
81
- );
77
+ const user = await this._findOrCreateUser(appUserId, appOrgId, orgRole);
82
78
  user.setOrgRole(orgRole);
83
- // What the membership row says: the role when the token carries none.
84
- user.setMembershipRole(membership?.orgRole ?? null);
85
79
 
86
80
  log.debug('Adopter token accepted', {
87
81
  eventName: `${log.name}.accepted`,
@@ -104,16 +98,12 @@ class GetUserFromAdopterJwt {
104
98
 
105
99
  // The token's organization is the active one whether or not this is
106
100
  // the person's first time in it.
107
- const { individual, membership } =
108
- await this.ensureOrganizationMembership.execute({
109
- individual: created.individual,
110
- organization: created.organization,
111
- orgRole,
112
- });
113
- return {
114
- user: buildUser(cfg, individual, created.organization),
115
- membership,
116
- };
101
+ const { individual } = await this.ensureOrganizationMembership.execute({
102
+ individual: created.individual,
103
+ organization: created.organization,
104
+ orgRole,
105
+ });
106
+ return buildUser(cfg, individual, created.organization);
117
107
  }
118
108
  }
119
109
 
package/user/user.js CHANGED
@@ -1,20 +1,5 @@
1
1
  const bcrypt = require('bcryptjs');
2
2
 
3
- /**
4
- * How the request that produced a `User` proved who it is. Set by
5
- * AuthenticateUser, never inferred from a missing value. Organization roles
6
- * (ADR-056 §3, §3.6) depend on it: a shared-secret caller is the app's own
7
- * backend and is trusted to manage; a Frigg bearer token keeps its pre-roles
8
- * behaviour unless the app opts in (`user.orgAdminGateBearerTokens`); an
9
- * adopter token has the role it carries, else the stored membership role,
10
- * else `MEMBER`.
11
- */
12
- const AUTH_SOURCES = Object.freeze({
13
- SHARED_SECRET: 'shared-secret',
14
- ADOPTER_JWT: 'adopter-jwt',
15
- BEARER_TOKEN: 'bearer-token',
16
- });
17
-
18
3
  /**
19
4
  * Represents a user in the system. The User class is a domain entity,
20
5
  * @class User
@@ -33,14 +18,9 @@ class User {
33
18
  this.individualUser = individualUser;
34
19
  this.organizationUser = organizationUser;
35
20
  this.usePassword = usePassword;
36
- // Organization role from an adopter token claim (ADR-056 §2), per
37
- // request. When present it wins over the stored membership role.
21
+ // Organization role from an adopter token claim (ADR-056 §2). Per request:
22
+ // the role stored on a membership is only what the first token carried.
38
23
  this.orgRole = null;
39
- // The role stored on the person's membership of the active organization
40
- // (OrganizationMembership.orgRole); used when the token carries none.
41
- this.membershipRole = null;
42
- // How this request authenticated (one of AUTH_SOURCES); null until AuthenticateUser says.
43
- this.authSource = null;
44
24
 
45
25
  this.config = {
46
26
  primary,
@@ -104,14 +84,6 @@ class User {
104
84
  return this.config.individualUserRequired;
105
85
  }
106
86
 
107
- getAuthSource() {
108
- return this.authSource;
109
- }
110
-
111
- setAuthSource(authSource) {
112
- this.authSource = authSource ?? null;
113
- }
114
-
115
87
  getOrgRole() {
116
88
  return this.orgRole;
117
89
  }
@@ -120,14 +92,6 @@ class User {
120
92
  this.orgRole = orgRole ?? null;
121
93
  }
122
94
 
123
- getMembershipRole() {
124
- return this.membershipRole;
125
- }
126
-
127
- setMembershipRole(membershipRole) {
128
- this.membershipRole = membershipRole ?? null;
129
- }
130
-
131
95
  getIndividualUser() {
132
96
  return this.individualUser;
133
97
  }
@@ -185,4 +149,4 @@ class User {
185
149
  }
186
150
  }
187
151
 
188
- module.exports = { User, AUTH_SOURCES };
152
+ module.exports = { User };
@@ -1,21 +0,0 @@
1
- const Boom = require('@hapi/boom');
2
- const { ACCESS } = require('../scope-resolution');
3
-
4
- /**
5
- * The one check behind every "may this caller change or delete it" gate
6
- * (ADR-056 §3 permissions): the caller may use it but not manage it, so 403
7
- * `ORG_ADMIN_REQUIRED`. `access` comes from describeRecord (entities,
8
- * integrations) or scopePolicy.accessToOwnedBy (credentials).
9
- *
10
- * @param {string|null} access - ACCESS value
11
- * @param {string} subject - e.g. `entity 5`, for the message
12
- */
13
- function assertCanManage(access, subject) {
14
- if (access !== ACCESS.MANAGE) {
15
- throw Boom.forbidden(`You may use ${subject} but not change or delete it`, {
16
- code: 'ORG_ADMIN_REQUIRED',
17
- });
18
- }
19
- }
20
-
21
- module.exports = { assertCanManage };