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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +668 -0
  2. package/README.md +21 -5
  3. package/dist/AbleObject.d.ts +20 -2
  4. package/dist/AbleObject.js +25 -13
  5. package/dist/AbleObject.js.map +1 -1
  6. package/dist/Group.d.ts +190 -27
  7. package/dist/Group.js +827 -440
  8. package/dist/Group.js.map +1 -1
  9. package/dist/Group.testArtifacts.d.ts +3 -0
  10. package/dist/Group.testArtifacts.js +120 -0
  11. package/dist/Group.testArtifacts.js.map +1 -0
  12. package/dist/Tilmeld.d.ts +275 -14
  13. package/dist/Tilmeld.js +789 -310
  14. package/dist/Tilmeld.js.map +1 -1
  15. package/dist/Tilmeld.types.d.ts +9 -9
  16. package/dist/Tilmeld.types.js +1 -2
  17. package/dist/User.d.ts +410 -41
  18. package/dist/User.js +2042 -1057
  19. package/dist/User.js.map +1 -1
  20. package/dist/User.testArtifacts.d.ts +3 -0
  21. package/dist/User.testArtifacts.js +175 -0
  22. package/dist/User.testArtifacts.js.map +1 -0
  23. package/dist/conf/defaults.d.ts +1 -1
  24. package/dist/conf/defaults.js +215 -141
  25. package/dist/conf/defaults.js.map +1 -1
  26. package/dist/conf/index.d.ts +361 -2
  27. package/dist/conf/index.js +2 -8
  28. package/dist/conf/index.js.map +1 -1
  29. package/dist/decorators/index.d.ts +2 -0
  30. package/dist/decorators/index.js +3 -0
  31. package/dist/decorators/index.js.map +1 -0
  32. package/dist/decorators/requireAbility.d.ts +10 -0
  33. package/dist/decorators/requireAbility.js +21 -0
  34. package/dist/decorators/requireAbility.js.map +1 -0
  35. package/dist/decorators/requireAuthentication.d.ts +8 -0
  36. package/dist/decorators/requireAuthentication.js +16 -0
  37. package/dist/decorators/requireAuthentication.js.map +1 -0
  38. package/dist/enforceTilmeld.d.ts +3 -0
  39. package/dist/enforceTilmeld.js +18 -0
  40. package/dist/enforceTilmeld.js.map +1 -0
  41. package/dist/errors/AccessControlError.d.ts +2 -1
  42. package/dist/errors/AccessControlError.js +3 -6
  43. package/dist/errors/AccessControlError.js.map +1 -1
  44. package/dist/errors/BadDataError.d.ts +2 -1
  45. package/dist/errors/BadDataError.js +3 -6
  46. package/dist/errors/BadDataError.js.map +1 -1
  47. package/dist/errors/BadEmailError.d.ts +2 -1
  48. package/dist/errors/BadEmailError.js +3 -6
  49. package/dist/errors/BadEmailError.js.map +1 -1
  50. package/dist/errors/BadUsernameError.d.ts +2 -1
  51. package/dist/errors/BadUsernameError.js +3 -6
  52. package/dist/errors/BadUsernameError.js.map +1 -1
  53. package/dist/errors/CouldNotChangeDefaultPrimaryGroupError.js +1 -5
  54. package/dist/errors/CouldNotChangeDefaultPrimaryGroupError.js.map +1 -1
  55. package/dist/errors/EmailChangeRateLimitExceededError.js +1 -5
  56. package/dist/errors/EmailChangeRateLimitExceededError.js.map +1 -1
  57. package/dist/errors/index.d.ts +6 -6
  58. package/dist/errors/index.js +6 -22
  59. package/dist/errors/index.js.map +1 -1
  60. package/dist/index.d.ts +10 -14
  61. package/dist/index.js +11 -35
  62. package/dist/index.js.map +1 -1
  63. package/dist/testArtifacts.d.ts +26 -0
  64. package/dist/testArtifacts.js +1346 -0
  65. package/dist/testArtifacts.js.map +1 -0
  66. package/dist/tilmeldJoiProps.d.ts +4 -5
  67. package/dist/tilmeldJoiProps.js +13 -18
  68. package/dist/tilmeldJoiProps.js.map +1 -1
  69. package/jest.config.js +31 -5
  70. package/package.json +49 -36
  71. package/src/AbleObject.ts +17 -11
  72. package/src/Group.testArtifacts.ts +159 -0
  73. package/src/Group.ts +712 -299
  74. package/src/Tilmeld.ts +666 -344
  75. package/src/Tilmeld.types.ts +9 -9
  76. package/src/User.testArtifacts.ts +226 -0
  77. package/src/User.ts +1984 -879
  78. package/src/conf/defaults.ts +205 -109
  79. package/src/conf/index.ts +381 -2
  80. package/src/decorators/index.ts +2 -0
  81. package/src/decorators/requireAbility.ts +26 -0
  82. package/src/decorators/requireAuthentication.ts +19 -0
  83. package/src/enforceTilmeld.ts +20 -0
  84. package/src/errors/AccessControlError.ts +4 -2
  85. package/src/errors/BadDataError.ts +4 -2
  86. package/src/errors/BadEmailError.ts +4 -2
  87. package/src/errors/BadUsernameError.ts +4 -2
  88. package/src/errors/index.ts +6 -6
  89. package/src/index.ts +11 -14
  90. package/src/testArtifacts.ts +2148 -0
  91. package/src/tilmeldJoiProps.ts +9 -24
  92. package/tsconfig.json +6 -3
  93. package/typedoc.json +4 -0
  94. package/dist/Group.test.d.ts +0 -1
  95. package/dist/Group.test.js +0 -48
  96. package/dist/Group.test.js.map +0 -1
  97. package/dist/User.test.d.ts +0 -1
  98. package/dist/User.test.js +0 -51
  99. package/dist/User.test.js.map +0 -1
  100. package/dist/conf/d.d.ts +0 -55
  101. package/dist/conf/d.js +0 -3
  102. package/dist/conf/d.js.map +0 -1
  103. package/src/Group.test.ts +0 -58
  104. package/src/User.test.ts +0 -61
  105. package/src/conf/d.ts +0 -303
package/dist/Tilmeld.js CHANGED
@@ -1,144 +1,271 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const nymph_1 = require("@nymphjs/nymph");
7
- const url_1 = require("url");
8
- const lodash_1 = require("lodash");
9
- const conf_1 = require("./conf");
10
- const errors_1 = require("./errors");
11
- const Group_1 = __importDefault(require("./Group"));
12
- const User_1 = __importDefault(require("./User"));
13
- class Tilmeld {
1
+ import { URL } from 'node:url';
2
+ import { Entity, TilmeldAccessLevels, TilmeldAccessRequest, entitiesToReferences, entityConstructorsToClassNames, classNamesToEntityConstructors, referencesToEntities, } from '@nymphjs/nymph';
3
+ import { xor } from 'lodash-es';
4
+ import { splitn } from '@sciactive/splitn';
5
+ import { ConfigDefaults as defaults } from './conf/index.js';
6
+ import { AccessControlError } from './errors/index.js';
7
+ import { Group } from './Group.js';
8
+ import { User } from './User.js';
9
+ import { enforceTilmeld } from './enforceTilmeld.js';
10
+ /**
11
+ * A user and group system for Nymph.js.
12
+ *
13
+ * Written by Hunter Perrin for SciActive.
14
+ *
15
+ * @author Hunter Perrin <hperrin@port87.com>
16
+ * @copyright SciActive Inc
17
+ * @see http://nymph.io/
18
+ */
19
+ export default class Tilmeld {
20
+ /**
21
+ * The Nymph instance.
22
+ */
23
+ nymph = {};
24
+ /**
25
+ * The Tilmeld config.
26
+ */
27
+ config;
28
+ /**
29
+ * The currently logged in user.
30
+ */
31
+ currentUser = null;
32
+ /**
33
+ * The user class for this instance of Tilmeld.
34
+ */
35
+ User = User;
36
+ /**
37
+ * The group class for this instance of Tilmeld.
38
+ */
39
+ Group = Group;
40
+ /**
41
+ * If you will be performing authentication functions (logging in/out), you
42
+ * should set these so Tilmeld can read and write cookies and headers.
43
+ *
44
+ * If you want the user to be authenticated with the cookie and/or header they
45
+ * provide, you should set at least the request. It's better to set both, so
46
+ * the JWT can be updated if needed.
47
+ *
48
+ * After you set these, call `authenticate()` to read user authentication data
49
+ * from them and fill the user's session.
50
+ *
51
+ * If you want to support cookie based authentication (which still requires an
52
+ * XSRF token for security), you should enable the cookie parser middleware.
53
+ */
54
+ request = null;
55
+ response = null;
56
+ /**
57
+ * Used to avoid infinite loop.
58
+ */
59
+ alreadyLoggedOutSwitch = false;
60
+ /**
61
+ * Gatekeeper ability cache.
62
+ *
63
+ * Gatekeeper will cache the user's abilities that it calculates, so it can
64
+ * check faster if that user has been checked before.
65
+ */
66
+ gatekeeperCache = null;
67
+ /**
68
+ * Create a new instance of Tilmeld.
69
+ *
70
+ * @param config The Tilmeld configuration.
71
+ */
14
72
  constructor(config) {
15
- this.nymph = {};
16
- this.currentUser = null;
17
- this.User = User_1.default;
18
- this.Group = Group_1.default;
19
- this.request = null;
20
- this.response = null;
21
- this.skipAuthenticate = false;
22
- this.config = { ...conf_1.ConfigDefaults, ...config };
73
+ this.config = { ...defaults, ...config };
74
+ if (this.config.emailUsernames &&
75
+ (!this.config.userFields.includes('email') ||
76
+ !this.config.regFields.includes('email'))) {
77
+ throw new Error('You must include email in userFields and regFields if you use emailUsernames.');
78
+ }
79
+ for (let field of this.config.regFields) {
80
+ if (!this.config.userFields.includes(field)) {
81
+ throw new Error(`All fields in regFields must also be in userFields: ${field}`);
82
+ }
83
+ }
84
+ if (this.config.emailUsernames && this.config.domainSupport) {
85
+ throw new Error('Email usernames and domain support cannot both be enabled.');
86
+ }
23
87
  }
88
+ /**
89
+ * This is used internally by Nymph. Don't call it yourself.
90
+ *
91
+ * @returns A clone of this instance.
92
+ */
24
93
  clone() {
25
94
  const tilmeld = new Tilmeld(this.config);
26
95
  tilmeld.request = this.request;
27
96
  tilmeld.response = this.response;
28
97
  tilmeld.currentUser = this.currentUser;
29
- tilmeld.skipAuthenticate = true;
98
+ tilmeld.gatekeeperCache = this.gatekeeperCache;
30
99
  return tilmeld;
31
100
  }
101
+ /**
102
+ * Initialize Tilmeld.
103
+ *
104
+ * This is meant to be called internally by Nymph. Don't call this directly.
105
+ *
106
+ * @param nymph The Nymph instance.
107
+ */
32
108
  init(nymph) {
33
109
  this.nymph = nymph;
34
- const tilmeld = this;
35
110
  if (this.nymph.driver == null) {
36
111
  throw new Error("Tilmeld can't be configured before Nymph.");
37
112
  }
38
- class NymphUser extends User_1.default {
113
+ // Configure the classes.
114
+ if (nymph.parent) {
115
+ // This is a cloned Nymph, it should have classes already.
116
+ this.User = this.nymph.getEntityClass(User);
117
+ this.Group = this.nymph.getEntityClass(Group);
39
118
  }
40
- NymphUser.nymph = nymph;
41
- NymphUser.tilmeld = tilmeld;
42
- NymphUser.skipOnClone = true;
43
- this.User = this.nymph.addEntityClass(NymphUser);
44
- class NymphGroup extends Group_1.default {
119
+ else {
120
+ // This is a new Nymph, add the classes.
121
+ this.User = this.nymph.addEntityClass(User);
122
+ this.Group = this.nymph.addEntityClass(Group);
45
123
  }
46
- NymphGroup.nymph = nymph;
47
- NymphGroup.tilmeld = tilmeld;
48
- NymphUser.skipOnClone = true;
49
- this.Group = this.nymph.addEntityClass(NymphGroup);
50
- this.initAccessControl();
51
- if (this.request != null && !this.skipAuthenticate) {
52
- this.authenticate();
124
+ // Clone the current user so its Nymph instance is correct.
125
+ if (this.currentUser) {
126
+ const currentUserGUID = this.currentUser.guid;
127
+ const currentUserCDate = this.currentUser.cdate;
128
+ const currentUserMDate = this.currentUser.mdate;
129
+ const currentUserTags = this.currentUser.tags;
130
+ const currentUserData = entityConstructorsToClassNames(entitiesToReferences(this.currentUser.$getData(false), true));
131
+ const currentUserSData = this.currentUser.$getSData();
132
+ const currentUserOriginalAcValues = this.currentUser.$getOriginalAcValues();
133
+ const user = this.User.factorySync();
134
+ user.guid = currentUserGUID;
135
+ user.cdate = currentUserCDate;
136
+ user.mdate = currentUserMDate;
137
+ user.tags = currentUserTags;
138
+ // First, put the trusted original AC values in with the server flag.
139
+ Entity.factorySync().$putData.call(user, {
140
+ ...currentUserOriginalAcValues,
141
+ user: currentUserOriginalAcValues.user != null
142
+ ? this.User.factoryReference([
143
+ 'nymph_entity_reference',
144
+ currentUserOriginalAcValues.user,
145
+ this.User.class,
146
+ ])
147
+ : undefined,
148
+ group: currentUserOriginalAcValues.group != null
149
+ ? this.Group.factoryReference([
150
+ 'nymph_entity_reference',
151
+ currentUserOriginalAcValues.group,
152
+ this.Group.class,
153
+ ])
154
+ : undefined,
155
+ }, {}, 'server');
156
+ // Now put the real data in, that could potentially have been altered from
157
+ // the client.
158
+ user.$putData(classNamesToEntityConstructors(this.nymph, referencesToEntities(currentUserData, this.nymph, false, false), false), currentUserSData);
159
+ user.$originalEmail = this.currentUser.$originalEmail;
160
+ user.$originalUsername = this.currentUser.$originalUsername;
161
+ this.currentUser = user;
162
+ this.currentUser.$updateDataProtection();
163
+ this.currentUser.$updateGroupDataProtection();
53
164
  }
54
- this.skipAuthenticate = false;
165
+ // Set up access control hooks when Nymph is called.
166
+ this.nymph.config.debugInfo('tilmeld', 'Initializing access controls.');
167
+ this.initAccessControl();
55
168
  }
169
+ /**
170
+ * Check to see if the current user has an ability.
171
+ *
172
+ * If `ability` is undefined, it will check to see if a user is currently
173
+ * logged in.
174
+ *
175
+ * @param ability The ability.
176
+ * @returns Whether the user has the given ability.
177
+ */
56
178
  gatekeeper(ability) {
57
- if (this.currentUser == null) {
179
+ if (this.currentUser == null || this.gatekeeperCache == null) {
58
180
  return false;
59
181
  }
60
- return this.currentUser.$gatekeeper(ability);
182
+ if (ability == null) {
183
+ return true;
184
+ }
185
+ return ((ability in this.gatekeeperCache && !!this.gatekeeperCache[ability]) ||
186
+ !!this.currentUser.abilities?.includes('system/admin'));
61
187
  }
62
188
  initAccessControl() {
63
- const handleQuery = function (nymph, options, selectors) {
64
- const tilmeld = nymph.tilmeld;
189
+ // Check for the skip access control option and add AC selectors.
190
+ const handleQuery = function (nymph, { options, selectors, }) {
191
+ const tilmeld = enforceTilmeld(nymph);
65
192
  if (!options ||
66
193
  !('skipAc' in options) ||
67
194
  !options.skipAc ||
68
195
  options.source === 'client') {
69
196
  if (options?.source === 'client') {
70
- if (!tilmeld.gatekeeper('tilmeld/admin') &&
197
+ if ((!tilmeld.currentUser ||
198
+ (!tilmeld.currentUser.abilities?.includes('tilmeld/admin') &&
199
+ !tilmeld.currentUser.abilities?.includes('system/admin'))) && // The user is not an admin.
71
200
  ((!tilmeld.config.enableUserSearch &&
72
- options.class?.factorySync() instanceof User_1.default) ||
201
+ options.class?.factorySync() instanceof User) || // The use is searching for a user and searching is disabled.
73
202
  (!tilmeld.config.enableGroupSearch &&
74
- options.class?.factorySync() instanceof Group_1.default)) &&
75
- (selectors[0] == null ||
76
- selectors[0].type !== '&' ||
77
- (!('guid' in selectors[0]) && !('equal' in selectors[0])) ||
203
+ options.class?.factorySync() instanceof Group)) && // Or the same, but for a group.
204
+ (selectors.length > 1 || // There's more than one selector.
205
+ selectors[0] == null || // Or no selector specified.
206
+ selectors[0].type !== '&' || // Or the type is not and.
207
+ Object.keys(selectors[0]).length > 2 || // Or there's more than one clause.
208
+ (!('guid' in selectors[0]) && !('equal' in selectors[0])) || // Or they're searching for something other than guid or equal.
78
209
  ('guid' in selectors[0] &&
79
210
  (!selectors[0].guid?.length ||
80
- !selectors[0].guid?.[0]?.length)) ||
211
+ !selectors[0].guid?.[0]?.length)) || // Or they're not searching for a specific guid.
81
212
  ('equal' in selectors[0] &&
82
213
  selectors[0].equal?.[0]?.[0] !== 'username' &&
83
- selectors[0].equal?.[0]?.[0] !== 'groupname'))) {
84
- throw new errors_1.AccessControlError('No permission to search.');
214
+ selectors[0].equal?.[0]?.[0] !== 'groupname')) // Or they're not searching for a specific username/groupname.
215
+ ) {
216
+ // If the user is not specifically searching for a GUID or username,
217
+ // and they're not allowed to search, it should fail.
218
+ throw new AccessControlError('No permission to search.');
85
219
  }
86
220
  }
221
+ // Add access control selectors
87
222
  tilmeld.addAccessControlSelectors(options, selectors);
88
223
  }
89
224
  };
90
225
  handleQuery.skipOnClone = true;
226
+ // Filter entities being deleted for user permissions.
91
227
  const checkPermissionsDelete = async function (nymph, entity) {
92
- const tilmeld = nymph.tilmeld;
93
- if (typeof entity.$tilmeldDeleteSkipAC === 'function' &&
228
+ const tilmeld = enforceTilmeld(nymph);
229
+ if ('$tilmeldDeleteSkipAC' in entity &&
230
+ typeof entity.$tilmeldDeleteSkipAC === 'function' &&
94
231
  entity.$tilmeldDeleteSkipAC()) {
95
232
  return;
96
233
  }
97
- if (!tilmeld.checkPermissions(entity, nymph_1.TilmeldAccessLevels.FULL_ACCESS)) {
98
- throw new errors_1.AccessControlError('No permission to delete.');
234
+ // Test for permissions.
235
+ if (!tilmeld.checkPermissions(entity, TilmeldAccessLevels.FULL_ACCESS)) {
236
+ throw new AccessControlError('No permission to delete.');
99
237
  }
100
238
  };
101
239
  checkPermissionsDelete.skipOnClone = true;
240
+ // Filter entities being deleted for user permissions.
102
241
  const checkPermissionsDeleteByID = async function (nymph, guid, className) {
103
- const entity = nymph.driver.getEntitySync({ class: nymph.getEntityClass(className ?? 'Entity') }, { type: '&', guid: guid });
104
- if (entity != null) {
105
- checkPermissionsDelete(nymph, entity);
242
+ if (className === 'User' || className === 'Group') {
243
+ throw new AccessControlError("You can't delete users or groups by ID.");
244
+ }
245
+ const entity = await nymph.getEntity({ class: nymph.getEntityClass(className ?? 'Entity') }, { type: '&', guid: guid });
246
+ if (entity == null) {
247
+ throw new AccessControlError("You can't delete this entity by ID.");
106
248
  }
249
+ await checkPermissionsDelete(nymph, entity);
107
250
  };
108
251
  checkPermissionsDeleteByID.skipOnClone = true;
252
+ // Filter entities being saved for user permissions, and filter any
253
+ // disallowed changes to AC properties.
109
254
  const checkPermissionsSaveAndFilterAcChanges = async function (nymph, entity) {
110
- const tilmeld = nymph.tilmeld;
255
+ const tilmeld = enforceTilmeld(nymph);
111
256
  if (!entity) {
112
257
  return;
113
258
  }
114
- if (typeof entity.$tilmeldSaveSkipAC === 'function' &&
259
+ if ('$tilmeldSaveSkipAC' in entity &&
260
+ typeof entity.$tilmeldSaveSkipAC === 'function' &&
115
261
  entity.$tilmeldSaveSkipAC()) {
116
262
  return;
117
263
  }
118
264
  if (entity.guid != null) {
265
+ // If the entity is not new, check that the user has full access before
266
+ // allowing a change to ac properties.
119
267
  const originalAc = entity.$getOriginalAcValues();
120
- const newAc = {
121
- user: entity.user ?? undefined,
122
- group: entity.group ?? undefined,
123
- acUser: entity.acUser ?? undefined,
124
- acGroup: entity.acGroup ?? undefined,
125
- acOther: entity.acOther ?? undefined,
126
- acRead: entity.acRead ?? undefined,
127
- acWrite: entity.acWrite ?? undefined,
128
- acFull: entity.acFull ?? undefined,
129
- };
130
- const setAcProperties = (acValues) => {
131
- for (let name in acValues) {
132
- const value = acValues[name];
133
- if (value == null) {
134
- delete entity[name];
135
- }
136
- else {
137
- entity[name] = value;
138
- }
139
- }
140
- };
141
- setAcProperties(originalAc);
268
+ const newAc = entity.$getCurrentAcValues();
142
269
  function areAcPropertiesChanged(a, b) {
143
270
  for (const name of ['user', 'group']) {
144
271
  const aVal = a[name];
@@ -149,7 +276,7 @@ class Tilmeld {
149
276
  else if (aVal == null || bVal == null) {
150
277
  return true;
151
278
  }
152
- if (!aVal.$is(bVal)) {
279
+ if (aVal !== bVal) {
153
280
  return true;
154
281
  }
155
282
  }
@@ -178,61 +305,77 @@ class Tilmeld {
178
305
  if (aVal.length !== bVal.length) {
179
306
  return true;
180
307
  }
181
- if ((0, lodash_1.xor)(aVal.map((entity) => entity.guid), bVal.map((entity) => entity.guid)).length) {
308
+ if (xor(aVal, bVal).length) {
182
309
  return true;
183
310
  }
184
311
  }
185
312
  return false;
186
313
  }
187
314
  if (areAcPropertiesChanged(originalAc, newAc)) {
188
- if (!tilmeld.checkPermissions(entity, nymph_1.TilmeldAccessLevels.FULL_ACCESS)) {
189
- throw new errors_1.AccessControlError('No permission to change access control properties.');
315
+ if (!tilmeld.checkPermissions(entity, TilmeldAccessLevels.FULL_ACCESS, originalAc)) {
316
+ // Only allow changes to AC properties if the user has full access.
317
+ throw new AccessControlError('No permission to change access control properties.');
190
318
  }
191
- if (((originalAc.user && !originalAc.user.$is(newAc.user)) ||
192
- (!originalAc.user &&
193
- newAc.user &&
194
- !newAc.user.$is(tilmeld.currentUser))) &&
195
- !tilmeld.gatekeeper('tilmeld/admin')) {
196
- throw new errors_1.AccessControlError('No permission to assign to another user.');
319
+ if ((originalAc.user != null && originalAc.user !== newAc.user) ||
320
+ (originalAc.user == null &&
321
+ newAc.user != null &&
322
+ newAc.user !== tilmeld.currentUser?.guid)) {
323
+ throw new AccessControlError('No permission to assign to another user.');
197
324
  }
198
- if (newAc.group &&
199
- !newAc.group.$is(originalAc.group) &&
200
- !(newAc.group.$is(tilmeld.currentUser?.group) ||
201
- newAc.group.$inArray(tilmeld.currentUser?.groups ?? [])) &&
202
- !tilmeld.gatekeeper('tilmeld/admin')) {
203
- throw new errors_1.AccessControlError('No permission to assign to another group.');
325
+ if (newAc.group != null &&
326
+ newAc.group !== originalAc.group &&
327
+ !(newAc.group === tilmeld.currentUser?.$getGid() ||
328
+ (tilmeld.currentUser?.$getGids() ?? []).includes(newAc.group))) {
329
+ throw new AccessControlError('No permission to assign to another group.');
204
330
  }
205
- setAcProperties(newAc);
206
331
  }
207
332
  }
208
- if (!tilmeld.checkPermissions(entity, nymph_1.TilmeldAccessLevels.WRITE_ACCESS)) {
209
- throw new errors_1.AccessControlError('No permission to write.');
333
+ // Test for permissions.
334
+ if (!tilmeld.checkPermissions(entity, TilmeldAccessLevels.WRITE_ACCESS)) {
335
+ throw new AccessControlError('No permission to write.');
210
336
  }
211
337
  };
212
338
  checkPermissionsSaveAndFilterAcChanges.skipOnClone = true;
339
+ /*
340
+ * Add the current user's "user", "group", and access control to new entity.
341
+ *
342
+ * This occurs right before an entity is saved. It only alters the entity
343
+ * if:
344
+ *
345
+ * - There is a user logged in.
346
+ * - The entity is new (doesn't have a GUID.)
347
+ * - The entity is not a user or group.
348
+ *
349
+ * Default access control is
350
+ *
351
+ * - acUser = TilmeldAccessLevels.FULL_ACCESS
352
+ * - acGroup = TilmeldAccessLevels.READ_ACCESS
353
+ * - acOther = TilmeldAccessLevels.NO_ACCESS
354
+ */
213
355
  const addAccess = async function (nymph, entity) {
214
- const tilmeld = nymph.tilmeld;
356
+ const tilmeld = enforceTilmeld(nymph);
215
357
  const user = tilmeld.currentUser;
216
358
  if (user != null &&
217
359
  entity.guid == null &&
218
- !(entity instanceof User_1.default) &&
219
- !(entity instanceof Group_1.default)) {
360
+ !(entity instanceof User) &&
361
+ !(entity instanceof Group)) {
220
362
  if (entity.user == null) {
221
363
  entity.user = user;
222
364
  }
223
- if (entity.group == null &&
224
- user.group != null &&
225
- user.group.guid != null) {
226
- entity.group = user.group;
365
+ if (entity.group == null) {
366
+ const group = user.group;
367
+ if (group != null && group.guid != null) {
368
+ entity.group = group;
369
+ }
227
370
  }
228
371
  if (entity.acUser == null) {
229
- entity.acUser = nymph_1.TilmeldAccessLevels.FULL_ACCESS;
372
+ entity.acUser = TilmeldAccessLevels.FULL_ACCESS;
230
373
  }
231
374
  if (entity.acGroup == null) {
232
- entity.acGroup = nymph_1.TilmeldAccessLevels.READ_ACCESS;
375
+ entity.acGroup = TilmeldAccessLevels.READ_ACCESS;
233
376
  }
234
377
  if (entity.acOther == null) {
235
- entity.acOther = nymph_1.TilmeldAccessLevels.NO_ACCESS;
378
+ entity.acOther = TilmeldAccessLevels.NO_ACCESS;
236
379
  }
237
380
  if (entity.acRead == null) {
238
381
  entity.acRead = [];
@@ -247,32 +390,32 @@ class Tilmeld {
247
390
  };
248
391
  addAccess.skipOnClone = true;
249
392
  const validate = async function (_nymph, entity) {
250
- if (!(entity instanceof User_1.default) && !(entity instanceof Group_1.default)) {
393
+ if (!(entity instanceof User) && !(entity instanceof Group)) {
251
394
  const ownershipAcPropertyValidator = (prop) => {
252
395
  if (typeof prop != 'number' ||
253
- prop < nymph_1.TilmeldAccessLevels.NO_ACCESS ||
254
- prop > nymph_1.TilmeldAccessLevels.FULL_ACCESS) {
255
- throw new errors_1.AccessControlError('Invalid access control property: ' + prop);
396
+ prop < TilmeldAccessLevels.NO_ACCESS ||
397
+ prop > TilmeldAccessLevels.FULL_ACCESS) {
398
+ throw new AccessControlError('Invalid access control property: ' + prop);
256
399
  }
257
400
  };
258
401
  const accessAcPropertyValidator = (prop) => {
259
402
  if (!Array.isArray(prop)) {
260
- throw new errors_1.AccessControlError('Invalid access control property: ' + prop);
403
+ throw new AccessControlError('Invalid access control property: ' + prop);
261
404
  }
262
405
  prop.forEach((value) => {
263
- if (!(value instanceof User_1.default || value instanceof Group_1.default)) {
264
- throw new errors_1.AccessControlError('Invalid access control property: ' + prop);
406
+ if (typeof value !== 'string' || !value.match(/^[0-9a-f]{24}$/i)) {
407
+ throw new AccessControlError('Invalid access control property: ' + prop);
265
408
  }
266
409
  });
267
410
  };
268
411
  if ('user' in entity) {
269
- if (!(entity.user instanceof User_1.default)) {
270
- throw new errors_1.AccessControlError('Invalid access control property: user');
412
+ if (!(entity.user instanceof User)) {
413
+ throw new AccessControlError('Invalid access control property: user');
271
414
  }
272
415
  }
273
416
  if ('group' in entity) {
274
- if (!(entity.group instanceof Group_1.default)) {
275
- throw new errors_1.AccessControlError('Invalid access control property: group');
417
+ if (!(entity.group instanceof Group)) {
418
+ throw new AccessControlError('Invalid access control property: group');
276
419
  }
277
420
  }
278
421
  if ('acUser' in entity) {
@@ -303,332 +446,668 @@ class Tilmeld {
303
446
  this.nymph.on('beforeDeleteEntity', checkPermissionsDelete);
304
447
  this.nymph.on('beforeDeleteEntityByID', checkPermissionsDeleteByID);
305
448
  }
449
+ /**
450
+ * Add selectors to a list of options and selectors which will limit results
451
+ * to only entities the current user has access to.
452
+ *
453
+ * @param optionsAndSelectors The options and selectors of the query.
454
+ */
306
455
  addAccessControlSelectors(options, selectors) {
307
456
  const user = this.currentUser;
308
- if (user != null &&
309
- (user.abilities?.indexOf('system/admin') ?? -1) !== -1) {
310
- return;
311
- }
312
457
  if (options == null) {
313
458
  throw new Error('No options in argument.');
314
459
  }
315
- else if ('class' in options &&
316
- (options.class?.factorySync() instanceof User_1.default ||
317
- options.class?.factorySync() instanceof Group_1.default)) {
460
+ else if (!('class' in options) ||
461
+ !(options.class?.prototype instanceof Entity)) {
462
+ throw new Error('No class in options.');
463
+ }
464
+ else if (options.class.class === 'Entity') {
465
+ throw new Error("Can't use Entity class directly.");
466
+ }
467
+ else if (options.class.factorySync() instanceof User ||
468
+ options.class.factorySync() instanceof Group) {
469
+ // They are requesting a user/group. Always accessible for reading.
318
470
  return;
319
471
  }
320
472
  if (user == null || user.guid == null) {
321
- selectors.push({
322
- type: '|',
323
- gte: [['acOther', nymph_1.TilmeldAccessLevels.READ_ACCESS]],
324
- selector: [
473
+ // Access requests for only public entities.
474
+ const allAccessRequests = TilmeldAccessRequest.OTHER_ACCESSIBLE | TilmeldAccessRequest.UNOWNED;
475
+ const requestedAccess = (options.acRequest ?? 0) & allAccessRequests;
476
+ const defaultAccess = requestedAccess === TilmeldAccessRequest.ALL_LEVELS;
477
+ const selector = { type: '|' };
478
+ if (defaultAccess ||
479
+ requestedAccess & TilmeldAccessRequest.OTHER_ACCESSIBLE) {
480
+ // Other access control is sufficient.
481
+ selector.gte = [['acOther', TilmeldAccessLevels.READ_ACCESS]];
482
+ }
483
+ if (defaultAccess || requestedAccess & TilmeldAccessRequest.UNOWNED) {
484
+ // The user and group are not set.
485
+ selector.selector = [
325
486
  {
326
487
  type: '&',
327
488
  '!defined': [['user', 'group']],
328
489
  },
329
- ],
330
- });
490
+ ];
491
+ }
492
+ if (Object.keys(selector).length <= 1) {
493
+ this.nymph.config.debugError('tilmeld', 'No access controls were added to the query. This should not be possible.');
494
+ throw new Error('Access control error.');
495
+ }
496
+ selectors.push(selector);
331
497
  }
332
498
  else {
333
- const subSelectors = [
334
- {
499
+ // Access requests for public and private entities.
500
+ const allAccessRequests = TilmeldAccessRequest.USER_OWNED |
501
+ TilmeldAccessRequest.PRIMARY_GROUP_OWNED |
502
+ TilmeldAccessRequest.SECONDARY_GROUP_OWNED |
503
+ TilmeldAccessRequest.USER_ACCESSIBLE |
504
+ TilmeldAccessRequest.PRIMARY_GROUP_ACCESSIBLE |
505
+ TilmeldAccessRequest.SECONDARY_GROUP_ACCESSIBLE |
506
+ TilmeldAccessRequest.OTHER_ACCESSIBLE |
507
+ TilmeldAccessRequest.UNOWNED;
508
+ const requestedAccess = (options.acRequest ?? 0) & allAccessRequests;
509
+ const defaultAccess = requestedAccess === TilmeldAccessRequest.ALL_LEVELS;
510
+ const subSelectors = [];
511
+ if (defaultAccess || requestedAccess & TilmeldAccessRequest.UNOWNED) {
512
+ subSelectors.push({
335
513
  type: '&',
514
+ // The user and group are not set.
336
515
  '!defined': [['user', 'group']],
337
- },
338
- {
339
- type: '&',
340
- ref: [['user', user.guid]],
341
- gte: [['acUser', nymph_1.TilmeldAccessLevels.READ_ACCESS]],
342
- },
343
- ];
516
+ });
517
+ }
518
+ // USER_OWNED is always enforced.
519
+ subSelectors.push(
520
+ // It is owned by the user.
521
+ {
522
+ type: '&',
523
+ ref: [['user', user.guid]],
524
+ gte: [['acUser', TilmeldAccessLevels.READ_ACCESS]],
525
+ });
344
526
  const groupRefs = [];
345
- const acRefs = [];
346
- if (user.group != null && user.group.guid != null) {
347
- groupRefs.push(['group', user.group.guid]);
348
- acRefs.push(['acRead', user.group.guid]);
349
- acRefs.push(['acWrite', user.group.guid]);
350
- acRefs.push(['acFull', user.group.guid]);
351
- }
352
- for (let curSecondaryGroup of user.groups ?? []) {
353
- if (curSecondaryGroup != null && curSecondaryGroup.guid != null) {
354
- groupRefs.push(['group', curSecondaryGroup.guid]);
355
- acRefs.push(['acRead', curSecondaryGroup.guid]);
356
- acRefs.push(['acWrite', curSecondaryGroup.guid]);
357
- acRefs.push(['acFull', curSecondaryGroup.guid]);
527
+ const acContains = [];
528
+ const gid = user.$getGid();
529
+ if (gid != null) {
530
+ if (defaultAccess ||
531
+ requestedAccess & TilmeldAccessRequest.PRIMARY_GROUP_OWNED) {
532
+ // It belongs to the user's primary group.
533
+ groupRefs.push(['group', gid]);
534
+ }
535
+ if (defaultAccess ||
536
+ requestedAccess & TilmeldAccessRequest.PRIMARY_GROUP_ACCESSIBLE) {
537
+ // User's primary group guid is listed in acRead, acWrite, or acFull.
538
+ acContains.push(['acRead', gid]);
539
+ acContains.push(['acWrite', gid]);
540
+ acContains.push(['acFull', gid]);
358
541
  }
359
542
  }
360
- for (let curDescendantGroup of user.$getDescendantGroupsSync()) {
361
- if (curDescendantGroup != null && curDescendantGroup.guid != null) {
362
- groupRefs.push(['group', curDescendantGroup.guid]);
363
- acRefs.push(['acRead', curDescendantGroup.guid]);
364
- acRefs.push(['acWrite', curDescendantGroup.guid]);
365
- acRefs.push(['acFull', curDescendantGroup.guid]);
543
+ const gids = user.$getGids();
544
+ for (let curGid of gids ?? []) {
545
+ if (defaultAccess ||
546
+ requestedAccess & TilmeldAccessRequest.SECONDARY_GROUP_OWNED) {
547
+ // It belongs to the user's secondary group.
548
+ groupRefs.push(['group', curGid]);
549
+ }
550
+ if (defaultAccess ||
551
+ requestedAccess & TilmeldAccessRequest.SECONDARY_GROUP_ACCESSIBLE) {
552
+ // User's secondary group guid is listed in acRead, acWrite, or acFull.
553
+ acContains.push(['acRead', curGid]);
554
+ acContains.push(['acWrite', curGid]);
555
+ acContains.push(['acFull', curGid]);
366
556
  }
367
557
  }
558
+ // All the group refs.
368
559
  if (groupRefs.length) {
369
560
  subSelectors.push({
370
561
  type: '&',
371
- gte: [['acGroup', nymph_1.TilmeldAccessLevels.READ_ACCESS]],
372
- selector: [
373
- {
374
- type: '|',
375
- ref: groupRefs,
376
- },
377
- ],
562
+ gte: [['acGroup', TilmeldAccessLevels.READ_ACCESS]],
563
+ selector: [{ type: '|', ref: groupRefs }],
378
564
  });
379
565
  }
380
- if (acRefs.length) {
381
- subSelectors.push({
382
- type: '|',
383
- ref: acRefs,
384
- });
566
+ // All the acRead, acWrite, and acFull contains.
567
+ if (acContains.length) {
568
+ subSelectors.push({ type: '|', contain: acContains });
569
+ }
570
+ const selector = { type: '|' };
571
+ if (defaultAccess ||
572
+ requestedAccess & TilmeldAccessRequest.OTHER_ACCESSIBLE) {
573
+ // Other access control is sufficient.
574
+ selector.gte = [['acOther', TilmeldAccessLevels.READ_ACCESS]];
385
575
  }
386
- const selector = {
387
- type: '|',
388
- gte: [['acOther', nymph_1.TilmeldAccessLevels.READ_ACCESS]],
389
- selector: subSelectors,
390
- ref: [
576
+ if (subSelectors.length) {
577
+ // One of the sub selectors.
578
+ selector.selector = subSelectors;
579
+ }
580
+ if (defaultAccess ||
581
+ requestedAccess & TilmeldAccessRequest.USER_ACCESSIBLE) {
582
+ // The user is listed in acRead, acWrite, or acFull.
583
+ selector.contain = [
391
584
  ['acRead', user.guid],
392
585
  ['acWrite', user.guid],
393
586
  ['acFull', user.guid],
394
- ],
395
- };
587
+ ];
588
+ }
589
+ if (Object.keys(selector).length <= 1) {
590
+ this.nymph.config.debugError('tilmeld', 'No access controls were added to the query. This should not be possible.');
591
+ throw new Error('Access control error.');
592
+ }
396
593
  selectors.push(selector);
397
594
  }
398
595
  }
399
- checkPermissions(entity, type = nymph_1.TilmeldAccessLevels.READ_ACCESS, user) {
400
- if (typeof entity !== 'object' || typeof entity.$is !== 'function') {
401
- return false;
402
- }
403
- let userOrNull = null;
404
- let userOrEmpty = this.User.factorySync();
405
- if (user == null) {
406
- userOrNull = this.currentUser;
407
- userOrEmpty = this.User.current(true);
408
- }
409
- else if (user !== false) {
410
- userOrNull = user;
411
- userOrEmpty = user;
412
- }
413
- if (userOrEmpty.$gatekeeper('system/admin')) {
414
- return true;
596
+ /**
597
+ * Check an entity's permissions for the current user.
598
+ *
599
+ * This will check the AC (Access Control) properties of the entity. These
600
+ * include the following properties:
601
+ *
602
+ * - acUser
603
+ * - acGroup
604
+ * - acOther
605
+ * - acRead
606
+ * - acWrite
607
+ * - acFull
608
+ *
609
+ * "acUser" refers to the entity's owner, "acGroup" refers to all users in the
610
+ * entity's group and all ancestor groups, and "acOther" refers to any user
611
+ * who doesn't fit these descriptions.
612
+ *
613
+ * Each of these properties should be either NO_ACCESS, READ_ACCESS,
614
+ * WRITE_ACCESS, or FULL_ACCESS.
615
+ *
616
+ * - NO_ACCESS - the user has no access to the entity.
617
+ * - READ_ACCESS, the user has read access to the entity.
618
+ * - WRITE_ACCESS, the user has read and write access to the entity, but can't
619
+ * delete it, change its access controls, or change its ownership.
620
+ * - FULL_ACCESS, the user has read, write, and delete access to the entity,
621
+ * as well as being able to manage its access controls and ownership.
622
+ *
623
+ * These properties default to:
624
+ *
625
+ * - acUser = TilmeldAccessLevels.FULL_ACCESS
626
+ * - acGroup = TilmeldAccessLevels.READ_ACCESS
627
+ * - acOther = TilmeldAccessLevels.NO_ACCESS
628
+ *
629
+ * "acRead", "acWrite", and "acFull" are arrays of users and/or groups that
630
+ * also have those permissions.
631
+ *
632
+ * Only users with FULL_ACCESS have the ability to change any of the ac*,
633
+ * user, and group properties.
634
+ *
635
+ * The following conditions will result in different checks, which determine
636
+ * whether the check passes:
637
+ *
638
+ * - It is a user or group. (True for READ_ACCESS or Tilmeld admins.)
639
+ * - It is a user or group that belongs to a domain the current user is an
640
+ * admin for, and domain support is enabled. (Always true.)
641
+ * - No user is logged in. (Check other AC.)
642
+ * - The entity is the user. (Always true.)
643
+ * - It is the user's primary group. (True for READ_ACCESS.)
644
+ * - The user or its groups are listed in "acRead". (True for READ_ACCESS.)
645
+ * - The user or its groups are listed in "acWrite". (True for READ_ACCESS and
646
+ * WRITE_ACCESS.)
647
+ * - The user or its groups are listed in "acFull". (Always true.)
648
+ * - Its "user" is the user. (It is owned by the user.) (Check user AC.)
649
+ * - Its "group" is the user's primary group. (Check group AC.)
650
+ * - Its "group" is one of the user's secondary groups. (Check group AC.)
651
+ * - Its "group" is a descendant of one of the user's groups. (Check group
652
+ * AC.)
653
+ * - None of the above. (Check other AC.)
654
+ *
655
+ * @param entity The entity to check.
656
+ * @param type The lowest level of permission to consider a pass.
657
+ * @param acProperties The acProperties to use instead of getting them from the entity.
658
+ * @returns Whether the current user has at least `type` permission for the entity.
659
+ */
660
+ checkPermissions(entity, type = TilmeldAccessLevels.READ_ACCESS, acProperties) {
661
+ if (!acProperties) {
662
+ acProperties = entity.$getCurrentAcValues();
415
663
  }
416
- if ((entity instanceof User_1.default || entity instanceof Group_1.default) &&
417
- (type === nymph_1.TilmeldAccessLevels.READ_ACCESS ||
418
- userOrEmpty.$gatekeeper('tilmeld/admin'))) {
664
+ let userOrEmpty = this.User.current(true);
665
+ // Users and groups are always readable. Editable by Tilmeld admins.
666
+ if ((entity instanceof User || entity instanceof Group) &&
667
+ (type === TilmeldAccessLevels.READ_ACCESS ||
668
+ this.gatekeeper('tilmeld/admin') ||
669
+ this.gatekeeper('system/admin'))) {
419
670
  return true;
420
671
  }
421
- if (entity.user == null && entity.group == null) {
422
- return true;
672
+ // Users and groups that belong to a domain are readable and editable by
673
+ // that domain's admins.
674
+ if (this.config.domainSupport &&
675
+ (entity instanceof User || entity instanceof Group)) {
676
+ const [_local, domain] = splitn((entity instanceof User
677
+ ? entity.username
678
+ : entity.groupname) ?? '', '@', 2);
679
+ if (domain && this.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
680
+ return true;
681
+ }
423
682
  }
424
- const acUser = entity.acUser ?? nymph_1.TilmeldAccessLevels.FULL_ACCESS;
425
- const acGroup = entity.acGroup ?? nymph_1.TilmeldAccessLevels.READ_ACCESS;
426
- const acOther = entity.acOther ?? nymph_1.TilmeldAccessLevels.NO_ACCESS;
427
- if (userOrNull === null) {
683
+ // Load access control, since we need it now...
684
+ const acUser = acProperties.acUser ?? TilmeldAccessLevels.FULL_ACCESS;
685
+ const acGroup = acProperties.acGroup ?? TilmeldAccessLevels.READ_ACCESS;
686
+ const acOther = acProperties.acOther ?? TilmeldAccessLevels.NO_ACCESS;
687
+ if (this.currentUser == null) {
428
688
  return acOther >= type;
429
689
  }
690
+ // Check if the entity is the user.
430
691
  if (userOrEmpty.$is(entity)) {
431
692
  return true;
432
693
  }
433
- if (userOrEmpty.group != null &&
434
- typeof userOrEmpty.group.$is === 'function' &&
435
- userOrEmpty.group.$is(entity) &&
436
- type === nymph_1.TilmeldAccessLevels.READ_ACCESS) {
694
+ // Check if the entity is the user's group. Always readable.
695
+ const gid = userOrEmpty.$getGid();
696
+ if (gid != null &&
697
+ entity.guid === gid &&
698
+ type === TilmeldAccessLevels.READ_ACCESS) {
437
699
  return true;
438
700
  }
439
- let allGroups = userOrEmpty.group == null ? [] : [userOrEmpty.group];
440
- allGroups = allGroups.concat(userOrEmpty.groups ?? []);
441
- allGroups = allGroups.concat(userOrEmpty.$getDescendantGroupsSync());
701
+ // Check if the entity is one of the user's secondary groups. Always
702
+ // readable.
703
+ const gids = userOrEmpty.$getGids();
704
+ if (gids != null &&
705
+ entity.guid != null &&
706
+ gids.indexOf(entity.guid) !== -1 &&
707
+ type === TilmeldAccessLevels.READ_ACCESS) {
708
+ return true;
709
+ }
710
+ // At this point, if the entity is a user or group, it should not be
711
+ // accessible.
712
+ if (entity instanceof User || entity instanceof Group) {
713
+ return false;
714
+ }
715
+ // Calculate all the groups the user belongs to.
716
+ let allGids = gid == null ? [] : [gid];
717
+ allGids = allGids.concat(gids ?? []);
718
+ // Check access ac properties.
442
719
  const checks = [
443
720
  {
444
- type: nymph_1.TilmeldAccessLevels.FULL_ACCESS,
445
- array: [...(entity.acFull ?? [])],
721
+ type: TilmeldAccessLevels.FULL_ACCESS,
722
+ array: [...(acProperties.acFull ?? [])],
446
723
  },
447
724
  {
448
- type: nymph_1.TilmeldAccessLevels.WRITE_ACCESS,
449
- array: [...(entity.acWrite ?? [])],
725
+ type: TilmeldAccessLevels.WRITE_ACCESS,
726
+ array: [...(acProperties.acWrite ?? [])],
450
727
  },
451
728
  {
452
- type: nymph_1.TilmeldAccessLevels.READ_ACCESS,
453
- array: [...(entity.acRead ?? [])],
729
+ type: TilmeldAccessLevels.READ_ACCESS,
730
+ array: [...(acProperties.acRead ?? [])],
454
731
  },
455
732
  ];
456
733
  for (let curCheck of checks) {
457
734
  if (type <= curCheck.type) {
458
- if (userOrEmpty.$inArray(curCheck.array)) {
735
+ if (curCheck.array.includes(userOrEmpty.guid)) {
459
736
  return true;
460
737
  }
461
- for (let curGroup of allGroups) {
462
- if (typeof curGroup.$inArray === 'function' &&
463
- curGroup.$inArray(curCheck.array)) {
738
+ for (let curGid of allGids) {
739
+ if (curCheck.array.includes(curGid)) {
464
740
  return true;
465
741
  }
466
742
  }
467
743
  }
468
744
  }
469
- if (entity.user != null &&
470
- typeof entity.user.$is === 'function' &&
471
- entity.user.$is(userOrNull)) {
745
+ // Check ownership ac properties.
746
+ if (acProperties.user != null &&
747
+ acProperties.user === this.currentUser.guid) {
472
748
  return acUser >= type;
473
749
  }
474
- if (entity.group != null &&
475
- typeof entity.group.$inArray === 'function' &&
476
- entity.group.$inArray(allGroups)) {
750
+ if (acProperties.group != null && allGids.includes(acProperties.group)) {
477
751
  return acGroup >= type;
478
752
  }
479
753
  return acOther >= type;
480
754
  }
481
- checkClientUIDPermissions(name, type = nymph_1.TilmeldAccessLevels.READ_ACCESS, user) {
755
+ /**
756
+ * Check a UID's permissions for a user.
757
+ *
758
+ * ## THIS ONLY CHECKS AUTOMATICALLY FOR CLIENT REQUESTS.
759
+ *
760
+ * UID functions on the Node.js side are not checked automatically. This
761
+ * function is only run automatically for UID functions run from the client.
762
+ * You should call this function manually if you're running a UID function on
763
+ * the Node.js side and you want it gated.
764
+ *
765
+ * This will check the Tilmeld config and the user's abilities.
766
+ *
767
+ * - READ_ACCESS, the UID is listed in clientReadableUIDs or the user has the
768
+ * "uid/get/nameofuid" ability.
769
+ * - WRITE_ACCESS, the UID is listed in clientEnabledUIDs or the user has the
770
+ * "uid/new/nameofuid" ability.
771
+ * - FULL_ACCESS, the UID is listed in clientSetabledUIDs or the user has the
772
+ * "uid/set/nameofuid" ability.
773
+ *
774
+ * @param name The UID to check.
775
+ * @param type The lowest level of permission to consider a pass.
776
+ * @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
777
+ * @returns Whether the current user has at least `type` permission for the UID.
778
+ */
779
+ async checkClientUIDPermissions(name, type = TilmeldAccessLevels.READ_ACCESS, user) {
482
780
  let userOrEmpty = this.User.factorySync();
781
+ // Calculate the user.
483
782
  if (user == null) {
484
783
  userOrEmpty = this.User.current(true);
485
784
  }
486
785
  else if (user !== false) {
487
786
  userOrEmpty = user;
488
787
  }
489
- if (userOrEmpty.$gatekeeper('system/admin')) {
788
+ if (userOrEmpty.abilities?.includes('system/admin')) {
490
789
  return true;
491
790
  }
492
- if (type === nymph_1.TilmeldAccessLevels.FULL_ACCESS) {
493
- return (this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
494
- userOrEmpty.$gatekeeper(`uid/set/${name}`));
791
+ if (type === TilmeldAccessLevels.FULL_ACCESS) {
792
+ return (this.config.clientSetabledUIDs.includes(name) ||
793
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`)));
495
794
  }
496
- else if (type === nymph_1.TilmeldAccessLevels.WRITE_ACCESS) {
497
- return (this.config.clientEnabledUIDs.indexOf(name) !== -1 ||
498
- this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
499
- userOrEmpty.$gatekeeper(`uid/new/${name}`) ||
500
- userOrEmpty.$gatekeeper(`uid/set/${name}`));
795
+ else if (type === TilmeldAccessLevels.WRITE_ACCESS) {
796
+ return (this.config.clientEnabledUIDs.includes(name) ||
797
+ this.config.clientSetabledUIDs.includes(name) ||
798
+ (await userOrEmpty.$gatekeeper(`uid/new/${name}`)) ||
799
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`)));
501
800
  }
502
- else if (type === nymph_1.TilmeldAccessLevels.READ_ACCESS) {
503
- return (this.config.clientReadableUIDs.indexOf(name) !== -1 ||
504
- this.config.clientEnabledUIDs.indexOf(name) !== -1 ||
505
- this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
506
- userOrEmpty.$gatekeeper(`uid/get/${name}`) ||
507
- userOrEmpty.$gatekeeper(`uid/new/${name}`) ||
508
- userOrEmpty.$gatekeeper(`uid/set/${name}`));
801
+ else if (type === TilmeldAccessLevels.READ_ACCESS) {
802
+ return (this.config.clientReadableUIDs.includes(name) ||
803
+ this.config.clientEnabledUIDs.includes(name) ||
804
+ this.config.clientSetabledUIDs.includes(name) ||
805
+ (await userOrEmpty.$gatekeeper(`uid/get/${name}`)) ||
806
+ (await userOrEmpty.$gatekeeper(`uid/new/${name}`)) ||
807
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`)));
509
808
  }
510
809
  return false;
511
810
  }
512
- fillSession(user) {
513
- const _group = user.group;
514
- const _groups = user.groups;
811
+ /**
812
+ * Fill session user data.
813
+ *
814
+ * @param user The user.
815
+ */
816
+ async fillSession(user) {
817
+ // Clear any currently logged in user.
818
+ const oldUser = this.currentUser;
819
+ this.currentUser = null;
820
+ this.gatekeeperCache = null;
821
+ if (oldUser) {
822
+ oldUser.$updateDataProtection();
823
+ oldUser.$updateGroupDataProtection();
824
+ }
825
+ // Update data protection on the incoming user.
826
+ user.$updateDataProtection();
827
+ user.$updateGroupDataProtection();
828
+ // Get the gatekeeper cache for incoming user.
829
+ this.gatekeeperCache = await user.$getGatekeeperCache();
830
+ // Now we're ready to set the current user.
515
831
  this.currentUser = user;
832
+ // And update the data protection on the user and all the groups.
516
833
  this.currentUser.$updateDataProtection();
517
- if (this.currentUser.group != null) {
518
- this.currentUser.group.$updateDataProtection();
519
- }
520
- for (let group of this.currentUser.groups ?? []) {
521
- group.$updateDataProtection();
522
- }
834
+ this.currentUser.$updateGroupDataProtection();
523
835
  }
836
+ /**
837
+ * Clear session user data.
838
+ */
524
839
  clearSession() {
525
840
  const user = this.currentUser;
526
841
  this.currentUser = null;
842
+ this.gatekeeperCache = null;
527
843
  if (user) {
528
844
  user.$updateDataProtection();
529
- if (user.group != null) {
530
- user.group.$updateDataProtection();
531
- }
532
- for (let group of user.groups ?? []) {
533
- group.$updateDataProtection();
534
- }
845
+ user.$updateGroupDataProtection();
535
846
  }
536
847
  }
537
- async extractToken(token) {
538
- const extract = this.config.jwtExtract(this.config, token);
848
+ /**
849
+ * Validate and extract the user from a token.
850
+ *
851
+ * For primary tokens, you want to enforce user restrictions. This keeps
852
+ * disabled users from logging in and protects accounts with token revocation
853
+ * dates.
854
+ *
855
+ * For switch tokens, you don't want to enforce user restrictions. This allows
856
+ * an admin to switch to a user even if they are disabled or have revoked the
857
+ * switch token. Admin actions take precedence over anything a normal user can
858
+ * do.
859
+ *
860
+ * @param token The authentication token.
861
+ * @param enforceUserRestrictions Validate that the user is enabled and the token was issued after user's revocation date.
862
+ * @param xsrfToken Validate this XSRF Token against the auth token.
863
+ * @returns The user and extraction on success, null on failure.
864
+ */
865
+ async extractToken(token, enforceUserRestrictions, xsrfToken) {
866
+ this.nymph.config.debugInfo('tilmeld', 'Extracting and validating token.');
867
+ const extract = this.config.jwtExtract(this.config, token, xsrfToken);
539
868
  if (extract == null) {
540
- return null;
869
+ return { user: null, extract };
870
+ }
871
+ const { guid, issued, expire } = extract;
872
+ if (expire.getTime() <= Date.now() || issued.getTime() > Date.now()) {
873
+ return { user: null, extract };
541
874
  }
542
- const { guid } = extract;
543
- const user = await this.nymph.getEntity({ class: this.User }, {
544
- type: '&',
545
- guid: guid,
546
- });
547
- if (!user || !user.guid || !user.enabled) {
548
- return null;
875
+ const user = await this.nymph.getEntity({ class: this.User }, { type: '&', guid });
876
+ if (user == null ||
877
+ user.guid == null ||
878
+ (enforceUserRestrictions &&
879
+ (!user.enabled ||
880
+ (user.revokeTokenDate != null &&
881
+ issued.getTime() <= user.revokeTokenDate)))) {
882
+ return { user: null, extract };
549
883
  }
550
- return user;
884
+ return { user, extract };
551
885
  }
552
- authenticate(skipXsrfToken = false) {
886
+ /**
887
+ * Check for TILMELDAUTH and TILMELDSWITCH tokens, and, if set, authenticate
888
+ * from it/them.
889
+ *
890
+ * You can also call this function after setting `response.locals.user` to the
891
+ * user you want to authenticate. You *should* check for `user.enabled` before
892
+ * setting this variable, unless you explicitly want to log in as a disabled
893
+ * user. (The user must be an instance of the User class for this Tilmeld
894
+ * instance.)
895
+ *
896
+ * This function will set `response.locals.user` to the logged in user on
897
+ * successful authentication.
898
+ *
899
+ * @param skipXsrfToken Skip the XSRF token check.
900
+ * @param skipRenew Skip the token renewal step, even if the token is close to expiration.
901
+ * @returns True if a user was authenticated, false on any failure.
902
+ */
903
+ async authenticate(skipXsrfToken = false, skipRenew = false) {
553
904
  if (this.request == null) {
554
905
  return false;
555
906
  }
907
+ if (this.response &&
908
+ this.response.locals.user &&
909
+ this.response.locals.user instanceof this.User) {
910
+ // The user has already been authenticated through some other means.
911
+ const user = this.response.locals.user;
912
+ await this.fillSession(user);
913
+ return true;
914
+ }
556
915
  const cookies = this.request.cookies ?? {};
916
+ // If a client does't support cookies, they can use the X-TILMELDAUTH header
917
+ // to provide the auth token.
557
918
  let fromAuthHeader = false;
558
919
  let authToken;
920
+ let switchToken;
559
921
  if (this.request.header('x-tilmeldauth') != null) {
560
922
  fromAuthHeader = true;
561
923
  authToken = this.request.header('x-tilmeldauth');
924
+ switchToken =
925
+ this.request.header('x-tilmeldswitch') ||
926
+ 'TILMELDSWITCH' in cookies
927
+ ? cookies.TILMELDSWITCH
928
+ : null;
562
929
  }
563
930
  else if ('TILMELDAUTH' in cookies) {
564
931
  fromAuthHeader = false;
565
932
  authToken = cookies.TILMELDAUTH;
933
+ switchToken = 'TILMELDSWITCH' in cookies ? cookies.TILMELDSWITCH : null;
566
934
  }
567
935
  else {
936
+ this.clearSession();
568
937
  return false;
569
938
  }
570
- let extract;
939
+ let user;
940
+ let authExtract;
941
+ let switchUser = null;
942
+ let switchExtract = null;
943
+ let xsrfToken = undefined;
571
944
  if (skipXsrfToken ||
572
945
  this.request.originalUrl.startsWith(this.config.setupPath)) {
573
- extract = this.config.jwtExtract(this.config, authToken);
946
+ // The request is for the setup app, or we were told to skip the XSRF
947
+ // check, so don't check for the XSRF token.
574
948
  }
575
949
  else {
576
- const xsrfToken = this.request.header('x-xsrf-token');
950
+ // The request is for something else, so check for a valid XSRF token,
951
+ // unless the auth token is provided by a header (instead of a cookie).
952
+ xsrfToken = this.request.header('x-xsrf-token');
577
953
  if (xsrfToken == null && !fromAuthHeader) {
578
954
  return false;
579
955
  }
580
- extract = this.config.jwtExtract(this.config, authToken, xsrfToken);
581
956
  }
582
- if (extract == null) {
583
- this.logout();
584
- return false;
957
+ ({ user, extract: authExtract } = await this.extractToken(authToken, true, xsrfToken));
958
+ if (switchToken) {
959
+ ({ user: switchUser, extract: switchExtract } = await this.extractToken(switchToken, false));
585
960
  }
586
- const { guid, expire } = extract;
587
- const user = this.User.factorySync(guid);
588
- if (user.guid == null || !user.enabled) {
589
- this.logout();
961
+ if (user == null || authExtract == null || authExtract.switch) {
962
+ await this.logout();
590
963
  return false;
591
964
  }
592
- if (expire.valueOf() < Date.now() + this.config.jwtRenew * 1000) {
593
- this.login(user, fromAuthHeader);
965
+ if (switchToken) {
966
+ if (switchUser == null ||
967
+ switchExtract == null ||
968
+ !switchExtract.switch ||
969
+ switchExtract.sub !== user.guid) {
970
+ await this.logoutSwitch();
971
+ }
972
+ else {
973
+ await this.fillSession(switchUser);
974
+ if (this.response) {
975
+ this.response.locals.user = switchUser;
976
+ }
977
+ return true;
978
+ }
979
+ }
980
+ if (!skipRenew &&
981
+ authExtract &&
982
+ authExtract.expire.getTime() < Date.now() + this.config.jwtRenew * 1000) {
983
+ // If the user is less than renew time from needing a new token, give them
984
+ // a new one.
985
+ await this.login(user, fromAuthHeader);
594
986
  }
595
987
  else {
596
- this.fillSession(user);
988
+ await this.fillSession(user);
989
+ }
990
+ if (this.response) {
991
+ this.response.locals.user = user;
597
992
  }
598
993
  return true;
599
994
  }
600
- login(user, sendAuthHeader) {
995
+ /**
996
+ * Logs the given user into the system.
997
+ *
998
+ * @param user The user.
999
+ * @param sendAuthHeader Send the auth token as a custom header.
1000
+ * @param sendCookie Send the auth token as a cookie.
1001
+ * @returns True on success, false on failure.
1002
+ */
1003
+ async login(user, sendAuthHeader, sendCookie = true) {
601
1004
  if (user.guid != null && user.enabled) {
602
- if (this.response && !this.response.headersSent) {
603
- const token = this.config.jwtBuilder(this.config, user);
604
- const appUrl = new url_1.URL(this.config.appUrl);
605
- this.response.cookie('TILMELDAUTH', token, {
606
- domain: this.config.cookieDomain,
607
- path: this.config.cookiePath,
608
- maxAge: this.config.jwtExpire * 1000,
609
- secure: appUrl.protocol === 'https',
610
- httpOnly: false,
611
- sameSite: appUrl.protocol === 'https' ? 'lax' : 'strict',
612
- });
613
- if (sendAuthHeader) {
614
- this.response.set('X-TILMELDAUTH', token);
1005
+ if (this.response) {
1006
+ if (!this.response.headersSent) {
1007
+ const token = this.config.jwtBuilder(this.config, user);
1008
+ if (sendCookie) {
1009
+ const appUrl = new URL(this.config.appUrl);
1010
+ this.response.cookie('TILMELDAUTH', token, {
1011
+ domain: this.config.cookieDomain,
1012
+ path: this.config.cookiePath,
1013
+ maxAge: this.config.jwtExpire * 1000,
1014
+ secure: appUrl.protocol === 'https:',
1015
+ httpOnly: false, // Allow JS access (for CSRF protection).
1016
+ sameSite: appUrl.protocol === 'https:' ? 'lax' : 'strict',
1017
+ });
1018
+ }
1019
+ if (sendAuthHeader) {
1020
+ this.response.set('X-TILMELDAUTH', token);
1021
+ }
615
1022
  }
1023
+ this.response.locals.user = user;
616
1024
  }
617
- this.fillSession(user);
1025
+ await this.fillSession(user);
618
1026
  return true;
619
1027
  }
620
1028
  return false;
621
1029
  }
622
- logout() {
1030
+ /**
1031
+ * Adds a switch auth token for the given user.
1032
+ *
1033
+ * This effectively logs the current user in to the system as the given user.
1034
+ *
1035
+ * @param user The current user doing the switch.
1036
+ * @param target The target user being switched to.
1037
+ * @param sendAuthHeader Send the auth token as a custom header.
1038
+ * @param sendCookie Send the auth token as a cookie.
1039
+ * @returns True on success, false on failure.
1040
+ */
1041
+ async loginSwitch(user, target, sendAuthHeader, sendCookie = true) {
1042
+ if (target.guid != null) {
1043
+ if (this.response) {
1044
+ if (!this.response.headersSent) {
1045
+ const token = this.config.jwtBuilder(this.config, target, true, user);
1046
+ if (sendCookie) {
1047
+ const appUrl = new URL(this.config.appUrl);
1048
+ this.response.cookie('TILMELDSWITCH', token, {
1049
+ domain: this.config.cookieDomain,
1050
+ path: this.config.cookiePath,
1051
+ maxAge: this.config.jwtSwitchExpire * 1000,
1052
+ secure: appUrl.protocol === 'https:',
1053
+ httpOnly: false, // Allow JS access (for PubSub auth token).
1054
+ sameSite: appUrl.protocol === 'https:' ? 'lax' : 'strict',
1055
+ });
1056
+ }
1057
+ if (sendAuthHeader) {
1058
+ this.response.set('X-TILMELDSWITCH', token);
1059
+ }
1060
+ }
1061
+ this.response.locals.user = target;
1062
+ }
1063
+ await this.fillSession(target);
1064
+ return true;
1065
+ }
1066
+ return false;
1067
+ }
1068
+ /**
1069
+ * Logs the current user out of the system.
1070
+ *
1071
+ * @param clearCookie Clear the auth cookie. (Also send a header.)
1072
+ */
1073
+ async logout(clearCookie = true) {
1074
+ if (this.request && !this.alreadyLoggedOutSwitch) {
1075
+ const cookies = this.request.cookies ?? {};
1076
+ if ('TILMELDSWITCH' in cookies) {
1077
+ this.alreadyLoggedOutSwitch = true;
1078
+ return await this.logoutSwitch(clearCookie);
1079
+ }
1080
+ }
623
1081
  this.clearSession();
624
- if (this.response && !this.response.headersSent) {
625
- this.response.clearCookie('TILMELDAUTH', {
626
- domain: this.config.cookieDomain,
627
- path: this.config.cookiePath,
628
- });
629
- this.response.set('X-TILMELDAUTH', '');
1082
+ if (this.response) {
1083
+ if (clearCookie && !this.response.headersSent) {
1084
+ this.response.clearCookie('TILMELDAUTH', {
1085
+ domain: this.config.cookieDomain,
1086
+ path: this.config.cookiePath,
1087
+ });
1088
+ this.response.set('X-TILMELDAUTH', '');
1089
+ }
1090
+ this.response.locals.user = null;
1091
+ }
1092
+ }
1093
+ /**
1094
+ * Clears the switch user out of the system.
1095
+ *
1096
+ * @param clearCookie Clear the auth cookie. (Also send a header.)
1097
+ */
1098
+ async logoutSwitch(clearCookie = true) {
1099
+ this.clearSession();
1100
+ if (this.response) {
1101
+ if (clearCookie && !this.response.headersSent) {
1102
+ this.response.clearCookie('TILMELDSWITCH', {
1103
+ domain: this.config.cookieDomain,
1104
+ path: this.config.cookiePath,
1105
+ });
1106
+ this.response.set('X-TILMELDSWITCH', '');
1107
+ }
1108
+ this.response.locals.user = null;
1109
+ await this.authenticate(false, true);
630
1110
  }
631
1111
  }
632
1112
  }
633
- exports.default = Tilmeld;
634
1113
  //# sourceMappingURL=Tilmeld.js.map