@nymphjs/tilmeld 1.0.0-beta.13 → 1.0.0-beta.131

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