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

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