@nymphjs/tilmeld 1.0.0-beta.5 → 1.0.0-beta.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Tilmeld.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  Options,
6
6
  TilmeldInterface,
7
7
  TilmeldAccessLevels,
8
+ ACProperties,
8
9
  } from '@nymphjs/nymph';
9
10
  import { URL } from 'url';
10
11
  import { Request, Response } from 'express';
@@ -15,6 +16,7 @@ import { AccessControlError } from './errors';
15
16
  import Group from './Group';
16
17
  import User, { UserData } from './User';
17
18
  import { AccessControlData } from './Tilmeld.types';
19
+ import { enforceTilmeld } from './enforceTilmeld';
18
20
 
19
21
  /**
20
22
  * A user and group system for Nymph.js.
@@ -69,9 +71,17 @@ export default class Tilmeld implements TilmeldInterface {
69
71
  public response: Response | null = null;
70
72
 
71
73
  /**
72
- * Skip the authenticate step of initialization.
74
+ * Used to avoid infinite loop.
73
75
  */
74
- private skipAuthenticate = false;
76
+ private alreadyLoggedOutSwitch = false;
77
+
78
+ /**
79
+ * Gatekeeper ability cache.
80
+ *
81
+ * Gatekeeper will cache the user's abilities that it calculates, so it can
82
+ * check faster if that user has been checked before.
83
+ */
84
+ private gatekeeperCache: { [k: string]: true } | null = null;
75
85
 
76
86
  /**
77
87
  * Create a new instance of Tilmeld.
@@ -84,6 +94,7 @@ export default class Tilmeld implements TilmeldInterface {
84
94
 
85
95
  /**
86
96
  * This is used internally by Nymph. Don't call it yourself.
97
+ *
87
98
  * @returns A clone of this instance.
88
99
  */
89
100
  public clone() {
@@ -91,7 +102,7 @@ export default class Tilmeld implements TilmeldInterface {
91
102
  tilmeld.request = this.request;
92
103
  tilmeld.response = this.response;
93
104
  tilmeld.currentUser = this.currentUser;
94
- tilmeld.skipAuthenticate = true;
105
+ tilmeld.gatekeeperCache = this.gatekeeperCache;
95
106
 
96
107
  return tilmeld;
97
108
  }
@@ -106,30 +117,43 @@ export default class Tilmeld implements TilmeldInterface {
106
117
  public init(nymph: Nymph) {
107
118
  this.nymph = nymph;
108
119
 
109
- // Set up access control hooks when Nymph is called.
110
120
  if (this.nymph.driver == null) {
111
121
  throw new Error("Tilmeld can't be configured before Nymph.");
112
122
  }
113
123
 
114
124
  // Configure the classes.
115
- class NymphUser extends User {}
116
- NymphUser.nymph = this.nymph;
117
- NymphUser.tilmeld = this;
118
- (NymphUser as any).skipOnClone = true;
119
- this.User = NymphUser;
120
- this.nymph.addEntityClass(NymphUser);
121
- class NymphGroup extends Group {}
122
- NymphGroup.nymph = this.nymph;
123
- NymphGroup.tilmeld = this;
124
- (NymphUser as any).skipOnClone = true;
125
- this.Group = NymphGroup;
126
- this.nymph.addEntityClass(NymphGroup);
125
+ if (nymph.parent) {
126
+ // This is a cloned Nymph, it should have classes already.
127
+ this.User = this.nymph.getEntityClass(User);
128
+ this.Group = this.nymph.getEntityClass(Group);
129
+ } else {
130
+ // This is a new Nymph, add the classes.
131
+ this.User = this.nymph.addEntityClass(User);
132
+ this.Group = this.nymph.addEntityClass(Group);
133
+ }
127
134
 
128
- this.initAccessControl();
129
- if (this.request != null && !this.skipAuthenticate) {
130
- this.authenticate();
135
+ // Clone the current user so its Nymph instance is correct.
136
+ if (this.currentUser) {
137
+ const currentUserGUID = this.currentUser.guid;
138
+ const currentUserCDate = this.currentUser.cdate;
139
+ const currentUserMDate = this.currentUser.mdate;
140
+ const currentUserTags = this.currentUser.tags;
141
+ const currentUserData = this.currentUser.$getData();
142
+ const currentUserSData = this.currentUser.$getSData();
143
+
144
+ const user = this.User.factorySync();
145
+ user.guid = currentUserGUID;
146
+ user.cdate = currentUserCDate;
147
+ user.mdate = currentUserMDate;
148
+ user.tags = currentUserTags;
149
+ user.$putData(currentUserData, currentUserSData);
150
+ this.currentUser = user;
151
+ this.currentUser.$updateDataProtection();
152
+ this.currentUser.$updateGroupDataProtection();
131
153
  }
132
- this.skipAuthenticate = false;
154
+
155
+ // Set up access control hooks when Nymph is called.
156
+ this.initAccessControl();
133
157
  }
134
158
 
135
159
  /**
@@ -141,11 +165,17 @@ export default class Tilmeld implements TilmeldInterface {
141
165
  * @param ability The ability.
142
166
  * @returns Whether the user has the given ability.
143
167
  */
144
- public gatekeeper(ability?: string): boolean {
145
- if (this.currentUser == null) {
168
+ public gatekeeper(ability?: string) {
169
+ if (this.currentUser == null || this.gatekeeperCache == null) {
146
170
  return false;
147
171
  }
148
- return this.currentUser.$gatekeeper(ability);
172
+ if (ability == null) {
173
+ return true;
174
+ }
175
+ return (
176
+ (ability in this.gatekeeperCache && !!this.gatekeeperCache[ability]) ||
177
+ !!this.currentUser.abilities?.includes('system/admin')
178
+ );
149
179
  }
150
180
 
151
181
  private initAccessControl() {
@@ -153,9 +183,9 @@ export default class Tilmeld implements TilmeldInterface {
153
183
  const handleQuery = function (
154
184
  nymph: Nymph,
155
185
  options: Options,
156
- selectors: FormattedSelector[]
186
+ selectors: FormattedSelector[],
157
187
  ) {
158
- const tilmeld = nymph.tilmeld as Tilmeld;
188
+ const tilmeld = enforceTilmeld(nymph);
159
189
  if (
160
190
  !options ||
161
191
  !('skipAc' in options) ||
@@ -164,7 +194,9 @@ export default class Tilmeld implements TilmeldInterface {
164
194
  ) {
165
195
  if (options?.source === 'client') {
166
196
  if (
167
- !tilmeld.gatekeeper('tilmeld/admin') && // The user is not an admin.
197
+ (!tilmeld.currentUser ||
198
+ (!tilmeld.currentUser.abilities?.includes('tilmeld/admin') &&
199
+ !tilmeld.currentUser.abilities?.includes('system/admin'))) && // The user is not an admin.
168
200
  ((!tilmeld.config.enableUserSearch &&
169
201
  options.class?.factorySync() instanceof User) || // The use is searching for a user and searching is disabled.
170
202
  (!tilmeld.config.enableGroupSearch &&
@@ -193,9 +225,9 @@ export default class Tilmeld implements TilmeldInterface {
193
225
  // Filter entities being deleted for user permissions.
194
226
  const checkPermissionsDelete = async function (
195
227
  nymph: Nymph,
196
- entity: EntityInterface
228
+ entity: EntityInterface,
197
229
  ) {
198
- const tilmeld = nymph.tilmeld as Tilmeld;
230
+ const tilmeld = enforceTilmeld(nymph);
199
231
  if (
200
232
  typeof entity.$tilmeldDeleteSkipAC === 'function' &&
201
233
  entity.$tilmeldDeleteSkipAC()
@@ -213,11 +245,11 @@ export default class Tilmeld implements TilmeldInterface {
213
245
  const checkPermissionsDeleteByID = async function (
214
246
  nymph: Nymph,
215
247
  guid: string,
216
- className?: string
248
+ className?: string,
217
249
  ) {
218
- const entity = nymph.driver.getEntitySync(
250
+ const entity = await nymph.getEntity(
219
251
  { class: nymph.getEntityClass(className ?? 'Entity') },
220
- { type: '&', guid: guid }
252
+ { type: '&', guid: guid },
221
253
  );
222
254
  if (entity != null) {
223
255
  checkPermissionsDelete(nymph, entity);
@@ -229,9 +261,9 @@ export default class Tilmeld implements TilmeldInterface {
229
261
  // disallowed changes to AC properties.
230
262
  const checkPermissionsSaveAndFilterAcChanges = async function (
231
263
  nymph: Nymph,
232
- entity: EntityInterface & AccessControlData
264
+ entity: EntityInterface & AccessControlData,
233
265
  ) {
234
- const tilmeld = nymph.tilmeld as Tilmeld;
266
+ const tilmeld = enforceTilmeld(nymph);
235
267
  if (!entity) {
236
268
  return;
237
269
  }
@@ -247,35 +279,9 @@ export default class Tilmeld implements TilmeldInterface {
247
279
  // allowing a change to ac properties.
248
280
 
249
281
  const originalAc = entity.$getOriginalAcValues();
250
- const newAc: AccessControlData = {
251
- user: entity.user ?? undefined,
252
- group: entity.group ?? undefined,
253
- acUser: entity.acUser ?? undefined,
254
- acGroup: entity.acGroup ?? undefined,
255
- acOther: entity.acOther ?? undefined,
256
- acRead: entity.acRead ?? undefined,
257
- acWrite: entity.acWrite ?? undefined,
258
- acFull: entity.acFull ?? undefined,
259
- };
282
+ const newAc = entity.$getCurrentAcValues();
260
283
 
261
- const setAcProperties = (acValues: { [k: string]: any }) => {
262
- for (let name in acValues) {
263
- const value = acValues[name];
264
- if (value == null) {
265
- delete entity[name];
266
- } else {
267
- entity[name] = value;
268
- }
269
- }
270
- };
271
-
272
- // Restore original AC properties and check permissions.
273
- setAcProperties(originalAc);
274
-
275
- function areAcPropertiesChanged(
276
- a: AccessControlData,
277
- b: AccessControlData
278
- ) {
284
+ function areAcPropertiesChanged(a: ACProperties, b: ACProperties) {
279
285
  for (const name of ['user', 'group'] as ('user' | 'group')[]) {
280
286
  const aVal = a[name];
281
287
  const bVal = b[name];
@@ -284,7 +290,7 @@ export default class Tilmeld implements TilmeldInterface {
284
290
  } else if (aVal == null || bVal == null) {
285
291
  return true;
286
292
  }
287
- if (!aVal.$is(bVal)) {
293
+ if (aVal !== bVal) {
288
294
  return true;
289
295
  }
290
296
  }
@@ -319,12 +325,7 @@ export default class Tilmeld implements TilmeldInterface {
319
325
  if (aVal.length !== bVal.length) {
320
326
  return true;
321
327
  }
322
- if (
323
- xor(
324
- aVal.map((entity) => entity.guid),
325
- bVal.map((entity) => entity.guid)
326
- ).length
327
- ) {
328
+ if (xor(aVal, bVal).length) {
328
329
  return true;
329
330
  }
330
331
  }
@@ -333,41 +334,41 @@ export default class Tilmeld implements TilmeldInterface {
333
334
 
334
335
  if (areAcPropertiesChanged(originalAc, newAc)) {
335
336
  if (
336
- !tilmeld.checkPermissions(entity, TilmeldAccessLevels.FULL_ACCESS)
337
+ !tilmeld.checkPermissions(
338
+ entity,
339
+ TilmeldAccessLevels.FULL_ACCESS,
340
+ undefined,
341
+ originalAc,
342
+ )
337
343
  ) {
338
344
  // Only allow changes to AC properties if the user has full access.
339
345
  throw new AccessControlError(
340
- 'No permission to change access control properties.'
346
+ 'No permission to change access control properties.',
341
347
  );
342
348
  }
343
349
 
344
350
  if (
345
- ((originalAc.user && !originalAc.user.$is(newAc.user)) ||
346
- (!originalAc.user &&
347
- newAc.user &&
348
- !newAc.user.$is(tilmeld.currentUser))) &&
349
- !tilmeld.gatekeeper('tilmeld/admin')
351
+ (originalAc.user && originalAc.user !== newAc.user) ||
352
+ (!originalAc.user &&
353
+ newAc.user &&
354
+ newAc.user !== tilmeld.currentUser)
350
355
  ) {
351
356
  throw new AccessControlError(
352
- 'No permission to assign to another user.'
357
+ 'No permission to assign to another user.',
353
358
  );
354
359
  }
355
360
 
356
361
  if (
357
362
  newAc.group &&
358
- !newAc.group.$is(originalAc.group) &&
359
- !(
360
- newAc.group.$is(tilmeld.currentUser?.group) ||
361
- newAc.group.$inArray(tilmeld.currentUser?.groups ?? [])
362
- ) &&
363
- !tilmeld.gatekeeper('tilmeld/admin')
363
+ newAc.group !== originalAc.group &&
364
+ !(newAc.group =
365
+ tilmeld.currentUser?.$getGid() ||
366
+ (tilmeld.currentUser?.$getGids() ?? []).includes(newAc.group))
364
367
  ) {
365
368
  throw new AccessControlError(
366
- 'No permission to assign to another group.'
369
+ 'No permission to assign to another group.',
367
370
  );
368
371
  }
369
-
370
- setAcProperties(newAc);
371
372
  }
372
373
  }
373
374
 
@@ -396,9 +397,9 @@ export default class Tilmeld implements TilmeldInterface {
396
397
  */
397
398
  const addAccess = async function (
398
399
  nymph: Nymph,
399
- entity: EntityInterface & AccessControlData
400
+ entity: EntityInterface & AccessControlData,
400
401
  ) {
401
- const tilmeld = nymph.tilmeld as Tilmeld;
402
+ const tilmeld = enforceTilmeld(nymph);
402
403
  const user = tilmeld.currentUser;
403
404
  if (
404
405
  user != null &&
@@ -409,12 +410,11 @@ export default class Tilmeld implements TilmeldInterface {
409
410
  if (entity.user == null) {
410
411
  entity.user = user;
411
412
  }
412
- if (
413
- entity.group == null &&
414
- user.group != null &&
415
- user.group.guid != null
416
- ) {
417
- entity.group = user.group;
413
+ if (entity.group == null) {
414
+ const group = user.group;
415
+ if (group != null && group.guid != null) {
416
+ entity.group = group;
417
+ }
418
418
  }
419
419
  if (entity.acUser == null) {
420
420
  entity.acUser = TilmeldAccessLevels.FULL_ACCESS;
@@ -440,7 +440,7 @@ export default class Tilmeld implements TilmeldInterface {
440
440
 
441
441
  const validate = async function (
442
442
  _nymph: Nymph,
443
- entity: EntityInterface & AccessControlData
443
+ entity: EntityInterface & AccessControlData,
444
444
  ) {
445
445
  if (!(entity instanceof User) && !(entity instanceof Group)) {
446
446
  const ownershipAcPropertyValidator = (prop: any) => {
@@ -450,7 +450,7 @@ export default class Tilmeld implements TilmeldInterface {
450
450
  prop > TilmeldAccessLevels.FULL_ACCESS
451
451
  ) {
452
452
  throw new AccessControlError(
453
- 'Invalid access control property: ' + prop
453
+ 'Invalid access control property: ' + prop,
454
454
  );
455
455
  }
456
456
  };
@@ -458,13 +458,13 @@ export default class Tilmeld implements TilmeldInterface {
458
458
  const accessAcPropertyValidator = (prop: any) => {
459
459
  if (!Array.isArray(prop)) {
460
460
  throw new AccessControlError(
461
- 'Invalid access control property: ' + prop
461
+ 'Invalid access control property: ' + prop,
462
462
  );
463
463
  }
464
464
  prop.forEach((value) => {
465
465
  if (!(value instanceof User || value instanceof Group)) {
466
466
  throw new AccessControlError(
467
- 'Invalid access control property: ' + prop
467
+ 'Invalid access control property: ' + prop,
468
468
  );
469
469
  }
470
470
  });
@@ -473,7 +473,7 @@ export default class Tilmeld implements TilmeldInterface {
473
473
  if ('user' in entity) {
474
474
  if (!(entity.user instanceof User)) {
475
475
  throw new AccessControlError(
476
- 'Invalid access control property: user'
476
+ 'Invalid access control property: user',
477
477
  );
478
478
  }
479
479
  }
@@ -481,7 +481,7 @@ export default class Tilmeld implements TilmeldInterface {
481
481
  if ('group' in entity) {
482
482
  if (!(entity.group instanceof Group)) {
483
483
  throw new AccessControlError(
484
- 'Invalid access control property: group'
484
+ 'Invalid access control property: group',
485
485
  );
486
486
  }
487
487
  }
@@ -526,18 +526,10 @@ export default class Tilmeld implements TilmeldInterface {
526
526
  */
527
527
  public addAccessControlSelectors(
528
528
  options: Options,
529
- selectors: FormattedSelector[]
529
+ selectors: FormattedSelector[],
530
530
  ) {
531
531
  const user = this.currentUser;
532
532
 
533
- if (
534
- user != null &&
535
- (user.abilities?.indexOf('system/admin') ?? -1) !== -1
536
- ) {
537
- // The user is a system admin, so they can see everything.
538
- return;
539
- }
540
-
541
533
  if (options == null) {
542
534
  throw new Error('No options in argument.');
543
535
  } else if (
@@ -577,33 +569,23 @@ export default class Tilmeld implements TilmeldInterface {
577
569
  ];
578
570
  const groupRefs: FormattedSelector['ref'] = [];
579
571
  const acRefs: FormattedSelector['ref'] = [];
580
- if (user.group != null && user.group.guid != null) {
572
+ const gid = user.$getGid();
573
+ if (gid != null) {
581
574
  // It belongs to the user's primary group.
582
- groupRefs.push(['group', user.group.guid]);
575
+ groupRefs.push(['group', gid]);
583
576
  // User's primary group is listed in acRead, acWrite, or acFull.
584
- acRefs.push(['acRead', user.group.guid]);
585
- acRefs.push(['acWrite', user.group.guid]);
586
- acRefs.push(['acFull', user.group.guid]);
577
+ acRefs.push(['acRead', gid]);
578
+ acRefs.push(['acWrite', gid]);
579
+ acRefs.push(['acFull', gid]);
587
580
  }
588
- for (let curSecondaryGroup of user.groups ?? []) {
589
- if (curSecondaryGroup != null && curSecondaryGroup.guid != null) {
590
- // It belongs to the user's secondary group.
591
- groupRefs.push(['group', curSecondaryGroup.guid]);
592
- // User's secondary group is listed in acRead, acWrite, or acFull.
593
- acRefs.push(['acRead', curSecondaryGroup.guid]);
594
- acRefs.push(['acWrite', curSecondaryGroup.guid]);
595
- acRefs.push(['acFull', curSecondaryGroup.guid]);
596
- }
597
- }
598
- for (let curDescendantGroup of user.$getDescendantGroupsSync()) {
599
- if (curDescendantGroup != null && curDescendantGroup.guid != null) {
600
- // It belongs to the user's secondary group.
601
- groupRefs.push(['group', curDescendantGroup.guid]);
602
- // User's secondary group is listed in acRead, acWrite, or acFull.
603
- acRefs.push(['acRead', curDescendantGroup.guid]);
604
- acRefs.push(['acWrite', curDescendantGroup.guid]);
605
- acRefs.push(['acFull', curDescendantGroup.guid]);
606
- }
581
+ const gids = user.$getGids();
582
+ for (let curGid of gids ?? []) {
583
+ // It belongs to the user's secondary group.
584
+ groupRefs.push(['group', curGid]);
585
+ // User's secondary group is listed in acRead, acWrite, or acFull.
586
+ acRefs.push(['acRead', curGid]);
587
+ acRefs.push(['acWrite', curGid]);
588
+ acRefs.push(['acFull', curGid]);
607
589
  }
608
590
  // All the group refs.
609
591
  if (groupRefs.length) {
@@ -669,7 +651,7 @@ export default class Tilmeld implements TilmeldInterface {
669
651
  * - FULL_ACCESS, the user has read, write, and delete access to the entity,
670
652
  * as well as being able to manage its access controls and ownership.
671
653
  *
672
- * These properties defaults to:
654
+ * These properties default to:
673
655
  *
674
656
  * - acUser = TilmeldAccessLevels.FULL_ACCESS
675
657
  * - acGroup = TilmeldAccessLevels.READ_ACCESS
@@ -684,9 +666,7 @@ export default class Tilmeld implements TilmeldInterface {
684
666
  * The following conditions will result in different checks, which determine
685
667
  * whether the check passes:
686
668
  *
687
- * - The user has the "system/admin" ability. (Always true.)
688
669
  * - It is a user or group. (True for READ_ACCESS or Tilmeld admins.)
689
- * - The entity has no "user" and no "group". (Always true.)
690
670
  * - No user is logged in. (Check other AC.)
691
671
  * - The entity is the user. (Always true.)
692
672
  * - It is the user's primary group. (True for READ_ACCESS.)
@@ -704,16 +684,17 @@ export default class Tilmeld implements TilmeldInterface {
704
684
  * @param entity The entity to check.
705
685
  * @param type The lowest level of permission to consider a pass.
706
686
  * @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
687
+ * @param acProperties The acProperties to use instead of getting them from the entity.
707
688
  * @returns Whether the current user has at least `type` permission for the entity.
708
689
  */
709
690
  public checkPermissions(
710
- entity: EntityInterface & AccessControlData,
691
+ entity: EntityInterface,
711
692
  type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
712
- user?: (User & UserData) | false
693
+ user?: (User & UserData) | false,
694
+ acProperties?: ACProperties,
713
695
  ) {
714
- // Only works for entities.
715
- if (typeof entity !== 'object' || typeof entity.$is !== 'function') {
716
- return false;
696
+ if (!acProperties) {
697
+ acProperties = entity.$getCurrentAcValues() as ACProperties;
717
698
  }
718
699
 
719
700
  let userOrNull: (User & UserData) | null = null;
@@ -727,30 +708,22 @@ export default class Tilmeld implements TilmeldInterface {
727
708
  userOrEmpty = user;
728
709
  }
729
710
 
730
- if (userOrEmpty.$gatekeeper('system/admin')) {
731
- return true;
732
- }
733
-
734
711
  // Users and groups are always readable. Editable by Tilmeld admins.
735
712
  if (
736
713
  (entity instanceof User || entity instanceof Group) &&
737
714
  (type === TilmeldAccessLevels.READ_ACCESS ||
738
- userOrEmpty.$gatekeeper('tilmeld/admin'))
715
+ userOrEmpty.abilities?.includes('tilmeld/admin') ||
716
+ userOrEmpty.abilities?.includes('system/admin'))
739
717
  ) {
740
718
  return true;
741
719
  }
742
720
 
743
- // Entities with no owners are always editable.
744
- if (entity.user == null && entity.group == null) {
745
- return true;
746
- }
747
-
748
721
  // Load access control, since we need it now...
749
- const acUser = entity.acUser ?? TilmeldAccessLevels.FULL_ACCESS;
750
- const acGroup = entity.acGroup ?? TilmeldAccessLevels.READ_ACCESS;
751
- const acOther = entity.acOther ?? TilmeldAccessLevels.NO_ACCESS;
722
+ const acUser = acProperties.acUser ?? TilmeldAccessLevels.FULL_ACCESS;
723
+ const acGroup = acProperties.acGroup ?? TilmeldAccessLevels.READ_ACCESS;
724
+ const acOther = acProperties.acOther ?? TilmeldAccessLevels.NO_ACCESS;
752
725
 
753
- if (userOrNull === null) {
726
+ if (userOrNull == null) {
754
727
  return acOther >= type;
755
728
  }
756
729
 
@@ -760,45 +733,41 @@ export default class Tilmeld implements TilmeldInterface {
760
733
  }
761
734
 
762
735
  // Check if the entity is the user's group. Always readable.
736
+ const gid = userOrEmpty.$getGid();
763
737
  if (
764
- userOrEmpty.group != null &&
765
- typeof userOrEmpty.group.$is === 'function' &&
766
- userOrEmpty.group.$is(entity) &&
738
+ gid != null &&
739
+ entity.guid === gid &&
767
740
  type === TilmeldAccessLevels.READ_ACCESS
768
741
  ) {
769
742
  return true;
770
743
  }
771
744
 
772
745
  // Calculate all the groups the user belongs to.
773
- let allGroups = userOrEmpty.group == null ? [] : [userOrEmpty.group];
774
- allGroups = allGroups.concat(userOrEmpty.groups ?? []);
775
- allGroups = allGroups.concat(userOrEmpty.$getDescendantGroupsSync());
746
+ let allGids = gid == null ? [] : [gid];
747
+ allGids = allGids.concat(userOrEmpty.$getGids() ?? []);
776
748
 
777
749
  // Check access ac properties.
778
750
  const checks = [
779
751
  {
780
752
  type: TilmeldAccessLevels.FULL_ACCESS,
781
- array: [...(entity.acFull ?? [])],
753
+ array: [...(acProperties.acFull ?? [])],
782
754
  },
783
755
  {
784
756
  type: TilmeldAccessLevels.WRITE_ACCESS,
785
- array: [...(entity.acWrite ?? [])],
757
+ array: [...(acProperties.acWrite ?? [])],
786
758
  },
787
759
  {
788
760
  type: TilmeldAccessLevels.READ_ACCESS,
789
- array: [...(entity.acRead ?? [])],
761
+ array: [...(acProperties.acRead ?? [])],
790
762
  },
791
763
  ];
792
764
  for (let curCheck of checks) {
793
765
  if (type <= curCheck.type) {
794
- if (userOrEmpty.$inArray(curCheck.array)) {
766
+ if (curCheck.array.includes(userOrEmpty.guid)) {
795
767
  return true;
796
768
  }
797
- for (let curGroup of allGroups) {
798
- if (
799
- typeof curGroup.$inArray === 'function' &&
800
- curGroup.$inArray(curCheck.array)
801
- ) {
769
+ for (let curGid of allGids) {
770
+ if (curCheck.array.includes(curGid)) {
802
771
  return true;
803
772
  }
804
773
  }
@@ -806,18 +775,10 @@ export default class Tilmeld implements TilmeldInterface {
806
775
  }
807
776
 
808
777
  // Check ownership ac properties.
809
- if (
810
- entity.user != null &&
811
- typeof entity.user.$is === 'function' &&
812
- entity.user.$is(userOrNull)
813
- ) {
778
+ if (acProperties.user != null && acProperties.user === userOrNull.guid) {
814
779
  return acUser >= type;
815
780
  }
816
- if (
817
- entity.group != null &&
818
- typeof entity.group.$inArray === 'function' &&
819
- entity.group.$inArray(allGroups)
820
- ) {
781
+ if (acProperties.group != null && allGids.includes(acProperties.group)) {
821
782
  return acGroup >= type;
822
783
  }
823
784
  return acOther >= type;
@@ -847,10 +808,10 @@ export default class Tilmeld implements TilmeldInterface {
847
808
  * @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
848
809
  * @returns Whether the current user has at least `type` permission for the UID.
849
810
  */
850
- public checkClientUIDPermissions(
811
+ public async checkClientUIDPermissions(
851
812
  name: string,
852
813
  type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
853
- user?: (User & UserData) | false
814
+ user?: (User & UserData) | false,
854
815
  ) {
855
816
  let userOrEmpty: User & UserData = this.User.factorySync();
856
817
  // Calculate the user.
@@ -860,30 +821,30 @@ export default class Tilmeld implements TilmeldInterface {
860
821
  userOrEmpty = user;
861
822
  }
862
823
 
863
- if (userOrEmpty.$gatekeeper('system/admin')) {
824
+ if (userOrEmpty.abilities?.includes('system/admin')) {
864
825
  return true;
865
826
  }
866
827
 
867
828
  if (type === TilmeldAccessLevels.FULL_ACCESS) {
868
829
  return (
869
830
  this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
870
- userOrEmpty.$gatekeeper(`uid/set/${name}`)
831
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`))
871
832
  );
872
833
  } else if (type === TilmeldAccessLevels.WRITE_ACCESS) {
873
834
  return (
874
835
  this.config.clientEnabledUIDs.indexOf(name) !== -1 ||
875
836
  this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
876
- userOrEmpty.$gatekeeper(`uid/new/${name}`) ||
877
- userOrEmpty.$gatekeeper(`uid/set/${name}`)
837
+ (await userOrEmpty.$gatekeeper(`uid/new/${name}`)) ||
838
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`))
878
839
  );
879
840
  } else if (type === TilmeldAccessLevels.READ_ACCESS) {
880
841
  return (
881
842
  this.config.clientReadableUIDs.indexOf(name) !== -1 ||
882
843
  this.config.clientEnabledUIDs.indexOf(name) !== -1 ||
883
844
  this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
884
- userOrEmpty.$gatekeeper(`uid/get/${name}`) ||
885
- userOrEmpty.$gatekeeper(`uid/new/${name}`) ||
886
- userOrEmpty.$gatekeeper(`uid/set/${name}`)
845
+ (await userOrEmpty.$gatekeeper(`uid/get/${name}`)) ||
846
+ (await userOrEmpty.$gatekeeper(`uid/new/${name}`)) ||
847
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`))
887
848
  );
888
849
  }
889
850
 
@@ -895,20 +856,13 @@ export default class Tilmeld implements TilmeldInterface {
895
856
  *
896
857
  * @param user The user.
897
858
  */
898
- public fillSession(user: User & UserData) {
899
- // Read groups right now, since gatekeeper needs them, so
900
- // $udpateDataProtection will fail to read them (since it runs gatekeeper).
901
- const _group = user.group;
902
- const _groups = user.groups;
859
+ public async fillSession(user: User & UserData) {
903
860
  this.currentUser = user;
861
+ this.gatekeeperCache = await user.$getGatekeeperCache();
862
+
904
863
  // Now update the data protection on the user and all the groups.
905
864
  this.currentUser.$updateDataProtection();
906
- if (this.currentUser.group != null) {
907
- this.currentUser.group.$updateDataProtection();
908
- }
909
- for (let group of this.currentUser.groups ?? []) {
910
- group.$updateDataProtection();
911
- }
865
+ this.currentUser.$updateGroupDataProtection();
912
866
  }
913
867
 
914
868
  /**
@@ -917,14 +871,10 @@ export default class Tilmeld implements TilmeldInterface {
917
871
  public clearSession() {
918
872
  const user = this.currentUser;
919
873
  this.currentUser = null;
874
+ this.gatekeeperCache = null;
920
875
  if (user) {
921
876
  user.$updateDataProtection();
922
- if (user.group != null) {
923
- user.group.$updateDataProtection();
924
- }
925
- for (let group of user.groups ?? []) {
926
- group.$updateDataProtection();
927
- }
877
+ user.$updateGroupDataProtection();
928
878
  }
929
879
  }
930
880
 
@@ -946,9 +896,9 @@ export default class Tilmeld implements TilmeldInterface {
946
896
  {
947
897
  type: '&',
948
898
  guid: guid,
949
- }
899
+ },
950
900
  );
951
- if (!user || !user.guid || !user.enabled) {
901
+ if (!user || !user.guid) {
952
902
  return null;
953
903
  }
954
904
 
@@ -956,40 +906,72 @@ export default class Tilmeld implements TilmeldInterface {
956
906
  }
957
907
 
958
908
  /**
959
- * Check for a TILMELDAUTH token, and, if set, authenticate from it.
909
+ * Check for TILMELDAUTH and TILMELDSWITCH tokens, and, if set, authenticate
910
+ * from it/them.
911
+ *
912
+ * You can also call this function after setting `response.locals.user` to the
913
+ * user you want to authenticate. You *should* check for `user.enabled` before
914
+ * setting this variable, unless you explicitly want to log in as a disabled
915
+ * user. (The user must be an instance of the User class for this Tilmeld
916
+ * instance.)
917
+ *
918
+ * This function will set `response.locals.user` to the logged in user on
919
+ * successful authentication.
960
920
  *
961
921
  * @param skipXsrfToken Skip the XSRF token check.
962
922
  * @returns True if a user was authenticated, false on any failure.
963
923
  */
964
- public authenticate(skipXsrfToken = false) {
924
+ public async authenticate(skipXsrfToken = false) {
965
925
  if (this.request == null) {
966
926
  return false;
967
927
  }
968
928
 
929
+ if (
930
+ this.response &&
931
+ this.response.locals.user &&
932
+ this.response.locals.user instanceof this.User
933
+ ) {
934
+ // The user has already been authenticated through some other means.
935
+ const user = this.response.locals.user;
936
+ await this.fillSession(user);
937
+ return true;
938
+ }
939
+
969
940
  const cookies = this.request.cookies ?? {};
970
941
 
971
942
  // If a client does't support cookies, they can use the X-TILMELDAUTH header
972
943
  // to provide the auth token.
973
944
  let fromAuthHeader = false;
974
945
  let authToken: string;
946
+ let switchToken: string | null;
975
947
  if (this.request.header('x-tilmeldauth') != null) {
976
948
  fromAuthHeader = true;
977
949
  authToken = this.request.header('x-tilmeldauth') as string;
950
+ switchToken =
951
+ (this.request.header('x-tilmeldswitch') as string) ||
952
+ 'TILMELDSWITCH' in cookies
953
+ ? cookies.TILMELDSWITCH
954
+ : null;
978
955
  } else if ('TILMELDAUTH' in cookies) {
979
956
  fromAuthHeader = false;
980
957
  authToken = cookies.TILMELDAUTH;
958
+ switchToken = 'TILMELDSWITCH' in cookies ? cookies.TILMELDSWITCH : null;
981
959
  } else {
982
960
  return false;
983
961
  }
984
962
 
985
- let extract: { guid: string; expire: Date } | null;
963
+ let authExtract: { guid: string; issued: Date; expire: Date } | null;
964
+ let switchExtract: { guid: string; expire: Date } | null;
986
965
  if (
987
966
  skipXsrfToken ||
988
967
  this.request.originalUrl.startsWith(this.config.setupPath)
989
968
  ) {
990
969
  // The request is for the setup app, or we were told to skip the XSRF
991
970
  // check, so don't check for the XSRF token.
992
- extract = this.config.jwtExtract(this.config, authToken);
971
+ authExtract = this.config.jwtExtract(this.config, authToken);
972
+ switchExtract = switchToken
973
+ ? this.config.jwtExtract(this.config, switchToken)
974
+ : null;
993
975
  } else {
994
976
  // The request is for something else, so check for a valid XSRF token,
995
977
  // unless the auth token is provided by a header (instead of a cookie).
@@ -998,28 +980,62 @@ export default class Tilmeld implements TilmeldInterface {
998
980
  return false;
999
981
  }
1000
982
 
1001
- extract = this.config.jwtExtract(this.config, authToken, xsrfToken);
983
+ authExtract = this.config.jwtExtract(this.config, authToken, xsrfToken);
984
+ switchExtract = switchToken
985
+ ? this.config.jwtExtract(this.config, switchToken)
986
+ : null;
1002
987
  }
1003
988
 
1004
- if (extract == null) {
1005
- this.logout();
989
+ if (authExtract == null) {
990
+ await this.logout();
1006
991
  return false;
1007
992
  }
1008
- const { guid, expire } = extract;
993
+ const { guid, issued, expire } = authExtract;
994
+ const { guid: switchGuid, expire: switchExpire } = switchExtract || {
995
+ guid: null,
996
+ expire: null,
997
+ };
1009
998
 
1010
- const user = this.User.factorySync(guid);
1011
- if (user.guid == null || !user.enabled) {
1012
- this.logout();
999
+ const user = await this.User.factory(guid);
1000
+ if (
1001
+ user.guid == null ||
1002
+ !user.enabled ||
1003
+ expire.getTime() <= Date.now() ||
1004
+ issued.getTime() > Date.now() ||
1005
+ (user.revokeTokenDate != null && issued.getTime() <= user.revokeTokenDate)
1006
+ ) {
1007
+ await this.logout();
1013
1008
  return false;
1014
1009
  }
1015
1010
 
1016
- if (expire.valueOf() < Date.now() + this.config.jwtRenew * 1000) {
1011
+ if (switchGuid && switchExpire && switchExpire.getTime() > Date.now()) {
1012
+ // Load the switch user.
1013
+ const switchUser = await this.User.factory(switchGuid);
1014
+ if (switchUser.guid == null) {
1015
+ await this.logoutSwitch();
1016
+ } else {
1017
+ await this.fillSession(switchUser);
1018
+
1019
+ if (this.response) {
1020
+ this.response.locals.user = switchUser;
1021
+ }
1022
+
1023
+ return true;
1024
+ }
1025
+ }
1026
+
1027
+ if (expire.getTime() < Date.now() + this.config.jwtRenew * 1000) {
1017
1028
  // If the user is less than renew time from needing a new token, give them
1018
1029
  // a new one.
1019
- this.login(user, fromAuthHeader);
1030
+ await this.login(user, fromAuthHeader);
1020
1031
  } else {
1021
- this.fillSession(user);
1032
+ await this.fillSession(user);
1022
1033
  }
1034
+
1035
+ if (this.response) {
1036
+ this.response.locals.user = user;
1037
+ }
1038
+
1023
1039
  return true;
1024
1040
  }
1025
1041
 
@@ -1027,27 +1043,87 @@ export default class Tilmeld implements TilmeldInterface {
1027
1043
  * Logs the given user into the system.
1028
1044
  *
1029
1045
  * @param user The user.
1030
- * @param sendAuthHeader When true, a custom header with the auth token will be sent.
1046
+ * @param sendAuthHeader Send the auth token as a custom header.
1047
+ * @param sendCookie Send the auth token as a cookie.
1031
1048
  * @returns True on success, false on failure.
1032
1049
  */
1033
- public login(user: User & UserData, sendAuthHeader: boolean) {
1050
+ public async login(
1051
+ user: User & UserData,
1052
+ sendAuthHeader: boolean,
1053
+ sendCookie = true,
1054
+ ) {
1034
1055
  if (user.guid != null && user.enabled) {
1035
- if (this.response && !this.response.headersSent) {
1036
- const token = this.config.jwtBuilder(this.config, user);
1037
- const appUrl = new URL(this.config.appUrl);
1038
- this.response.cookie('TILMELDAUTH', token, {
1039
- domain: this.config.cookieDomain,
1040
- path: this.config.cookiePath,
1041
- maxAge: this.config.jwtExpire * 1000,
1042
- secure: appUrl.protocol === 'https',
1043
- httpOnly: false, // Allow JS access (for CSRF protection).
1044
- sameSite: appUrl.protocol === 'https' ? 'lax' : 'strict',
1045
- });
1046
- if (sendAuthHeader) {
1047
- this.response.set('X-TILMELDAUTH', token);
1056
+ if (this.response) {
1057
+ if (!this.response.headersSent) {
1058
+ const token = this.config.jwtBuilder(this.config, user);
1059
+
1060
+ if (sendCookie) {
1061
+ const appUrl = new URL(this.config.appUrl);
1062
+ this.response.cookie('TILMELDAUTH', token, {
1063
+ domain: this.config.cookieDomain,
1064
+ path: this.config.cookiePath,
1065
+ maxAge: this.config.jwtExpire * 1000,
1066
+ secure: appUrl.protocol === 'https:',
1067
+ httpOnly: false, // Allow JS access (for CSRF protection).
1068
+ sameSite: appUrl.protocol === 'https:' ? 'lax' : 'strict',
1069
+ });
1070
+ }
1071
+
1072
+ if (sendAuthHeader) {
1073
+ this.response.set('X-TILMELDAUTH', token);
1074
+ }
1048
1075
  }
1076
+
1077
+ this.response.locals.user = user;
1049
1078
  }
1050
- this.fillSession(user);
1079
+
1080
+ await this.fillSession(user);
1081
+ return true;
1082
+ }
1083
+ return false;
1084
+ }
1085
+
1086
+ /**
1087
+ * Adds a switch auth token for the given user.
1088
+ *
1089
+ * This effectively logs the current user in to the system as the given user.
1090
+ *
1091
+ * @param user The user.
1092
+ * @param sendAuthHeader Send the auth token as a custom header.
1093
+ * @param sendCookie Send the auth token as a cookie.
1094
+ * @returns True on success, false on failure.
1095
+ */
1096
+ public async loginSwitch(
1097
+ user: User & UserData,
1098
+ sendAuthHeader: boolean,
1099
+ sendCookie = true,
1100
+ ) {
1101
+ if (user.guid != null) {
1102
+ if (this.response) {
1103
+ if (!this.response.headersSent) {
1104
+ const token = this.config.jwtBuilder(this.config, user, true);
1105
+
1106
+ if (sendCookie) {
1107
+ const appUrl = new URL(this.config.appUrl);
1108
+ this.response.cookie('TILMELDSWITCH', token, {
1109
+ domain: this.config.cookieDomain,
1110
+ path: this.config.cookiePath,
1111
+ maxAge: this.config.jwtSwitchExpire * 1000,
1112
+ secure: appUrl.protocol === 'https:',
1113
+ httpOnly: false, // Allow JS access (for PubSub auth token).
1114
+ sameSite: appUrl.protocol === 'https:' ? 'lax' : 'strict',
1115
+ });
1116
+ }
1117
+
1118
+ if (sendAuthHeader) {
1119
+ this.response.set('X-TILMELDSWITCH', token);
1120
+ }
1121
+ }
1122
+
1123
+ this.response.locals.user = user;
1124
+ }
1125
+
1126
+ await this.fillSession(user);
1051
1127
  return true;
1052
1128
  }
1053
1129
  return false;
@@ -1055,15 +1131,50 @@ export default class Tilmeld implements TilmeldInterface {
1055
1131
 
1056
1132
  /**
1057
1133
  * Logs the current user out of the system.
1134
+ *
1135
+ * @param clearCookie Clear the auth cookie. (Also send a header.)
1058
1136
  */
1059
- public logout() {
1137
+ public async logout(clearCookie = true) {
1138
+ if (this.request && !this.alreadyLoggedOutSwitch) {
1139
+ const cookies = this.request.cookies ?? {};
1140
+
1141
+ if ('TILMELDSWITCH' in cookies) {
1142
+ this.alreadyLoggedOutSwitch = true;
1143
+ return await this.logoutSwitch(clearCookie);
1144
+ }
1145
+ }
1146
+
1060
1147
  this.clearSession();
1061
- if (this.response && !this.response.headersSent) {
1062
- this.response.clearCookie('TILMELDAUTH', {
1063
- domain: this.config.cookieDomain,
1064
- path: this.config.cookiePath,
1065
- });
1066
- this.response.set('X-TILMELDAUTH', '');
1148
+ if (this.response) {
1149
+ if (clearCookie && !this.response.headersSent) {
1150
+ this.response.clearCookie('TILMELDAUTH', {
1151
+ domain: this.config.cookieDomain,
1152
+ path: this.config.cookiePath,
1153
+ });
1154
+ this.response.set('X-TILMELDAUTH', '');
1155
+ }
1156
+ this.response.locals.user = null;
1157
+ }
1158
+ }
1159
+
1160
+ /**
1161
+ * Clears the switch user out of the system.
1162
+ *
1163
+ * @param clearCookie Clear the auth cookie. (Also send a header.)
1164
+ */
1165
+ public async logoutSwitch(clearCookie = true) {
1166
+ this.clearSession();
1167
+ if (this.response) {
1168
+ if (clearCookie && !this.response.headersSent) {
1169
+ this.response.clearCookie('TILMELDSWITCH', {
1170
+ domain: this.config.cookieDomain,
1171
+ path: this.config.cookiePath,
1172
+ });
1173
+ this.response.set('X-TILMELDSWITCH', '');
1174
+ }
1175
+ this.response.locals.user = null;
1176
+
1177
+ await this.authenticate();
1067
1178
  }
1068
1179
  }
1069
1180