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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Group.ts CHANGED
@@ -342,6 +342,12 @@ export class Group extends AbleObject<GroupData> {
342
342
 
343
343
  if ('abilities' in input.data) {
344
344
  this.$checkIncomingAbilities(input.data.abilities);
345
+ } else if (
346
+ (this.$allowlistData == null ||
347
+ this.$allowlistData.includes('abilities')) &&
348
+ (this.$privateData == null || !this.$privateData.includes('abilities'))
349
+ ) {
350
+ this.$checkIncomingAbilities(undefined);
345
351
  }
346
352
 
347
353
  super.$jsonAcceptData(input, allowConflict);
@@ -354,6 +360,10 @@ export class Group extends AbleObject<GroupData> {
354
360
  this.$checkIncomingAbilities(patch.set.abilities);
355
361
  }
356
362
 
363
+ if (patch.unset.includes('abilities')) {
364
+ this.$checkIncomingAbilities(undefined);
365
+ }
366
+
357
367
  super.$jsonAcceptPatch(patch, allowConflict);
358
368
  }
359
369
 
@@ -378,6 +388,20 @@ export class Group extends AbleObject<GroupData> {
378
388
  );
379
389
  }
380
390
 
391
+ if (
392
+ xor(
393
+ incoming.filter((ability) => ability.startsWith('system/')),
394
+ this.abilities?.filter((ability) => ability.startsWith('system/')) ??
395
+ [],
396
+ ).length &&
397
+ (!tilmeld.currentUser ||
398
+ !tilmeld.currentUser.abilities?.includes('system/admin'))
399
+ ) {
400
+ throw new BadDataError(
401
+ "You don't have the authority to grant or revoke system abilities.",
402
+ );
403
+ }
404
+
381
405
  if (
382
406
  incoming.includes('tilmeld/admin') !==
383
407
  this.abilities?.includes('tilmeld/admin') &&
@@ -402,10 +426,9 @@ export class Group extends AbleObject<GroupData> {
402
426
 
403
427
  if (
404
428
  xor(
405
- incoming.filter((ability: string) => ability.startsWith('tilmeld/')),
406
- this.abilities?.filter((ability: string) =>
407
- ability.startsWith('tilmeld/'),
408
- ) ?? [],
429
+ incoming.filter((ability) => ability.startsWith('tilmeld/')),
430
+ this.abilities?.filter((ability) => ability.startsWith('tilmeld/')) ??
431
+ [],
409
432
  ).length &&
410
433
  (!tilmeld.currentUser ||
411
434
  (!tilmeld.currentUser.abilities?.includes('system/admin') &&
@@ -422,14 +445,21 @@ export class Group extends AbleObject<GroupData> {
422
445
  sdata?: SerializedEntityData,
423
446
  source?: 'server',
424
447
  ) {
425
- super.$putData(data, sdata, source);
448
+ let newData = { ...data };
449
+ let newSdata = sdata && { ...sdata };
426
450
  // TODO: Fix these being added by the drivers.
427
451
  // @ts-ignore
428
- delete this.acRead;
452
+ delete newData.acRead;
429
453
  // @ts-ignore
430
- delete this.acWrite;
454
+ delete newData.acWrite;
431
455
  // @ts-ignore
432
- delete this.acFull;
456
+ delete newData.acFull;
457
+ if (newSdata) {
458
+ delete newSdata.acRead;
459
+ delete newSdata.acWrite;
460
+ delete newSdata.acFull;
461
+ }
462
+ super.$putData(newData, newSdata, source);
433
463
  if (source === 'server') {
434
464
  this.$originalGroupname = this.groupname;
435
465
  }
@@ -437,13 +467,10 @@ export class Group extends AbleObject<GroupData> {
437
467
  }
438
468
 
439
469
  /**
440
- * Update the data protection arrays for a user.
441
- *
442
- * @param givenUser User to update protection for. If undefined, will use the currently logged in user.
470
+ * Update the data protection arrays for the current user.
443
471
  */
444
- public $updateDataProtection(givenUser?: UserClass) {
472
+ public $updateDataProtection() {
445
473
  const tilmeld = enforceTilmeld(this);
446
- let user = givenUser ?? tilmeld.User.current();
447
474
  let [_groupname, domain] = tilmeld.config.domainSupport
448
475
  ? splitn(this.groupname ?? '', '@', 2)
449
476
  : [this.groupname, null];
@@ -460,25 +487,23 @@ export class Group extends AbleObject<GroupData> {
460
487
  }
461
488
 
462
489
  if (
463
- user != null &&
464
- (user.abilities?.includes('tilmeld/admin') ||
465
- user.abilities?.includes('system/admin'))
490
+ tilmeld.gatekeeper('tilmeld/admin') ||
491
+ tilmeld.gatekeeper('system/admin')
466
492
  ) {
467
493
  // Users who can edit groups can see their data.
468
494
  this.$privateData = [];
469
495
  this.$allowlistData = undefined;
470
496
  } else if (
471
497
  this.user != null &&
472
- user != null &&
473
- this.user.guid === user.guid
498
+ tilmeld.currentUser != null &&
499
+ this.user.guid === tilmeld.currentUser.guid
474
500
  ) {
475
501
  // Users can see their group's data.
476
502
  this.$privateData = [];
477
503
  } else if (
478
- user != null &&
479
504
  tilmeld.config.domainSupport &&
480
505
  domain &&
481
- user.abilities?.includes(`tilmeld/domain/${domain}/admin`)
506
+ tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)
482
507
  ) {
483
508
  // Domain admins can see their groups' data.
484
509
  this.$privateData = [];
@@ -914,7 +939,7 @@ export class Group extends AbleObject<GroupData> {
914
939
  }
915
940
 
916
941
  // Groups should never have 'system/admin', 'tilmeld/admin',
917
- // 'tilmeld/switch', or domain admin abilities.
942
+ // 'tilmeld/switch', or system abilities.
918
943
  this.abilities = (this.abilities ?? []).filter((ability) => {
919
944
  if (
920
945
  ['system/admin', 'tilmeld/admin', 'tilmeld/switch'].indexOf(ability) !==
@@ -925,7 +950,7 @@ export class Group extends AbleObject<GroupData> {
925
950
  if (ability == null) {
926
951
  return false;
927
952
  }
928
- if (ability.startsWith('tilmeld/domain/') && ability.endsWith('/admin')) {
953
+ if (ability.startsWith('system/')) {
929
954
  return false;
930
955
  }
931
956
  return true;
package/src/Tilmeld.ts CHANGED
@@ -9,6 +9,10 @@ import {
9
9
  TilmeldAccessLevels,
10
10
  TilmeldAccessRequest,
11
11
  ACProperties,
12
+ entitiesToReferences,
13
+ entityConstructorsToClassNames,
14
+ classNamesToEntityConstructors,
15
+ referencesToEntities,
12
16
  } from '@nymphjs/nymph';
13
17
  import { Request, Response } from 'express';
14
18
  import { xor } from 'lodash-es';
@@ -165,15 +169,53 @@ export default class Tilmeld implements TilmeldInterface {
165
169
  const currentUserCDate = this.currentUser.cdate;
166
170
  const currentUserMDate = this.currentUser.mdate;
167
171
  const currentUserTags = this.currentUser.tags;
168
- const currentUserData = this.currentUser.$getData();
172
+ const currentUserData = entityConstructorsToClassNames(
173
+ entitiesToReferences(this.currentUser.$getData(false), true),
174
+ );
169
175
  const currentUserSData = this.currentUser.$getSData();
176
+ const currentUserOriginalAcValues =
177
+ this.currentUser.$getOriginalAcValues();
170
178
 
171
179
  const user = this.User.factorySync();
172
180
  user.guid = currentUserGUID;
173
181
  user.cdate = currentUserCDate;
174
182
  user.mdate = currentUserMDate;
175
183
  user.tags = currentUserTags;
176
- user.$putData(currentUserData, currentUserSData);
184
+ // First, put the trusted original AC values in with the server flag.
185
+ Entity.factorySync().$putData.call(
186
+ user,
187
+ {
188
+ ...currentUserOriginalAcValues,
189
+ user:
190
+ currentUserOriginalAcValues.user != null
191
+ ? this.User.factoryReference([
192
+ 'nymph_entity_reference',
193
+ currentUserOriginalAcValues.user,
194
+ this.User.class,
195
+ ])
196
+ : undefined,
197
+ group:
198
+ currentUserOriginalAcValues.group != null
199
+ ? this.Group.factoryReference([
200
+ 'nymph_entity_reference',
201
+ currentUserOriginalAcValues.group,
202
+ this.Group.class,
203
+ ])
204
+ : undefined,
205
+ },
206
+ {},
207
+ 'server',
208
+ );
209
+ // Now put the real data in, that could potentially have been altered from
210
+ // the client.
211
+ user.$putData(
212
+ classNamesToEntityConstructors(
213
+ this.nymph,
214
+ referencesToEntities(currentUserData, this.nymph, false, false),
215
+ false,
216
+ ),
217
+ currentUserSData,
218
+ );
177
219
  user.$originalEmail = this.currentUser.$originalEmail;
178
220
  user.$originalUsername = this.currentUser.$originalUsername;
179
221
  this.currentUser = user;
@@ -292,7 +334,7 @@ export default class Tilmeld implements TilmeldInterface {
292
334
  if (entity == null) {
293
335
  throw new AccessControlError("You can't delete this entity by ID.");
294
336
  }
295
- checkPermissionsDelete(nymph, entity);
337
+ await checkPermissionsDelete(nymph, entity);
296
338
  };
297
339
  checkPermissionsDeleteByID.skipOnClone = true;
298
340
 
@@ -373,7 +415,6 @@ export default class Tilmeld implements TilmeldInterface {
373
415
  !tilmeld.checkPermissions(
374
416
  entity,
375
417
  TilmeldAccessLevels.FULL_ACCESS,
376
- undefined,
377
418
  originalAc,
378
419
  )
379
420
  ) {
@@ -740,7 +781,7 @@ export default class Tilmeld implements TilmeldInterface {
740
781
  }
741
782
 
742
783
  /**
743
- * Check an entity's permissions for a user.
784
+ * Check an entity's permissions for the current user.
744
785
  *
745
786
  * This will check the AC (Access Control) properties of the entity. These
746
787
  * include the following properties:
@@ -800,37 +841,26 @@ export default class Tilmeld implements TilmeldInterface {
800
841
  *
801
842
  * @param entity The entity to check.
802
843
  * @param type The lowest level of permission to consider a pass.
803
- * @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
804
844
  * @param acProperties The acProperties to use instead of getting them from the entity.
805
845
  * @returns Whether the current user has at least `type` permission for the entity.
806
846
  */
807
847
  public checkPermissions(
808
848
  entity: EntityInterface,
809
849
  type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
810
- user?: User | false,
811
850
  acProperties?: ACProperties,
812
851
  ) {
813
852
  if (!acProperties) {
814
853
  acProperties = entity.$getCurrentAcValues() as ACProperties;
815
854
  }
816
855
 
817
- let userOrNull: User | null = null;
818
- let userOrEmpty: User = this.User.factorySync();
819
- // Calculate the user.
820
- if (user == null) {
821
- userOrNull = this.currentUser;
822
- userOrEmpty = this.User.current(true);
823
- } else if (user !== false) {
824
- userOrNull = user;
825
- userOrEmpty = user;
826
- }
856
+ let userOrEmpty: User = this.User.current(true);
827
857
 
828
858
  // Users and groups are always readable. Editable by Tilmeld admins.
829
859
  if (
830
860
  (entity instanceof User || entity instanceof Group) &&
831
861
  (type === TilmeldAccessLevels.READ_ACCESS ||
832
- userOrEmpty.abilities?.includes('tilmeld/admin') ||
833
- userOrEmpty.abilities?.includes('system/admin'))
862
+ this.gatekeeper('tilmeld/admin') ||
863
+ this.gatekeeper('system/admin'))
834
864
  ) {
835
865
  return true;
836
866
  }
@@ -848,10 +878,7 @@ export default class Tilmeld implements TilmeldInterface {
848
878
  '@',
849
879
  2,
850
880
  );
851
- if (
852
- domain &&
853
- userOrEmpty.abilities?.includes(`tilmeld/domain/${domain}/admin`)
854
- ) {
881
+ if (domain && this.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
855
882
  return true;
856
883
  }
857
884
  }
@@ -861,7 +888,7 @@ export default class Tilmeld implements TilmeldInterface {
861
888
  const acGroup = acProperties.acGroup ?? TilmeldAccessLevels.READ_ACCESS;
862
889
  const acOther = acProperties.acOther ?? TilmeldAccessLevels.NO_ACCESS;
863
890
 
864
- if (userOrNull == null) {
891
+ if (this.currentUser == null) {
865
892
  return acOther >= type;
866
893
  }
867
894
 
@@ -931,7 +958,10 @@ export default class Tilmeld implements TilmeldInterface {
931
958
  }
932
959
 
933
960
  // Check ownership ac properties.
934
- if (acProperties.user != null && acProperties.user === userOrNull.guid) {
961
+ if (
962
+ acProperties.user != null &&
963
+ acProperties.user === this.currentUser.guid
964
+ ) {
935
965
  return acUser >= type;
936
966
  }
937
967
  if (acProperties.group != null && allGids.includes(acProperties.group)) {
@@ -1013,10 +1043,26 @@ export default class Tilmeld implements TilmeldInterface {
1013
1043
  * @param user The user.
1014
1044
  */
1015
1045
  public async fillSession(user: User) {
1016
- this.currentUser = user;
1046
+ // Clear any currently logged in user.
1047
+ const oldUser = this.currentUser;
1048
+ this.currentUser = null;
1049
+ this.gatekeeperCache = null;
1050
+ if (oldUser) {
1051
+ oldUser.$updateDataProtection();
1052
+ oldUser.$updateGroupDataProtection();
1053
+ }
1054
+
1055
+ // Update data protection on the incoming user.
1056
+ user.$updateDataProtection();
1057
+ user.$updateGroupDataProtection();
1058
+
1059
+ // Get the gatekeeper cache for incoming user.
1017
1060
  this.gatekeeperCache = await user.$getGatekeeperCache();
1018
1061
 
1019
- // Now update the data protection on the user and all the groups.
1062
+ // Now we're ready to set the current user.
1063
+ this.currentUser = user;
1064
+
1065
+ // And update the data protection on the user and all the groups.
1020
1066
  this.currentUser.$updateDataProtection();
1021
1067
  this.currentUser.$updateGroupDataProtection();
1022
1068
  }
package/src/User.ts CHANGED
@@ -927,19 +927,12 @@ export class User extends AbleObject<UserData> {
927
927
  if (domain) {
928
928
  domain = domain.toLowerCase();
929
929
  }
930
- const user = tilmeld.User.current();
931
- const isCurrentUser = user != null && user.$is(this);
932
- // While loading, accessing abilities on the current user will infinitely
933
- // loop, but if this is the current user, we can use its abilities.
934
- const abilities =
935
- (isCurrentUser || user == null ? this.abilities : user?.abilities) ??
936
- [];
930
+ const isCurrentUser = this.$is(tilmeld.currentUser);
937
931
  const isNewUser = input.guid == null;
938
932
  const isDomainUser =
939
933
  !isCurrentUser &&
940
- user != null &&
941
934
  domain &&
942
- abilities.includes(`tilmeld/domain/${domain}/admin`);
935
+ tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`);
943
936
 
944
937
  reloadDataProtection = !!(isNewUser && isDomainUser);
945
938
  }
@@ -1045,14 +1038,21 @@ export class User extends AbleObject<UserData> {
1045
1038
  sdata?: SerializedEntityData,
1046
1039
  source?: 'server',
1047
1040
  ) {
1048
- super.$putData(data, sdata, source);
1041
+ let newData = { ...data };
1042
+ let newSdata = sdata && { ...sdata };
1049
1043
  // TODO: Fix these being added by the drivers.
1050
1044
  // @ts-ignore
1051
- delete this.acRead;
1045
+ delete newData.acRead;
1052
1046
  // @ts-ignore
1053
- delete this.acWrite;
1047
+ delete newData.acWrite;
1054
1048
  // @ts-ignore
1055
- delete this.acFull;
1049
+ delete newData.acFull;
1050
+ if (newSdata) {
1051
+ delete newSdata.acRead;
1052
+ delete newSdata.acWrite;
1053
+ delete newSdata.acFull;
1054
+ }
1055
+ super.$putData(newData, newSdata, source);
1056
1056
  if (source === 'server') {
1057
1057
  this.$originalEmail = this.email;
1058
1058
  this.$originalUsername = this.username;
@@ -1061,13 +1061,10 @@ export class User extends AbleObject<UserData> {
1061
1061
  }
1062
1062
 
1063
1063
  /**
1064
- * Update the data protection arrays for a user.
1065
- *
1066
- * @param givenUser User to update protection for. If undefined, will use the currently logged in user.
1064
+ * Update the data protection arrays for the current user.
1067
1065
  */
1068
- public $updateDataProtection(givenUser?: User) {
1066
+ public $updateDataProtection() {
1069
1067
  const tilmeld = enforceTilmeld(this);
1070
- let user = givenUser ?? tilmeld.User.current();
1071
1068
  let [_username, domain] = tilmeld.config.domainSupport
1072
1069
  ? splitn(this.$originalUsername ?? this.username ?? '', '@', 2)
1073
1070
  : [this.$originalUsername ?? this.username, null];
@@ -1084,18 +1081,13 @@ export class User extends AbleObject<UserData> {
1084
1081
  this.$privateData.push('username');
1085
1082
  }
1086
1083
 
1087
- const isCurrentUser = user != null && user.$is(this);
1088
- // While loading, accessing abilities on the current user will infinitely
1089
- // loop, but if this is the current user, we can use its abilities.
1090
- const abilities =
1091
- (isCurrentUser || user == null ? this.abilities : user?.abilities) ?? [];
1084
+ const isCurrentUser = this.$is(tilmeld.currentUser);
1092
1085
  const isNewUser = this.guid == null;
1093
1086
  const isDomainUser =
1094
1087
  tilmeld.config.domainSupport &&
1095
1088
  !isCurrentUser &&
1096
- user != null &&
1097
1089
  domain &&
1098
- abilities.includes(`tilmeld/domain/${domain}/admin`);
1090
+ tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`);
1099
1091
 
1100
1092
  if (isCurrentUser) {
1101
1093
  // Users can check to see what abilities they have.
@@ -1121,12 +1113,18 @@ export class User extends AbleObject<UserData> {
1121
1113
  }
1122
1114
 
1123
1115
  if (
1124
- user != null &&
1125
- (abilities.includes('tilmeld/admin') ||
1126
- abilities.includes('system/admin'))
1116
+ tilmeld.gatekeeper('tilmeld/admin') ||
1117
+ tilmeld.gatekeeper('system/admin')
1127
1118
  ) {
1128
1119
  // Users who can edit other users can see most of their data.
1129
- this.$privateData = ['password', 'salt', 'totpSecret'];
1120
+ this.$privateData = [
1121
+ 'newEmailSecret',
1122
+ 'cancelEmailSecret',
1123
+ 'recoverSecret',
1124
+ 'salt',
1125
+ 'password',
1126
+ 'totpSecret',
1127
+ ];
1130
1128
  this.$clientEnabledMethods.push('$hasTOTPSecret');
1131
1129
  this.$clientEnabledMethods.push('$removeTOTPSecret');
1132
1130
  this.$allowlistData = undefined;
@@ -1229,10 +1227,9 @@ export class User extends AbleObject<UserData> {
1229
1227
  }
1230
1228
 
1231
1229
  if (
1232
- user != null &&
1233
1230
  !isCurrentUser &&
1234
- (abilities.includes('tilmeld/switch') ||
1235
- abilities.includes('system/admin'))
1231
+ (tilmeld.gatekeeper('tilmeld/switch') ||
1232
+ tilmeld.gatekeeper('system/admin'))
1236
1233
  ) {
1237
1234
  // Users with this ability can switch to other users.
1238
1235
  this.$clientEnabledMethods.push('$switchUser');
@@ -1647,14 +1644,42 @@ export class User extends AbleObject<UserData> {
1647
1644
  * @param data The input data from the client.
1648
1645
  * @returns An object with a boolean 'result' entry and a 'message' entry.
1649
1646
  */
1650
- public async $revokeCurrentTokens(data: {
1647
+ public async $revokeCurrentTokens(data?: {
1651
1648
  password: string;
1652
1649
  }): Promise<{ result: boolean; message: string }> {
1653
1650
  const tilmeld = enforceTilmeld(this);
1651
+
1654
1652
  if (!tilmeld.gatekeeper()) {
1655
1653
  return { result: false, message: 'You must be logged in.' };
1656
1654
  }
1657
- if (!(await this.$checkPassword(data.password ?? ''))) {
1655
+
1656
+ if (this.guid == null) {
1657
+ return { result: false, message: 'This user has not been saved.' };
1658
+ }
1659
+
1660
+ // Work on real data.
1661
+ await this.$refresh();
1662
+
1663
+ let [_username, domain] = tilmeld.config.domainSupport
1664
+ ? splitn(this.$originalUsername ?? this.username ?? '', '@', 2)
1665
+ : [this.$originalUsername ?? this.username, null];
1666
+ // Lowercase the domain part.
1667
+ if (domain) {
1668
+ domain = domain.toLowerCase();
1669
+ }
1670
+
1671
+ const isAdmin = tilmeld.gatekeeper('tilmeld/admin');
1672
+ const isDomainUser =
1673
+ tilmeld.config.domainSupport &&
1674
+ !this.$is(tilmeld.currentUser) &&
1675
+ domain &&
1676
+ tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`);
1677
+
1678
+ if (
1679
+ !isAdmin &&
1680
+ !isDomainUser &&
1681
+ (data == null || !(await this.$checkPassword(data.password ?? '')))
1682
+ ) {
1658
1683
  return { result: false, message: 'Incorrect password.' };
1659
1684
  }
1660
1685
 
@@ -233,6 +233,21 @@ export default {
233
233
  .pattern(/^[0-9]+$/, 'numbers'),
234
234
  }
235
235
  : {};
236
+ for (let ability of group.abilities ?? []) {
237
+ if (ability.startsWith('system/')) {
238
+ throw new Error('Invalid Group: Groups cannot have system abilities.');
239
+ }
240
+ if (ability === 'tilmeld/admin') {
241
+ throw new Error(
242
+ 'Invalid Group: Groups cannot have Tilmeld admin ability.',
243
+ );
244
+ }
245
+ if (ability === 'tilmeld/switch') {
246
+ throw new Error(
247
+ 'Invalid Group: Groups cannot have Tilmeld switch ability.',
248
+ );
249
+ }
250
+ }
236
251
  Joi.attempt(
237
252
  group.$getValidatable(),
238
253
  Joi.object().keys({