@nymphjs/tilmeld 1.0.0-beta.14 → 1.0.0-beta.140

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.
Files changed (105) hide show
  1. package/CHANGELOG.md +668 -0
  2. package/README.md +21 -5
  3. package/dist/AbleObject.d.ts +20 -2
  4. package/dist/AbleObject.js +25 -13
  5. package/dist/AbleObject.js.map +1 -1
  6. package/dist/Group.d.ts +190 -27
  7. package/dist/Group.js +827 -440
  8. package/dist/Group.js.map +1 -1
  9. package/dist/Group.testArtifacts.d.ts +3 -0
  10. package/dist/Group.testArtifacts.js +120 -0
  11. package/dist/Group.testArtifacts.js.map +1 -0
  12. package/dist/Tilmeld.d.ts +275 -14
  13. package/dist/Tilmeld.js +789 -310
  14. package/dist/Tilmeld.js.map +1 -1
  15. package/dist/Tilmeld.types.d.ts +9 -9
  16. package/dist/Tilmeld.types.js +1 -2
  17. package/dist/User.d.ts +410 -41
  18. package/dist/User.js +2042 -1057
  19. package/dist/User.js.map +1 -1
  20. package/dist/User.testArtifacts.d.ts +3 -0
  21. package/dist/User.testArtifacts.js +175 -0
  22. package/dist/User.testArtifacts.js.map +1 -0
  23. package/dist/conf/defaults.d.ts +1 -1
  24. package/dist/conf/defaults.js +215 -141
  25. package/dist/conf/defaults.js.map +1 -1
  26. package/dist/conf/index.d.ts +361 -2
  27. package/dist/conf/index.js +2 -8
  28. package/dist/conf/index.js.map +1 -1
  29. package/dist/decorators/index.d.ts +2 -0
  30. package/dist/decorators/index.js +3 -0
  31. package/dist/decorators/index.js.map +1 -0
  32. package/dist/decorators/requireAbility.d.ts +10 -0
  33. package/dist/decorators/requireAbility.js +21 -0
  34. package/dist/decorators/requireAbility.js.map +1 -0
  35. package/dist/decorators/requireAuthentication.d.ts +8 -0
  36. package/dist/decorators/requireAuthentication.js +16 -0
  37. package/dist/decorators/requireAuthentication.js.map +1 -0
  38. package/dist/enforceTilmeld.d.ts +3 -0
  39. package/dist/enforceTilmeld.js +18 -0
  40. package/dist/enforceTilmeld.js.map +1 -0
  41. package/dist/errors/AccessControlError.d.ts +2 -1
  42. package/dist/errors/AccessControlError.js +3 -6
  43. package/dist/errors/AccessControlError.js.map +1 -1
  44. package/dist/errors/BadDataError.d.ts +2 -1
  45. package/dist/errors/BadDataError.js +3 -6
  46. package/dist/errors/BadDataError.js.map +1 -1
  47. package/dist/errors/BadEmailError.d.ts +2 -1
  48. package/dist/errors/BadEmailError.js +3 -6
  49. package/dist/errors/BadEmailError.js.map +1 -1
  50. package/dist/errors/BadUsernameError.d.ts +2 -1
  51. package/dist/errors/BadUsernameError.js +3 -6
  52. package/dist/errors/BadUsernameError.js.map +1 -1
  53. package/dist/errors/CouldNotChangeDefaultPrimaryGroupError.js +1 -5
  54. package/dist/errors/CouldNotChangeDefaultPrimaryGroupError.js.map +1 -1
  55. package/dist/errors/EmailChangeRateLimitExceededError.js +1 -5
  56. package/dist/errors/EmailChangeRateLimitExceededError.js.map +1 -1
  57. package/dist/errors/index.d.ts +6 -6
  58. package/dist/errors/index.js +6 -22
  59. package/dist/errors/index.js.map +1 -1
  60. package/dist/index.d.ts +10 -14
  61. package/dist/index.js +11 -35
  62. package/dist/index.js.map +1 -1
  63. package/dist/testArtifacts.d.ts +26 -0
  64. package/dist/testArtifacts.js +1346 -0
  65. package/dist/testArtifacts.js.map +1 -0
  66. package/dist/tilmeldJoiProps.d.ts +4 -5
  67. package/dist/tilmeldJoiProps.js +13 -18
  68. package/dist/tilmeldJoiProps.js.map +1 -1
  69. package/jest.config.js +31 -5
  70. package/package.json +49 -36
  71. package/src/AbleObject.ts +17 -11
  72. package/src/Group.testArtifacts.ts +159 -0
  73. package/src/Group.ts +712 -299
  74. package/src/Tilmeld.ts +666 -344
  75. package/src/Tilmeld.types.ts +9 -9
  76. package/src/User.testArtifacts.ts +226 -0
  77. package/src/User.ts +1984 -879
  78. package/src/conf/defaults.ts +205 -109
  79. package/src/conf/index.ts +381 -2
  80. package/src/decorators/index.ts +2 -0
  81. package/src/decorators/requireAbility.ts +26 -0
  82. package/src/decorators/requireAuthentication.ts +19 -0
  83. package/src/enforceTilmeld.ts +20 -0
  84. package/src/errors/AccessControlError.ts +4 -2
  85. package/src/errors/BadDataError.ts +4 -2
  86. package/src/errors/BadEmailError.ts +4 -2
  87. package/src/errors/BadUsernameError.ts +4 -2
  88. package/src/errors/index.ts +6 -6
  89. package/src/index.ts +11 -14
  90. package/src/testArtifacts.ts +2148 -0
  91. package/src/tilmeldJoiProps.ts +9 -24
  92. package/tsconfig.json +6 -3
  93. package/typedoc.json +4 -0
  94. package/dist/Group.test.d.ts +0 -1
  95. package/dist/Group.test.js +0 -48
  96. package/dist/Group.test.js.map +0 -1
  97. package/dist/User.test.d.ts +0 -1
  98. package/dist/User.test.js +0 -51
  99. package/dist/User.test.js.map +0 -1
  100. package/dist/conf/d.d.ts +0 -55
  101. package/dist/conf/d.js +0 -3
  102. package/dist/conf/d.js.map +0 -1
  103. package/src/Group.test.ts +0 -58
  104. package/src/User.test.ts +0 -61
  105. package/src/conf/d.ts +0 -303
package/src/Tilmeld.ts CHANGED
@@ -1,27 +1,36 @@
1
+ import { URL } from 'node:url';
1
2
  import {
2
3
  Nymph,
4
+ Entity,
3
5
  EntityInterface,
4
6
  FormattedSelector,
5
7
  Options,
6
8
  TilmeldInterface,
7
9
  TilmeldAccessLevels,
10
+ TilmeldAccessRequest,
11
+ ACProperties,
12
+ entitiesToReferences,
13
+ entityConstructorsToClassNames,
14
+ classNamesToEntityConstructors,
15
+ referencesToEntities,
8
16
  } from '@nymphjs/nymph';
9
- import { URL } from 'url';
10
17
  import { Request, Response } from 'express';
11
- import { xor } from 'lodash';
18
+ import { xor } from 'lodash-es';
19
+ import { splitn } from '@sciactive/splitn';
12
20
 
13
- import { Config, ConfigDefaults as defaults } from './conf';
14
- import { AccessControlError } from './errors';
15
- import Group from './Group';
16
- import User, { UserData } from './User';
17
- import { AccessControlData } from './Tilmeld.types';
21
+ import { Config, ConfigDefaults as defaults } from './conf/index.js';
22
+ import { AccessControlError } from './errors/index.js';
23
+ import { Group } from './Group.js';
24
+ import { User } from './User.js';
25
+ import { AccessControlData } from './Tilmeld.types.js';
26
+ import { enforceTilmeld } from './enforceTilmeld.js';
18
27
 
19
28
  /**
20
29
  * A user and group system for Nymph.js.
21
30
  *
22
31
  * Written by Hunter Perrin for SciActive.
23
32
  *
24
- * @author Hunter Perrin <hperrin@gmail.com>
33
+ * @author Hunter Perrin <hperrin@port87.com>
25
34
  * @copyright SciActive Inc
26
35
  * @see http://nymph.io/
27
36
  */
@@ -39,7 +48,7 @@ export default class Tilmeld implements TilmeldInterface {
39
48
  /**
40
49
  * The currently logged in user.
41
50
  */
42
- public currentUser: (User & UserData) | null = null;
51
+ public currentUser: User | null = null;
43
52
 
44
53
  /**
45
54
  * The user class for this instance of Tilmeld.
@@ -69,9 +78,17 @@ export default class Tilmeld implements TilmeldInterface {
69
78
  public response: Response | null = null;
70
79
 
71
80
  /**
72
- * Skip the authenticate step of initialization.
81
+ * Used to avoid infinite loop.
73
82
  */
74
- private skipAuthenticate = false;
83
+ private alreadyLoggedOutSwitch = false;
84
+
85
+ /**
86
+ * Gatekeeper ability cache.
87
+ *
88
+ * Gatekeeper will cache the user's abilities that it calculates, so it can
89
+ * check faster if that user has been checked before.
90
+ */
91
+ private gatekeeperCache: { [k: string]: true } | null = null;
75
92
 
76
93
  /**
77
94
  * Create a new instance of Tilmeld.
@@ -80,6 +97,30 @@ export default class Tilmeld implements TilmeldInterface {
80
97
  */
81
98
  public constructor(config: Partial<Config> & { jwtSecret: string }) {
82
99
  this.config = { ...defaults, ...config };
100
+
101
+ if (
102
+ this.config.emailUsernames &&
103
+ (!this.config.userFields.includes('email') ||
104
+ !this.config.regFields.includes('email'))
105
+ ) {
106
+ throw new Error(
107
+ 'You must include email in userFields and regFields if you use emailUsernames.',
108
+ );
109
+ }
110
+
111
+ for (let field of this.config.regFields) {
112
+ if (!this.config.userFields.includes(field)) {
113
+ throw new Error(
114
+ `All fields in regFields must also be in userFields: ${field}`,
115
+ );
116
+ }
117
+ }
118
+
119
+ if (this.config.emailUsernames && this.config.domainSupport) {
120
+ throw new Error(
121
+ 'Email usernames and domain support cannot both be enabled.',
122
+ );
123
+ }
83
124
  }
84
125
 
85
126
  /**
@@ -92,7 +133,7 @@ export default class Tilmeld implements TilmeldInterface {
92
133
  tilmeld.request = this.request;
93
134
  tilmeld.response = this.response;
94
135
  tilmeld.currentUser = this.currentUser;
95
- tilmeld.skipAuthenticate = true;
136
+ tilmeld.gatekeeperCache = this.gatekeeperCache;
96
137
 
97
138
  return tilmeld;
98
139
  }
@@ -106,32 +147,85 @@ export default class Tilmeld implements TilmeldInterface {
106
147
  */
107
148
  public init(nymph: Nymph) {
108
149
  this.nymph = nymph;
109
- const tilmeld = this;
110
150
 
111
- // Set up access control hooks when Nymph is called.
112
151
  if (this.nymph.driver == null) {
113
152
  throw new Error("Tilmeld can't be configured before Nymph.");
114
153
  }
115
154
 
116
155
  // Configure the classes.
117
- class NymphUser extends User {
118
- static nymph: Nymph = nymph;
119
- static tilmeld: Tilmeld = tilmeld;
156
+ if (nymph.parent) {
157
+ // This is a cloned Nymph, it should have classes already.
158
+ this.User = this.nymph.getEntityClass(User);
159
+ this.Group = this.nymph.getEntityClass(Group);
160
+ } else {
161
+ // This is a new Nymph, add the classes.
162
+ this.User = this.nymph.addEntityClass(User);
163
+ this.Group = this.nymph.addEntityClass(Group);
120
164
  }
121
- (NymphUser as any).skipOnClone = true;
122
- this.User = this.nymph.addEntityClass(NymphUser);
123
- class NymphGroup extends Group {
124
- static nymph: Nymph = nymph;
125
- static tilmeld: Tilmeld = tilmeld;
165
+
166
+ // Clone the current user so its Nymph instance is correct.
167
+ if (this.currentUser) {
168
+ const currentUserGUID = this.currentUser.guid;
169
+ const currentUserCDate = this.currentUser.cdate;
170
+ const currentUserMDate = this.currentUser.mdate;
171
+ const currentUserTags = this.currentUser.tags;
172
+ const currentUserData = entityConstructorsToClassNames(
173
+ entitiesToReferences(this.currentUser.$getData(false), true),
174
+ );
175
+ const currentUserSData = this.currentUser.$getSData();
176
+ const currentUserOriginalAcValues =
177
+ this.currentUser.$getOriginalAcValues();
178
+
179
+ const user = this.User.factorySync();
180
+ user.guid = currentUserGUID;
181
+ user.cdate = currentUserCDate;
182
+ user.mdate = currentUserMDate;
183
+ user.tags = currentUserTags;
184
+ // First, put the trusted original AC values in with the server flag.
185
+ Entity.factorySync().$putData.call(
186
+ user,
187
+ {
188
+ ...currentUserOriginalAcValues,
189
+ user:
190
+ currentUserOriginalAcValues.user != null
191
+ ? this.User.factoryReference([
192
+ 'nymph_entity_reference',
193
+ currentUserOriginalAcValues.user,
194
+ this.User.class,
195
+ ])
196
+ : undefined,
197
+ group:
198
+ currentUserOriginalAcValues.group != null
199
+ ? this.Group.factoryReference([
200
+ 'nymph_entity_reference',
201
+ currentUserOriginalAcValues.group,
202
+ this.Group.class,
203
+ ])
204
+ : undefined,
205
+ },
206
+ {},
207
+ 'server',
208
+ );
209
+ // Now put the real data in, that could potentially have been altered from
210
+ // the client.
211
+ user.$putData(
212
+ classNamesToEntityConstructors(
213
+ this.nymph,
214
+ referencesToEntities(currentUserData, this.nymph, false, false),
215
+ false,
216
+ ),
217
+ currentUserSData,
218
+ );
219
+ user.$originalEmail = this.currentUser.$originalEmail;
220
+ user.$originalUsername = this.currentUser.$originalUsername;
221
+ this.currentUser = user;
222
+ this.currentUser.$updateDataProtection();
223
+ this.currentUser.$updateGroupDataProtection();
126
224
  }
127
- (NymphUser as any).skipOnClone = true;
128
- this.Group = this.nymph.addEntityClass(NymphGroup);
129
225
 
226
+ // Set up access control hooks when Nymph is called.
227
+ this.nymph.config.debugInfo('tilmeld', 'Initializing access controls.');
130
228
  this.initAccessControl();
131
- if (this.request != null && !this.skipAuthenticate) {
132
- this.authenticate();
133
- }
134
- this.skipAuthenticate = false;
135
229
  }
136
230
 
137
231
  /**
@@ -143,21 +237,29 @@ export default class Tilmeld implements TilmeldInterface {
143
237
  * @param ability The ability.
144
238
  * @returns Whether the user has the given ability.
145
239
  */
146
- public gatekeeper(ability?: string): boolean {
147
- if (this.currentUser == null) {
240
+ public gatekeeper(ability?: string) {
241
+ if (this.currentUser == null || this.gatekeeperCache == null) {
148
242
  return false;
149
243
  }
150
- return this.currentUser.$gatekeeper(ability);
244
+ if (ability == null) {
245
+ return true;
246
+ }
247
+ return (
248
+ (ability in this.gatekeeperCache && !!this.gatekeeperCache[ability]) ||
249
+ !!this.currentUser.abilities?.includes('system/admin')
250
+ );
151
251
  }
152
252
 
153
253
  private initAccessControl() {
154
254
  // Check for the skip access control option and add AC selectors.
155
255
  const handleQuery = function (
156
256
  nymph: Nymph,
157
- options: Options,
158
- selectors: FormattedSelector[]
257
+ {
258
+ options,
259
+ selectors,
260
+ }: { options: Options; selectors: FormattedSelector[] },
159
261
  ) {
160
- const tilmeld = nymph.tilmeld as Tilmeld;
262
+ const tilmeld = enforceTilmeld(nymph);
161
263
  if (
162
264
  !options ||
163
265
  !('skipAc' in options) ||
@@ -166,13 +268,17 @@ export default class Tilmeld implements TilmeldInterface {
166
268
  ) {
167
269
  if (options?.source === 'client') {
168
270
  if (
169
- !tilmeld.gatekeeper('tilmeld/admin') && // The user is not an admin.
271
+ (!tilmeld.currentUser ||
272
+ (!tilmeld.currentUser.abilities?.includes('tilmeld/admin') &&
273
+ !tilmeld.currentUser.abilities?.includes('system/admin'))) && // The user is not an admin.
170
274
  ((!tilmeld.config.enableUserSearch &&
171
275
  options.class?.factorySync() instanceof User) || // The use is searching for a user and searching is disabled.
172
276
  (!tilmeld.config.enableGroupSearch &&
173
277
  options.class?.factorySync() instanceof Group)) && // Or the same, but for a group.
174
- (selectors[0] == null || // No selector specified.
278
+ (selectors.length > 1 || // There's more than one selector.
279
+ selectors[0] == null || // Or no selector specified.
175
280
  selectors[0].type !== '&' || // Or the type is not and.
281
+ Object.keys(selectors[0]).length > 2 || // Or there's more than one clause.
176
282
  (!('guid' in selectors[0]) && !('equal' in selectors[0])) || // Or they're searching for something other than guid or equal.
177
283
  ('guid' in selectors[0] &&
178
284
  (!selectors[0].guid?.length ||
@@ -195,10 +301,11 @@ export default class Tilmeld implements TilmeldInterface {
195
301
  // Filter entities being deleted for user permissions.
196
302
  const checkPermissionsDelete = async function (
197
303
  nymph: Nymph,
198
- entity: EntityInterface
304
+ entity: EntityInterface,
199
305
  ) {
200
- const tilmeld = nymph.tilmeld as Tilmeld;
306
+ const tilmeld = enforceTilmeld(nymph);
201
307
  if (
308
+ '$tilmeldDeleteSkipAC' in entity &&
202
309
  typeof entity.$tilmeldDeleteSkipAC === 'function' &&
203
310
  entity.$tilmeldDeleteSkipAC()
204
311
  ) {
@@ -215,15 +322,19 @@ export default class Tilmeld implements TilmeldInterface {
215
322
  const checkPermissionsDeleteByID = async function (
216
323
  nymph: Nymph,
217
324
  guid: string,
218
- className?: string
325
+ className?: string,
219
326
  ) {
220
- const entity = nymph.driver.getEntitySync(
327
+ if (className === 'User' || className === 'Group') {
328
+ throw new AccessControlError("You can't delete users or groups by ID.");
329
+ }
330
+ const entity = await nymph.getEntity(
221
331
  { class: nymph.getEntityClass(className ?? 'Entity') },
222
- { type: '&', guid: guid }
332
+ { type: '&', guid: guid },
223
333
  );
224
- if (entity != null) {
225
- checkPermissionsDelete(nymph, entity);
334
+ if (entity == null) {
335
+ throw new AccessControlError("You can't delete this entity by ID.");
226
336
  }
337
+ await checkPermissionsDelete(nymph, entity);
227
338
  };
228
339
  checkPermissionsDeleteByID.skipOnClone = true;
229
340
 
@@ -231,13 +342,14 @@ export default class Tilmeld implements TilmeldInterface {
231
342
  // disallowed changes to AC properties.
232
343
  const checkPermissionsSaveAndFilterAcChanges = async function (
233
344
  nymph: Nymph,
234
- entity: EntityInterface & AccessControlData
345
+ entity: EntityInterface & AccessControlData,
235
346
  ) {
236
- const tilmeld = nymph.tilmeld as Tilmeld;
347
+ const tilmeld = enforceTilmeld(nymph);
237
348
  if (!entity) {
238
349
  return;
239
350
  }
240
351
  if (
352
+ '$tilmeldSaveSkipAC' in entity &&
241
353
  typeof entity.$tilmeldSaveSkipAC === 'function' &&
242
354
  entity.$tilmeldSaveSkipAC()
243
355
  ) {
@@ -249,35 +361,9 @@ export default class Tilmeld implements TilmeldInterface {
249
361
  // allowing a change to ac properties.
250
362
 
251
363
  const originalAc = entity.$getOriginalAcValues();
252
- const newAc: AccessControlData = {
253
- user: entity.user ?? undefined,
254
- group: entity.group ?? undefined,
255
- acUser: entity.acUser ?? undefined,
256
- acGroup: entity.acGroup ?? undefined,
257
- acOther: entity.acOther ?? undefined,
258
- acRead: entity.acRead ?? undefined,
259
- acWrite: entity.acWrite ?? undefined,
260
- acFull: entity.acFull ?? undefined,
261
- };
364
+ const newAc = entity.$getCurrentAcValues();
262
365
 
263
- const setAcProperties = (acValues: { [k: string]: any }) => {
264
- for (let name in acValues) {
265
- const value = acValues[name];
266
- if (value == null) {
267
- delete entity[name];
268
- } else {
269
- entity[name] = value;
270
- }
271
- }
272
- };
273
-
274
- // Restore original AC properties and check permissions.
275
- setAcProperties(originalAc);
276
-
277
- function areAcPropertiesChanged(
278
- a: AccessControlData,
279
- b: AccessControlData
280
- ) {
366
+ function areAcPropertiesChanged(a: ACProperties, b: ACProperties) {
281
367
  for (const name of ['user', 'group'] as ('user' | 'group')[]) {
282
368
  const aVal = a[name];
283
369
  const bVal = b[name];
@@ -286,14 +372,12 @@ export default class Tilmeld implements TilmeldInterface {
286
372
  } else if (aVal == null || bVal == null) {
287
373
  return true;
288
374
  }
289
- if (!aVal.$is(bVal)) {
375
+ if (aVal !== bVal) {
290
376
  return true;
291
377
  }
292
378
  }
293
379
  for (const name of ['acUser', 'acGroup', 'acOther'] as (
294
- | 'acUser'
295
- | 'acGroup'
296
- | 'acOther'
380
+ 'acUser' | 'acGroup' | 'acOther'
297
381
  )[]) {
298
382
  const aVal = a[name];
299
383
  const bVal = b[name];
@@ -307,9 +391,7 @@ export default class Tilmeld implements TilmeldInterface {
307
391
  }
308
392
  }
309
393
  for (const name of ['acRead', 'acWrite', 'acFull'] as (
310
- | 'acRead'
311
- | 'acWrite'
312
- | 'acFull'
394
+ 'acRead' | 'acWrite' | 'acFull'
313
395
  )[]) {
314
396
  const aVal = a[name];
315
397
  const bVal = b[name];
@@ -321,12 +403,7 @@ export default class Tilmeld implements TilmeldInterface {
321
403
  if (aVal.length !== bVal.length) {
322
404
  return true;
323
405
  }
324
- if (
325
- xor(
326
- aVal.map((entity) => entity.guid),
327
- bVal.map((entity) => entity.guid)
328
- ).length
329
- ) {
406
+ if (xor(aVal, bVal).length) {
330
407
  return true;
331
408
  }
332
409
  }
@@ -335,41 +412,41 @@ export default class Tilmeld implements TilmeldInterface {
335
412
 
336
413
  if (areAcPropertiesChanged(originalAc, newAc)) {
337
414
  if (
338
- !tilmeld.checkPermissions(entity, TilmeldAccessLevels.FULL_ACCESS)
415
+ !tilmeld.checkPermissions(
416
+ entity,
417
+ TilmeldAccessLevels.FULL_ACCESS,
418
+ originalAc,
419
+ )
339
420
  ) {
340
421
  // Only allow changes to AC properties if the user has full access.
341
422
  throw new AccessControlError(
342
- 'No permission to change access control properties.'
423
+ 'No permission to change access control properties.',
343
424
  );
344
425
  }
345
426
 
346
427
  if (
347
- ((originalAc.user && !originalAc.user.$is(newAc.user)) ||
348
- (!originalAc.user &&
349
- newAc.user &&
350
- !newAc.user.$is(tilmeld.currentUser))) &&
351
- !tilmeld.gatekeeper('tilmeld/admin')
428
+ (originalAc.user != null && originalAc.user !== newAc.user) ||
429
+ (originalAc.user == null &&
430
+ newAc.user != null &&
431
+ newAc.user !== tilmeld.currentUser?.guid)
352
432
  ) {
353
433
  throw new AccessControlError(
354
- 'No permission to assign to another user.'
434
+ 'No permission to assign to another user.',
355
435
  );
356
436
  }
357
437
 
358
438
  if (
359
- newAc.group &&
360
- !newAc.group.$is(originalAc.group) &&
439
+ newAc.group != null &&
440
+ newAc.group !== originalAc.group &&
361
441
  !(
362
- newAc.group.$is(tilmeld.currentUser?.group) ||
363
- newAc.group.$inArray(tilmeld.currentUser?.groups ?? [])
364
- ) &&
365
- !tilmeld.gatekeeper('tilmeld/admin')
442
+ newAc.group === tilmeld.currentUser?.$getGid() ||
443
+ (tilmeld.currentUser?.$getGids() ?? []).includes(newAc.group)
444
+ )
366
445
  ) {
367
446
  throw new AccessControlError(
368
- 'No permission to assign to another group.'
447
+ 'No permission to assign to another group.',
369
448
  );
370
449
  }
371
-
372
- setAcProperties(newAc);
373
450
  }
374
451
  }
375
452
 
@@ -398,9 +475,9 @@ export default class Tilmeld implements TilmeldInterface {
398
475
  */
399
476
  const addAccess = async function (
400
477
  nymph: Nymph,
401
- entity: EntityInterface & AccessControlData
478
+ entity: EntityInterface & AccessControlData,
402
479
  ) {
403
- const tilmeld = nymph.tilmeld as Tilmeld;
480
+ const tilmeld = enforceTilmeld(nymph);
404
481
  const user = tilmeld.currentUser;
405
482
  if (
406
483
  user != null &&
@@ -411,12 +488,11 @@ export default class Tilmeld implements TilmeldInterface {
411
488
  if (entity.user == null) {
412
489
  entity.user = user;
413
490
  }
414
- if (
415
- entity.group == null &&
416
- user.group != null &&
417
- user.group.guid != null
418
- ) {
419
- entity.group = user.group;
491
+ if (entity.group == null) {
492
+ const group = user.group;
493
+ if (group != null && group.guid != null) {
494
+ entity.group = group;
495
+ }
420
496
  }
421
497
  if (entity.acUser == null) {
422
498
  entity.acUser = TilmeldAccessLevels.FULL_ACCESS;
@@ -442,7 +518,7 @@ export default class Tilmeld implements TilmeldInterface {
442
518
 
443
519
  const validate = async function (
444
520
  _nymph: Nymph,
445
- entity: EntityInterface & AccessControlData
521
+ entity: EntityInterface & AccessControlData,
446
522
  ) {
447
523
  if (!(entity instanceof User) && !(entity instanceof Group)) {
448
524
  const ownershipAcPropertyValidator = (prop: any) => {
@@ -452,7 +528,7 @@ export default class Tilmeld implements TilmeldInterface {
452
528
  prop > TilmeldAccessLevels.FULL_ACCESS
453
529
  ) {
454
530
  throw new AccessControlError(
455
- 'Invalid access control property: ' + prop
531
+ 'Invalid access control property: ' + prop,
456
532
  );
457
533
  }
458
534
  };
@@ -460,13 +536,13 @@ export default class Tilmeld implements TilmeldInterface {
460
536
  const accessAcPropertyValidator = (prop: any) => {
461
537
  if (!Array.isArray(prop)) {
462
538
  throw new AccessControlError(
463
- 'Invalid access control property: ' + prop
539
+ 'Invalid access control property: ' + prop,
464
540
  );
465
541
  }
466
542
  prop.forEach((value) => {
467
- if (!(value instanceof User || value instanceof Group)) {
543
+ if (typeof value !== 'string' || !value.match(/^[0-9a-f]{24}$/i)) {
468
544
  throw new AccessControlError(
469
- 'Invalid access control property: ' + prop
545
+ 'Invalid access control property: ' + prop,
470
546
  );
471
547
  }
472
548
  });
@@ -475,7 +551,7 @@ export default class Tilmeld implements TilmeldInterface {
475
551
  if ('user' in entity) {
476
552
  if (!(entity.user instanceof User)) {
477
553
  throw new AccessControlError(
478
- 'Invalid access control property: user'
554
+ 'Invalid access control property: user',
479
555
  );
480
556
  }
481
557
  }
@@ -483,7 +559,7 @@ export default class Tilmeld implements TilmeldInterface {
483
559
  if ('group' in entity) {
484
560
  if (!(entity.group instanceof Group)) {
485
561
  throw new AccessControlError(
486
- 'Invalid access control property: group'
562
+ 'Invalid access control property: group',
487
563
  );
488
564
  }
489
565
  }
@@ -528,83 +604,134 @@ export default class Tilmeld implements TilmeldInterface {
528
604
  */
529
605
  public addAccessControlSelectors(
530
606
  options: Options,
531
- selectors: FormattedSelector[]
607
+ selectors: FormattedSelector[],
532
608
  ) {
533
609
  const user = this.currentUser;
534
610
 
535
- if (
536
- user != null &&
537
- (user.abilities?.indexOf('system/admin') ?? -1) !== -1
538
- ) {
539
- // The user is a system admin, so they can see everything.
540
- return;
541
- }
542
-
543
611
  if (options == null) {
544
612
  throw new Error('No options in argument.');
545
613
  } else if (
546
- 'class' in options &&
547
- (options.class?.factorySync() instanceof User ||
548
- options.class?.factorySync() instanceof Group)
614
+ !('class' in options) ||
615
+ !(options.class?.prototype instanceof Entity)
616
+ ) {
617
+ throw new Error('No class in options.');
618
+ } else if (options.class.class === 'Entity') {
619
+ throw new Error("Can't use Entity class directly.");
620
+ } else if (
621
+ options.class.factorySync() instanceof User ||
622
+ options.class.factorySync() instanceof Group
549
623
  ) {
550
624
  // They are requesting a user/group. Always accessible for reading.
551
625
  return;
552
626
  }
553
627
 
554
628
  if (user == null || user.guid == null) {
555
- selectors.push({
556
- type: '|',
629
+ // Access requests for only public entities.
630
+
631
+ const allAccessRequests =
632
+ TilmeldAccessRequest.OTHER_ACCESSIBLE | TilmeldAccessRequest.UNOWNED;
633
+ const requestedAccess = (options.acRequest ?? 0) & allAccessRequests;
634
+ const defaultAccess = requestedAccess === TilmeldAccessRequest.ALL_LEVELS;
635
+
636
+ const selector: FormattedSelector = { type: '|' };
637
+
638
+ if (
639
+ defaultAccess ||
640
+ requestedAccess & TilmeldAccessRequest.OTHER_ACCESSIBLE
641
+ ) {
557
642
  // Other access control is sufficient.
558
- gte: [['acOther', TilmeldAccessLevels.READ_ACCESS]],
643
+ selector.gte = [['acOther', TilmeldAccessLevels.READ_ACCESS]];
644
+ }
645
+
646
+ if (defaultAccess || requestedAccess & TilmeldAccessRequest.UNOWNED) {
559
647
  // The user and group are not set.
560
- selector: [
648
+ selector.selector = [
561
649
  {
562
650
  type: '&',
563
651
  '!defined': [['user', 'group']],
564
652
  },
565
- ],
566
- });
653
+ ];
654
+ }
655
+
656
+ if (Object.keys(selector).length <= 1) {
657
+ this.nymph.config.debugError(
658
+ 'tilmeld',
659
+ 'No access controls were added to the query. This should not be possible.',
660
+ );
661
+ throw new Error('Access control error.');
662
+ }
663
+
664
+ selectors.push(selector);
567
665
  } else {
568
- const subSelectors: FormattedSelector[] = [
569
- {
666
+ // Access requests for public and private entities.
667
+
668
+ const allAccessRequests =
669
+ TilmeldAccessRequest.USER_OWNED |
670
+ TilmeldAccessRequest.PRIMARY_GROUP_OWNED |
671
+ TilmeldAccessRequest.SECONDARY_GROUP_OWNED |
672
+ TilmeldAccessRequest.USER_ACCESSIBLE |
673
+ TilmeldAccessRequest.PRIMARY_GROUP_ACCESSIBLE |
674
+ TilmeldAccessRequest.SECONDARY_GROUP_ACCESSIBLE |
675
+ TilmeldAccessRequest.OTHER_ACCESSIBLE |
676
+ TilmeldAccessRequest.UNOWNED;
677
+ const requestedAccess = (options.acRequest ?? 0) & allAccessRequests;
678
+ const defaultAccess = requestedAccess === TilmeldAccessRequest.ALL_LEVELS;
679
+
680
+ const subSelectors: FormattedSelector[] = [];
681
+ if (defaultAccess || requestedAccess & TilmeldAccessRequest.UNOWNED) {
682
+ subSelectors.push({
570
683
  type: '&',
684
+ // The user and group are not set.
571
685
  '!defined': [['user', 'group']],
572
- },
686
+ });
687
+ }
688
+ // USER_OWNED is always enforced.
689
+ subSelectors.push(
573
690
  // It is owned by the user.
574
691
  {
575
692
  type: '&',
576
693
  ref: [['user', user.guid]],
577
694
  gte: [['acUser', TilmeldAccessLevels.READ_ACCESS]],
578
695
  },
579
- ];
696
+ );
580
697
  const groupRefs: FormattedSelector['ref'] = [];
581
- const acRefs: FormattedSelector['ref'] = [];
582
- if (user.group != null && user.group.guid != null) {
583
- // It belongs to the user's primary group.
584
- groupRefs.push(['group', user.group.guid]);
585
- // User's primary group is listed in acRead, acWrite, or acFull.
586
- acRefs.push(['acRead', user.group.guid]);
587
- acRefs.push(['acWrite', user.group.guid]);
588
- acRefs.push(['acFull', user.group.guid]);
589
- }
590
- for (let curSecondaryGroup of user.groups ?? []) {
591
- if (curSecondaryGroup != null && curSecondaryGroup.guid != null) {
592
- // It belongs to the user's secondary group.
593
- groupRefs.push(['group', curSecondaryGroup.guid]);
594
- // User's secondary group is listed in acRead, acWrite, or acFull.
595
- acRefs.push(['acRead', curSecondaryGroup.guid]);
596
- acRefs.push(['acWrite', curSecondaryGroup.guid]);
597
- acRefs.push(['acFull', curSecondaryGroup.guid]);
698
+ const acContains: FormattedSelector['contain'] = [];
699
+ const gid = user.$getGid();
700
+ if (gid != null) {
701
+ if (
702
+ defaultAccess ||
703
+ requestedAccess & TilmeldAccessRequest.PRIMARY_GROUP_OWNED
704
+ ) {
705
+ // It belongs to the user's primary group.
706
+ groupRefs.push(['group', gid]);
707
+ }
708
+ if (
709
+ defaultAccess ||
710
+ requestedAccess & TilmeldAccessRequest.PRIMARY_GROUP_ACCESSIBLE
711
+ ) {
712
+ // User's primary group guid is listed in acRead, acWrite, or acFull.
713
+ acContains.push(['acRead', gid]);
714
+ acContains.push(['acWrite', gid]);
715
+ acContains.push(['acFull', gid]);
598
716
  }
599
717
  }
600
- for (let curDescendantGroup of user.$getDescendantGroupsSync()) {
601
- if (curDescendantGroup != null && curDescendantGroup.guid != null) {
718
+ const gids = user.$getGids();
719
+ for (let curGid of gids ?? []) {
720
+ if (
721
+ defaultAccess ||
722
+ requestedAccess & TilmeldAccessRequest.SECONDARY_GROUP_OWNED
723
+ ) {
602
724
  // It belongs to the user's secondary group.
603
- groupRefs.push(['group', curDescendantGroup.guid]);
604
- // User's secondary group is listed in acRead, acWrite, or acFull.
605
- acRefs.push(['acRead', curDescendantGroup.guid]);
606
- acRefs.push(['acWrite', curDescendantGroup.guid]);
607
- acRefs.push(['acFull', curDescendantGroup.guid]);
725
+ groupRefs.push(['group', curGid]);
726
+ }
727
+ if (
728
+ defaultAccess ||
729
+ requestedAccess & TilmeldAccessRequest.SECONDARY_GROUP_ACCESSIBLE
730
+ ) {
731
+ // User's secondary group guid is listed in acRead, acWrite, or acFull.
732
+ acContains.push(['acRead', curGid]);
733
+ acContains.push(['acWrite', curGid]);
734
+ acContains.push(['acFull', curGid]);
608
735
  }
609
736
  }
610
737
  // All the group refs.
@@ -612,40 +739,49 @@ export default class Tilmeld implements TilmeldInterface {
612
739
  subSelectors.push({
613
740
  type: '&',
614
741
  gte: [['acGroup', TilmeldAccessLevels.READ_ACCESS]],
615
- selector: [
616
- {
617
- type: '|',
618
- ref: groupRefs,
619
- },
620
- ],
742
+ selector: [{ type: '|', ref: groupRefs }],
621
743
  });
622
744
  }
623
- // All the acRead, acWrite, and acFull refs.
624
- if (acRefs.length) {
625
- subSelectors.push({
626
- type: '|',
627
- ref: acRefs,
628
- });
745
+ // All the acRead, acWrite, and acFull contains.
746
+ if (acContains.length) {
747
+ subSelectors.push({ type: '|', contain: acContains });
629
748
  }
630
- const selector: FormattedSelector = {
631
- type: '|',
749
+ const selector: FormattedSelector = { type: '|' };
750
+ if (
751
+ defaultAccess ||
752
+ requestedAccess & TilmeldAccessRequest.OTHER_ACCESSIBLE
753
+ ) {
632
754
  // Other access control is sufficient.
633
- gte: [['acOther', TilmeldAccessLevels.READ_ACCESS]],
634
- // The user and group are not set.
635
- selector: subSelectors,
755
+ selector.gte = [['acOther', TilmeldAccessLevels.READ_ACCESS]];
756
+ }
757
+ if (subSelectors.length) {
758
+ // One of the sub selectors.
759
+ selector.selector = subSelectors;
760
+ }
761
+ if (
762
+ defaultAccess ||
763
+ requestedAccess & TilmeldAccessRequest.USER_ACCESSIBLE
764
+ ) {
636
765
  // The user is listed in acRead, acWrite, or acFull.
637
- ref: [
766
+ selector.contain = [
638
767
  ['acRead', user.guid],
639
768
  ['acWrite', user.guid],
640
769
  ['acFull', user.guid],
641
- ],
642
- };
770
+ ];
771
+ }
772
+ if (Object.keys(selector).length <= 1) {
773
+ this.nymph.config.debugError(
774
+ 'tilmeld',
775
+ 'No access controls were added to the query. This should not be possible.',
776
+ );
777
+ throw new Error('Access control error.');
778
+ }
643
779
  selectors.push(selector);
644
780
  }
645
781
  }
646
782
 
647
783
  /**
648
- * Check an entity's permissions for a user.
784
+ * Check an entity's permissions for the current user.
649
785
  *
650
786
  * This will check the AC (Access Control) properties of the entity. These
651
787
  * include the following properties:
@@ -671,7 +807,7 @@ export default class Tilmeld implements TilmeldInterface {
671
807
  * - FULL_ACCESS, the user has read, write, and delete access to the entity,
672
808
  * as well as being able to manage its access controls and ownership.
673
809
  *
674
- * These properties defaults to:
810
+ * These properties default to:
675
811
  *
676
812
  * - acUser = TilmeldAccessLevels.FULL_ACCESS
677
813
  * - acGroup = TilmeldAccessLevels.READ_ACCESS
@@ -686,9 +822,9 @@ export default class Tilmeld implements TilmeldInterface {
686
822
  * The following conditions will result in different checks, which determine
687
823
  * whether the check passes:
688
824
  *
689
- * - The user has the "system/admin" ability. (Always true.)
690
825
  * - It is a user or group. (True for READ_ACCESS or Tilmeld admins.)
691
- * - The entity has no "user" and no "group". (Always true.)
826
+ * - It is a user or group that belongs to a domain the current user is an
827
+ * admin for, and domain support is enabled. (Always true.)
692
828
  * - No user is logged in. (Check other AC.)
693
829
  * - The entity is the user. (Always true.)
694
830
  * - It is the user's primary group. (True for READ_ACCESS.)
@@ -705,54 +841,54 @@ export default class Tilmeld implements TilmeldInterface {
705
841
  *
706
842
  * @param entity The entity to check.
707
843
  * @param type The lowest level of permission to consider a pass.
708
- * @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
844
+ * @param acProperties The acProperties to use instead of getting them from the entity.
709
845
  * @returns Whether the current user has at least `type` permission for the entity.
710
846
  */
711
847
  public checkPermissions(
712
- entity: EntityInterface & AccessControlData,
848
+ entity: EntityInterface,
713
849
  type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
714
- user?: (User & UserData) | false
850
+ acProperties?: ACProperties,
715
851
  ) {
716
- // Only works for entities.
717
- if (typeof entity !== 'object' || typeof entity.$is !== 'function') {
718
- return false;
852
+ if (!acProperties) {
853
+ acProperties = entity.$getCurrentAcValues() as ACProperties;
719
854
  }
720
855
 
721
- let userOrNull: (User & UserData) | null = null;
722
- let userOrEmpty: User & UserData = this.User.factorySync();
723
- // Calculate the user.
724
- if (user == null) {
725
- userOrNull = this.currentUser;
726
- userOrEmpty = this.User.current(true);
727
- } else if (user !== false) {
728
- userOrNull = user;
729
- userOrEmpty = user;
730
- }
731
-
732
- if (userOrEmpty.$gatekeeper('system/admin')) {
733
- return true;
734
- }
856
+ let userOrEmpty: User = this.User.current(true);
735
857
 
736
858
  // Users and groups are always readable. Editable by Tilmeld admins.
737
859
  if (
738
860
  (entity instanceof User || entity instanceof Group) &&
739
861
  (type === TilmeldAccessLevels.READ_ACCESS ||
740
- userOrEmpty.$gatekeeper('tilmeld/admin'))
862
+ this.gatekeeper('tilmeld/admin') ||
863
+ this.gatekeeper('system/admin'))
741
864
  ) {
742
865
  return true;
743
866
  }
744
867
 
745
- // Entities with no owners are always editable.
746
- if (entity.user == null && entity.group == null) {
747
- return true;
868
+ // Users and groups that belong to a domain are readable and editable by
869
+ // that domain's admins.
870
+ if (
871
+ this.config.domainSupport &&
872
+ (entity instanceof User || entity instanceof Group)
873
+ ) {
874
+ const [_local, domain] = splitn(
875
+ (entity instanceof User
876
+ ? (entity as User).username
877
+ : (entity as Group).groupname) ?? '',
878
+ '@',
879
+ 2,
880
+ );
881
+ if (domain && this.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
882
+ return true;
883
+ }
748
884
  }
749
885
 
750
886
  // Load access control, since we need it now...
751
- const acUser = entity.acUser ?? TilmeldAccessLevels.FULL_ACCESS;
752
- const acGroup = entity.acGroup ?? TilmeldAccessLevels.READ_ACCESS;
753
- const acOther = entity.acOther ?? TilmeldAccessLevels.NO_ACCESS;
887
+ const acUser = acProperties.acUser ?? TilmeldAccessLevels.FULL_ACCESS;
888
+ const acGroup = acProperties.acGroup ?? TilmeldAccessLevels.READ_ACCESS;
889
+ const acOther = acProperties.acOther ?? TilmeldAccessLevels.NO_ACCESS;
754
890
 
755
- if (userOrNull === null) {
891
+ if (this.currentUser == null) {
756
892
  return acOther >= type;
757
893
  }
758
894
 
@@ -762,45 +898,59 @@ export default class Tilmeld implements TilmeldInterface {
762
898
  }
763
899
 
764
900
  // Check if the entity is the user's group. Always readable.
901
+ const gid = userOrEmpty.$getGid();
765
902
  if (
766
- userOrEmpty.group != null &&
767
- typeof userOrEmpty.group.$is === 'function' &&
768
- userOrEmpty.group.$is(entity) &&
903
+ gid != null &&
904
+ entity.guid === gid &&
769
905
  type === TilmeldAccessLevels.READ_ACCESS
770
906
  ) {
771
907
  return true;
772
908
  }
773
909
 
910
+ // Check if the entity is one of the user's secondary groups. Always
911
+ // readable.
912
+ const gids = userOrEmpty.$getGids();
913
+ if (
914
+ gids != null &&
915
+ entity.guid != null &&
916
+ gids.indexOf(entity.guid) !== -1 &&
917
+ type === TilmeldAccessLevels.READ_ACCESS
918
+ ) {
919
+ return true;
920
+ }
921
+
922
+ // At this point, if the entity is a user or group, it should not be
923
+ // accessible.
924
+ if (entity instanceof User || entity instanceof Group) {
925
+ return false;
926
+ }
927
+
774
928
  // Calculate all the groups the user belongs to.
775
- let allGroups = userOrEmpty.group == null ? [] : [userOrEmpty.group];
776
- allGroups = allGroups.concat(userOrEmpty.groups ?? []);
777
- allGroups = allGroups.concat(userOrEmpty.$getDescendantGroupsSync());
929
+ let allGids = gid == null ? [] : [gid];
930
+ allGids = allGids.concat(gids ?? []);
778
931
 
779
932
  // Check access ac properties.
780
933
  const checks = [
781
934
  {
782
935
  type: TilmeldAccessLevels.FULL_ACCESS,
783
- array: [...(entity.acFull ?? [])],
936
+ array: [...(acProperties.acFull ?? [])],
784
937
  },
785
938
  {
786
939
  type: TilmeldAccessLevels.WRITE_ACCESS,
787
- array: [...(entity.acWrite ?? [])],
940
+ array: [...(acProperties.acWrite ?? [])],
788
941
  },
789
942
  {
790
943
  type: TilmeldAccessLevels.READ_ACCESS,
791
- array: [...(entity.acRead ?? [])],
944
+ array: [...(acProperties.acRead ?? [])],
792
945
  },
793
946
  ];
794
947
  for (let curCheck of checks) {
795
948
  if (type <= curCheck.type) {
796
- if (userOrEmpty.$inArray(curCheck.array)) {
949
+ if (curCheck.array.includes(userOrEmpty.guid)) {
797
950
  return true;
798
951
  }
799
- for (let curGroup of allGroups) {
800
- if (
801
- typeof curGroup.$inArray === 'function' &&
802
- curGroup.$inArray(curCheck.array)
803
- ) {
952
+ for (let curGid of allGids) {
953
+ if (curCheck.array.includes(curGid)) {
804
954
  return true;
805
955
  }
806
956
  }
@@ -809,17 +959,12 @@ export default class Tilmeld implements TilmeldInterface {
809
959
 
810
960
  // Check ownership ac properties.
811
961
  if (
812
- entity.user != null &&
813
- typeof entity.user.$is === 'function' &&
814
- entity.user.$is(userOrNull)
962
+ acProperties.user != null &&
963
+ acProperties.user === this.currentUser.guid
815
964
  ) {
816
965
  return acUser >= type;
817
966
  }
818
- if (
819
- entity.group != null &&
820
- typeof entity.group.$inArray === 'function' &&
821
- entity.group.$inArray(allGroups)
822
- ) {
967
+ if (acProperties.group != null && allGids.includes(acProperties.group)) {
823
968
  return acGroup >= type;
824
969
  }
825
970
  return acOther >= type;
@@ -849,12 +994,12 @@ export default class Tilmeld implements TilmeldInterface {
849
994
  * @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
850
995
  * @returns Whether the current user has at least `type` permission for the UID.
851
996
  */
852
- public checkClientUIDPermissions(
997
+ public async checkClientUIDPermissions(
853
998
  name: string,
854
999
  type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
855
- user?: (User & UserData) | false
1000
+ user?: User | false,
856
1001
  ) {
857
- let userOrEmpty: User & UserData = this.User.factorySync();
1002
+ let userOrEmpty: User = this.User.factorySync();
858
1003
  // Calculate the user.
859
1004
  if (user == null) {
860
1005
  userOrEmpty = this.User.current(true);
@@ -862,30 +1007,30 @@ export default class Tilmeld implements TilmeldInterface {
862
1007
  userOrEmpty = user;
863
1008
  }
864
1009
 
865
- if (userOrEmpty.$gatekeeper('system/admin')) {
1010
+ if (userOrEmpty.abilities?.includes('system/admin')) {
866
1011
  return true;
867
1012
  }
868
1013
 
869
1014
  if (type === TilmeldAccessLevels.FULL_ACCESS) {
870
1015
  return (
871
- this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
872
- userOrEmpty.$gatekeeper(`uid/set/${name}`)
1016
+ this.config.clientSetabledUIDs.includes(name) ||
1017
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`))
873
1018
  );
874
1019
  } else if (type === TilmeldAccessLevels.WRITE_ACCESS) {
875
1020
  return (
876
- this.config.clientEnabledUIDs.indexOf(name) !== -1 ||
877
- this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
878
- userOrEmpty.$gatekeeper(`uid/new/${name}`) ||
879
- userOrEmpty.$gatekeeper(`uid/set/${name}`)
1021
+ this.config.clientEnabledUIDs.includes(name) ||
1022
+ this.config.clientSetabledUIDs.includes(name) ||
1023
+ (await userOrEmpty.$gatekeeper(`uid/new/${name}`)) ||
1024
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`))
880
1025
  );
881
1026
  } else if (type === TilmeldAccessLevels.READ_ACCESS) {
882
1027
  return (
883
- this.config.clientReadableUIDs.indexOf(name) !== -1 ||
884
- this.config.clientEnabledUIDs.indexOf(name) !== -1 ||
885
- this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
886
- userOrEmpty.$gatekeeper(`uid/get/${name}`) ||
887
- userOrEmpty.$gatekeeper(`uid/new/${name}`) ||
888
- userOrEmpty.$gatekeeper(`uid/set/${name}`)
1028
+ this.config.clientReadableUIDs.includes(name) ||
1029
+ this.config.clientEnabledUIDs.includes(name) ||
1030
+ this.config.clientSetabledUIDs.includes(name) ||
1031
+ (await userOrEmpty.$gatekeeper(`uid/get/${name}`)) ||
1032
+ (await userOrEmpty.$gatekeeper(`uid/new/${name}`)) ||
1033
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`))
889
1034
  );
890
1035
  }
891
1036
 
@@ -897,20 +1042,29 @@ export default class Tilmeld implements TilmeldInterface {
897
1042
  *
898
1043
  * @param user The user.
899
1044
  */
900
- public fillSession(user: User & UserData) {
901
- // Read groups right now, since gatekeeper needs them, so
902
- // $udpateDataProtection will fail to read them (since it runs gatekeeper).
903
- const _group = user.group;
904
- const _groups = user.groups;
1045
+ public async fillSession(user: User) {
1046
+ // Clear any currently logged in user.
1047
+ const oldUser = this.currentUser;
1048
+ this.currentUser = null;
1049
+ this.gatekeeperCache = null;
1050
+ if (oldUser) {
1051
+ oldUser.$updateDataProtection();
1052
+ oldUser.$updateGroupDataProtection();
1053
+ }
1054
+
1055
+ // Update data protection on the incoming user.
1056
+ user.$updateDataProtection();
1057
+ user.$updateGroupDataProtection();
1058
+
1059
+ // Get the gatekeeper cache for incoming user.
1060
+ this.gatekeeperCache = await user.$getGatekeeperCache();
1061
+
1062
+ // Now we're ready to set the current user.
905
1063
  this.currentUser = user;
906
- // Now update the data protection on the user and all the groups.
1064
+
1065
+ // And update the data protection on the user and all the groups.
907
1066
  this.currentUser.$updateDataProtection();
908
- if (this.currentUser.group != null) {
909
- this.currentUser.group.$updateDataProtection();
910
- }
911
- for (let group of this.currentUser.groups ?? []) {
912
- group.$updateDataProtection();
913
- }
1067
+ this.currentUser.$updateGroupDataProtection();
914
1068
  }
915
1069
 
916
1070
  /**
@@ -919,109 +1073,204 @@ export default class Tilmeld implements TilmeldInterface {
919
1073
  public clearSession() {
920
1074
  const user = this.currentUser;
921
1075
  this.currentUser = null;
1076
+ this.gatekeeperCache = null;
922
1077
  if (user) {
923
1078
  user.$updateDataProtection();
924
- if (user.group != null) {
925
- user.group.$updateDataProtection();
926
- }
927
- for (let group of user.groups ?? []) {
928
- group.$updateDataProtection();
929
- }
1079
+ user.$updateGroupDataProtection();
930
1080
  }
931
1081
  }
932
1082
 
933
1083
  /**
934
1084
  * Validate and extract the user from a token.
935
1085
  *
1086
+ * For primary tokens, you want to enforce user restrictions. This keeps
1087
+ * disabled users from logging in and protects accounts with token revocation
1088
+ * dates.
1089
+ *
1090
+ * For switch tokens, you don't want to enforce user restrictions. This allows
1091
+ * an admin to switch to a user even if they are disabled or have revoked the
1092
+ * switch token. Admin actions take precedence over anything a normal user can
1093
+ * do.
1094
+ *
936
1095
  * @param token The authentication token.
937
- * @returns The user on success, null on failure.
1096
+ * @param enforceUserRestrictions Validate that the user is enabled and the token was issued after user's revocation date.
1097
+ * @param xsrfToken Validate this XSRF Token against the auth token.
1098
+ * @returns The user and extraction on success, null on failure.
938
1099
  */
939
- public async extractToken(token: string) {
940
- const extract = this.config.jwtExtract(this.config, token);
1100
+ public async extractToken(
1101
+ token: string,
1102
+ enforceUserRestrictions: boolean,
1103
+ xsrfToken?: string,
1104
+ ) {
1105
+ this.nymph.config.debugInfo('tilmeld', 'Extracting and validating token.');
1106
+ const extract = this.config.jwtExtract(this.config, token, xsrfToken);
941
1107
  if (extract == null) {
942
- return null;
1108
+ return { user: null, extract };
1109
+ }
1110
+ const { guid, issued, expire } = extract;
1111
+
1112
+ if (expire.getTime() <= Date.now() || issued.getTime() > Date.now()) {
1113
+ return { user: null, extract };
943
1114
  }
944
- const { guid } = extract;
945
1115
 
946
1116
  const user = await this.nymph.getEntity(
947
1117
  { class: this.User },
948
- {
949
- type: '&',
950
- guid: guid,
951
- }
1118
+ { type: '&', guid },
952
1119
  );
953
- if (!user || !user.guid || !user.enabled) {
954
- return null;
1120
+ if (
1121
+ user == null ||
1122
+ user.guid == null ||
1123
+ (enforceUserRestrictions &&
1124
+ (!user.enabled ||
1125
+ (user.revokeTokenDate != null &&
1126
+ issued.getTime() <= user.revokeTokenDate)))
1127
+ ) {
1128
+ return { user: null, extract };
955
1129
  }
956
1130
 
957
- return user;
1131
+ return { user, extract };
958
1132
  }
959
1133
 
960
1134
  /**
961
- * Check for a TILMELDAUTH token, and, if set, authenticate from it.
1135
+ * Check for TILMELDAUTH and TILMELDSWITCH tokens, and, if set, authenticate
1136
+ * from it/them.
1137
+ *
1138
+ * You can also call this function after setting `response.locals.user` to the
1139
+ * user you want to authenticate. You *should* check for `user.enabled` before
1140
+ * setting this variable, unless you explicitly want to log in as a disabled
1141
+ * user. (The user must be an instance of the User class for this Tilmeld
1142
+ * instance.)
1143
+ *
1144
+ * This function will set `response.locals.user` to the logged in user on
1145
+ * successful authentication.
962
1146
  *
963
1147
  * @param skipXsrfToken Skip the XSRF token check.
1148
+ * @param skipRenew Skip the token renewal step, even if the token is close to expiration.
964
1149
  * @returns True if a user was authenticated, false on any failure.
965
1150
  */
966
- public authenticate(skipXsrfToken = false) {
1151
+ public async authenticate(skipXsrfToken = false, skipRenew = false) {
967
1152
  if (this.request == null) {
968
1153
  return false;
969
1154
  }
970
1155
 
1156
+ if (
1157
+ this.response &&
1158
+ this.response.locals.user &&
1159
+ this.response.locals.user instanceof this.User
1160
+ ) {
1161
+ // The user has already been authenticated through some other means.
1162
+ const user = this.response.locals.user;
1163
+ await this.fillSession(user);
1164
+ return true;
1165
+ }
1166
+
971
1167
  const cookies = this.request.cookies ?? {};
972
1168
 
973
1169
  // If a client does't support cookies, they can use the X-TILMELDAUTH header
974
1170
  // to provide the auth token.
975
1171
  let fromAuthHeader = false;
976
1172
  let authToken: string;
1173
+ let switchToken: string | null;
977
1174
  if (this.request.header('x-tilmeldauth') != null) {
978
1175
  fromAuthHeader = true;
979
1176
  authToken = this.request.header('x-tilmeldauth') as string;
1177
+ switchToken =
1178
+ (this.request.header('x-tilmeldswitch') as string) ||
1179
+ 'TILMELDSWITCH' in cookies
1180
+ ? cookies.TILMELDSWITCH
1181
+ : null;
980
1182
  } else if ('TILMELDAUTH' in cookies) {
981
1183
  fromAuthHeader = false;
982
1184
  authToken = cookies.TILMELDAUTH;
1185
+ switchToken = 'TILMELDSWITCH' in cookies ? cookies.TILMELDSWITCH : null;
983
1186
  } else {
1187
+ this.clearSession();
984
1188
  return false;
985
1189
  }
986
1190
 
987
- let extract: { guid: string; expire: Date } | null;
1191
+ let user: User | null;
1192
+ let authExtract: {
1193
+ sub: string;
1194
+ guid: string;
1195
+ issued: Date;
1196
+ expire: Date;
1197
+ switch?: true;
1198
+ } | null;
1199
+ let switchUser: User | null = null;
1200
+ let switchExtract: {
1201
+ sub: string;
1202
+ guid: string;
1203
+ issued: Date;
1204
+ expire: Date;
1205
+ switch?: true;
1206
+ } | null = null;
1207
+ let xsrfToken: string | undefined = undefined;
988
1208
  if (
989
1209
  skipXsrfToken ||
990
1210
  this.request.originalUrl.startsWith(this.config.setupPath)
991
1211
  ) {
992
1212
  // The request is for the setup app, or we were told to skip the XSRF
993
1213
  // check, so don't check for the XSRF token.
994
- extract = this.config.jwtExtract(this.config, authToken);
995
1214
  } else {
996
1215
  // The request is for something else, so check for a valid XSRF token,
997
1216
  // unless the auth token is provided by a header (instead of a cookie).
998
- const xsrfToken = this.request.header('x-xsrf-token');
1217
+ xsrfToken = this.request.header('x-xsrf-token');
999
1218
  if (xsrfToken == null && !fromAuthHeader) {
1000
1219
  return false;
1001
1220
  }
1002
-
1003
- extract = this.config.jwtExtract(this.config, authToken, xsrfToken);
1221
+ }
1222
+ ({ user, extract: authExtract } = await this.extractToken(
1223
+ authToken,
1224
+ true,
1225
+ xsrfToken,
1226
+ ));
1227
+ if (switchToken) {
1228
+ ({ user: switchUser, extract: switchExtract } = await this.extractToken(
1229
+ switchToken,
1230
+ false,
1231
+ ));
1004
1232
  }
1005
1233
 
1006
- if (extract == null) {
1007
- this.logout();
1234
+ if (user == null || authExtract == null || authExtract.switch) {
1235
+ await this.logout();
1008
1236
  return false;
1009
1237
  }
1010
- const { guid, expire } = extract;
1011
1238
 
1012
- const user = this.User.factorySync(guid);
1013
- if (user.guid == null || !user.enabled) {
1014
- this.logout();
1015
- return false;
1239
+ if (switchToken) {
1240
+ if (
1241
+ switchUser == null ||
1242
+ switchExtract == null ||
1243
+ !switchExtract.switch ||
1244
+ switchExtract.sub !== user.guid
1245
+ ) {
1246
+ await this.logoutSwitch();
1247
+ } else {
1248
+ await this.fillSession(switchUser);
1249
+
1250
+ if (this.response) {
1251
+ this.response.locals.user = switchUser;
1252
+ }
1253
+
1254
+ return true;
1255
+ }
1016
1256
  }
1017
1257
 
1018
- if (expire.valueOf() < Date.now() + this.config.jwtRenew * 1000) {
1258
+ if (
1259
+ !skipRenew &&
1260
+ authExtract &&
1261
+ authExtract.expire.getTime() < Date.now() + this.config.jwtRenew * 1000
1262
+ ) {
1019
1263
  // If the user is less than renew time from needing a new token, give them
1020
1264
  // a new one.
1021
- this.login(user, fromAuthHeader);
1265
+ await this.login(user, fromAuthHeader);
1022
1266
  } else {
1023
- this.fillSession(user);
1267
+ await this.fillSession(user);
1024
1268
  }
1269
+
1270
+ if (this.response) {
1271
+ this.response.locals.user = user;
1272
+ }
1273
+
1025
1274
  return true;
1026
1275
  }
1027
1276
 
@@ -1029,27 +1278,85 @@ export default class Tilmeld implements TilmeldInterface {
1029
1278
  * Logs the given user into the system.
1030
1279
  *
1031
1280
  * @param user The user.
1032
- * @param sendAuthHeader When true, a custom header with the auth token will be sent.
1281
+ * @param sendAuthHeader Send the auth token as a custom header.
1282
+ * @param sendCookie Send the auth token as a cookie.
1033
1283
  * @returns True on success, false on failure.
1034
1284
  */
1035
- public login(user: User & UserData, sendAuthHeader: boolean) {
1285
+ public async login(user: User, sendAuthHeader: boolean, sendCookie = true) {
1036
1286
  if (user.guid != null && user.enabled) {
1037
- if (this.response && !this.response.headersSent) {
1038
- const token = this.config.jwtBuilder(this.config, user);
1039
- const appUrl = new URL(this.config.appUrl);
1040
- this.response.cookie('TILMELDAUTH', token, {
1041
- domain: this.config.cookieDomain,
1042
- path: this.config.cookiePath,
1043
- maxAge: this.config.jwtExpire * 1000,
1044
- secure: appUrl.protocol === 'https',
1045
- httpOnly: false, // Allow JS access (for CSRF protection).
1046
- sameSite: appUrl.protocol === 'https' ? 'lax' : 'strict',
1047
- });
1048
- if (sendAuthHeader) {
1049
- this.response.set('X-TILMELDAUTH', token);
1287
+ if (this.response) {
1288
+ if (!this.response.headersSent) {
1289
+ const token = this.config.jwtBuilder(this.config, user);
1290
+
1291
+ if (sendCookie) {
1292
+ const appUrl = new URL(this.config.appUrl);
1293
+ this.response.cookie('TILMELDAUTH', token, {
1294
+ domain: this.config.cookieDomain,
1295
+ path: this.config.cookiePath,
1296
+ maxAge: this.config.jwtExpire * 1000,
1297
+ secure: appUrl.protocol === 'https:',
1298
+ httpOnly: false, // Allow JS access (for CSRF protection).
1299
+ sameSite: appUrl.protocol === 'https:' ? 'lax' : 'strict',
1300
+ });
1301
+ }
1302
+
1303
+ if (sendAuthHeader) {
1304
+ this.response.set('X-TILMELDAUTH', token);
1305
+ }
1306
+ }
1307
+
1308
+ this.response.locals.user = user;
1309
+ }
1310
+
1311
+ await this.fillSession(user);
1312
+ return true;
1313
+ }
1314
+ return false;
1315
+ }
1316
+
1317
+ /**
1318
+ * Adds a switch auth token for the given user.
1319
+ *
1320
+ * This effectively logs the current user in to the system as the given user.
1321
+ *
1322
+ * @param user The current user doing the switch.
1323
+ * @param target The target user being switched to.
1324
+ * @param sendAuthHeader Send the auth token as a custom header.
1325
+ * @param sendCookie Send the auth token as a cookie.
1326
+ * @returns True on success, false on failure.
1327
+ */
1328
+ public async loginSwitch(
1329
+ user: User,
1330
+ target: User,
1331
+ sendAuthHeader: boolean,
1332
+ sendCookie = true,
1333
+ ) {
1334
+ if (target.guid != null) {
1335
+ if (this.response) {
1336
+ if (!this.response.headersSent) {
1337
+ const token = this.config.jwtBuilder(this.config, target, true, user);
1338
+
1339
+ if (sendCookie) {
1340
+ const appUrl = new URL(this.config.appUrl);
1341
+ this.response.cookie('TILMELDSWITCH', token, {
1342
+ domain: this.config.cookieDomain,
1343
+ path: this.config.cookiePath,
1344
+ maxAge: this.config.jwtSwitchExpire * 1000,
1345
+ secure: appUrl.protocol === 'https:',
1346
+ httpOnly: false, // Allow JS access (for PubSub auth token).
1347
+ sameSite: appUrl.protocol === 'https:' ? 'lax' : 'strict',
1348
+ });
1349
+ }
1350
+
1351
+ if (sendAuthHeader) {
1352
+ this.response.set('X-TILMELDSWITCH', token);
1353
+ }
1050
1354
  }
1355
+
1356
+ this.response.locals.user = target;
1051
1357
  }
1052
- this.fillSession(user);
1358
+
1359
+ await this.fillSession(target);
1053
1360
  return true;
1054
1361
  }
1055
1362
  return false;
@@ -1057,35 +1364,50 @@ export default class Tilmeld implements TilmeldInterface {
1057
1364
 
1058
1365
  /**
1059
1366
  * Logs the current user out of the system.
1367
+ *
1368
+ * @param clearCookie Clear the auth cookie. (Also send a header.)
1060
1369
  */
1061
- public logout() {
1370
+ public async logout(clearCookie = true) {
1371
+ if (this.request && !this.alreadyLoggedOutSwitch) {
1372
+ const cookies = this.request.cookies ?? {};
1373
+
1374
+ if ('TILMELDSWITCH' in cookies) {
1375
+ this.alreadyLoggedOutSwitch = true;
1376
+ return await this.logoutSwitch(clearCookie);
1377
+ }
1378
+ }
1379
+
1062
1380
  this.clearSession();
1063
- if (this.response && !this.response.headersSent) {
1064
- this.response.clearCookie('TILMELDAUTH', {
1065
- domain: this.config.cookieDomain,
1066
- path: this.config.cookiePath,
1067
- });
1068
- this.response.set('X-TILMELDAUTH', '');
1381
+ if (this.response) {
1382
+ if (clearCookie && !this.response.headersSent) {
1383
+ this.response.clearCookie('TILMELDAUTH', {
1384
+ domain: this.config.cookieDomain,
1385
+ path: this.config.cookiePath,
1386
+ });
1387
+ this.response.set('X-TILMELDAUTH', '');
1388
+ }
1389
+ this.response.locals.user = null;
1069
1390
  }
1070
1391
  }
1071
1392
 
1072
- // /**
1073
- // * Sort an array of groups hierarchically.
1074
- // *
1075
- // * An additional property of the groups can be used to sort them under their
1076
- // * parents.
1077
- // *
1078
- // * @param array The array of groups.
1079
- // * @param property The name of the property to sort groups by. Undefined for no additional sorting.
1080
- // * @param caseSensitive Sort case sensitively.
1081
- // * @param reverse Reverse the sort order.
1082
- // */
1083
- // public static groupSort(
1084
- // array: any[],
1085
- // property?: string,
1086
- // caseSensitive = false,
1087
- // reverse = false
1088
- // ) {
1089
- // hsort(array, property, 'parent', caseSensitive, reverse);
1090
- // }
1393
+ /**
1394
+ * Clears the switch user out of the system.
1395
+ *
1396
+ * @param clearCookie Clear the auth cookie. (Also send a header.)
1397
+ */
1398
+ public async logoutSwitch(clearCookie = true) {
1399
+ this.clearSession();
1400
+ if (this.response) {
1401
+ if (clearCookie && !this.response.headersSent) {
1402
+ this.response.clearCookie('TILMELDSWITCH', {
1403
+ domain: this.config.cookieDomain,
1404
+ path: this.config.cookiePath,
1405
+ });
1406
+ this.response.set('X-TILMELDSWITCH', '');
1407
+ }
1408
+ this.response.locals.user = null;
1409
+
1410
+ await this.authenticate(false, true);
1411
+ }
1412
+ }
1091
1413
  }