@nymphjs/tilmeld 1.0.0-beta.55 → 1.0.0-beta.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/AbleObject.d.ts +15 -0
- package/dist/AbleObject.js +15 -0
- package/dist/AbleObject.js.map +1 -1
- package/dist/Group.d.ts +116 -0
- package/dist/Group.js +98 -0
- package/dist/Group.js.map +1 -1
- package/dist/Group.test.js +1 -0
- package/dist/Group.test.js.map +1 -1
- package/dist/Tilmeld.d.ts +226 -1
- package/dist/Tilmeld.js +304 -13
- package/dist/Tilmeld.js.map +1 -1
- package/dist/User.d.ts +265 -0
- package/dist/User.js +232 -0
- package/dist/User.js.map +1 -1
- package/dist/User.test.js +1 -0
- package/dist/User.test.js.map +1 -1
- package/dist/conf/d.d.ts +244 -0
- package/dist/conf/defaults.js +6 -3
- package/dist/conf/defaults.js.map +1 -1
- package/dist/errors/AccessControlError.d.ts +2 -1
- package/dist/errors/AccessControlError.js +3 -2
- package/dist/errors/AccessControlError.js.map +1 -1
- package/dist/errors/BadDataError.d.ts +2 -1
- package/dist/errors/BadDataError.js +3 -2
- package/dist/errors/BadDataError.js.map +1 -1
- package/dist/errors/BadEmailError.d.ts +2 -1
- package/dist/errors/BadEmailError.js +3 -2
- package/dist/errors/BadEmailError.js.map +1 -1
- package/dist/errors/BadUsernameError.d.ts +2 -1
- package/dist/errors/BadUsernameError.js +3 -2
- package/dist/errors/BadUsernameError.js.map +1 -1
- package/package.json +8 -7
- package/src/Tilmeld.ts +7 -3
- package/src/errors/AccessControlError.ts +4 -2
- package/src/errors/BadDataError.ts +4 -2
- package/src/errors/BadEmailError.ts +4 -2
- package/src/errors/BadUsernameError.ts +4 -2
- package/tsconfig.json +1 -1
package/dist/Tilmeld.js
CHANGED
|
@@ -11,15 +11,64 @@ const errors_1 = require("./errors");
|
|
|
11
11
|
const Group_1 = __importDefault(require("./Group"));
|
|
12
12
|
const User_1 = __importDefault(require("./User"));
|
|
13
13
|
const enforceTilmeld_1 = require("./enforceTilmeld");
|
|
14
|
+
/**
|
|
15
|
+
* A user and group system for Nymph.js.
|
|
16
|
+
*
|
|
17
|
+
* Written by Hunter Perrin for SciActive.
|
|
18
|
+
*
|
|
19
|
+
* @author Hunter Perrin <hperrin@gmail.com>
|
|
20
|
+
* @copyright SciActive Inc
|
|
21
|
+
* @see http://nymph.io/
|
|
22
|
+
*/
|
|
14
23
|
class Tilmeld {
|
|
24
|
+
/**
|
|
25
|
+
* Create a new instance of Tilmeld.
|
|
26
|
+
*
|
|
27
|
+
* @param config The Tilmeld configuration.
|
|
28
|
+
*/
|
|
15
29
|
constructor(config) {
|
|
30
|
+
/**
|
|
31
|
+
* The Nymph instance.
|
|
32
|
+
*/
|
|
16
33
|
this.nymph = {};
|
|
34
|
+
/**
|
|
35
|
+
* The currently logged in user.
|
|
36
|
+
*/
|
|
17
37
|
this.currentUser = null;
|
|
38
|
+
/**
|
|
39
|
+
* The user class for this instance of Tilmeld.
|
|
40
|
+
*/
|
|
18
41
|
this.User = User_1.default;
|
|
42
|
+
/**
|
|
43
|
+
* The group class for this instance of Tilmeld.
|
|
44
|
+
*/
|
|
19
45
|
this.Group = Group_1.default;
|
|
46
|
+
/**
|
|
47
|
+
* If you will be performing authentication functions (logging in/out), you
|
|
48
|
+
* should set these so Tilmeld can read and write cookies and headers.
|
|
49
|
+
*
|
|
50
|
+
* If you want the user to be authenticated with the cookie and/or header they
|
|
51
|
+
* provide, you should set at least the request. It's better to set both, so
|
|
52
|
+
* the JWT can be updated if needed.
|
|
53
|
+
*
|
|
54
|
+
* After you set these, call `authenticate()` to read user authentication data
|
|
55
|
+
* from them and fill the user's session.
|
|
56
|
+
*
|
|
57
|
+
* If you want to support cookie based authentication (which still requires an
|
|
58
|
+
* XSRF token for security), you should enable the cookie parser middleware.
|
|
59
|
+
*/
|
|
20
60
|
this.request = null;
|
|
21
61
|
this.response = null;
|
|
62
|
+
/**
|
|
63
|
+
* Used to avoid infinite loop.
|
|
64
|
+
*/
|
|
22
65
|
this.alreadyLoggedOutSwitch = false;
|
|
66
|
+
/**
|
|
67
|
+
* Gatekeeper ability cache.
|
|
68
|
+
*
|
|
69
|
+
* Gatekeeper will cache the user's abilities that it calculates, so it can
|
|
70
|
+
* check faster if that user has been checked before.
|
|
71
|
+
*/
|
|
23
72
|
this.gatekeeperCache = null;
|
|
24
73
|
this.config = { ...conf_1.ConfigDefaults, ...config };
|
|
25
74
|
if (this.config.emailUsernames &&
|
|
@@ -33,6 +82,11 @@ class Tilmeld {
|
|
|
33
82
|
}
|
|
34
83
|
}
|
|
35
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* This is used internally by Nymph. Don't call it yourself.
|
|
87
|
+
*
|
|
88
|
+
* @returns A clone of this instance.
|
|
89
|
+
*/
|
|
36
90
|
clone() {
|
|
37
91
|
const tilmeld = new Tilmeld(this.config);
|
|
38
92
|
tilmeld.request = this.request;
|
|
@@ -41,19 +95,30 @@ class Tilmeld {
|
|
|
41
95
|
tilmeld.gatekeeperCache = this.gatekeeperCache;
|
|
42
96
|
return tilmeld;
|
|
43
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Initialize Tilmeld.
|
|
100
|
+
*
|
|
101
|
+
* This is meant to be called internally by Nymph. Don't call this directly.
|
|
102
|
+
*
|
|
103
|
+
* @param nymph The Nymph instance.
|
|
104
|
+
*/
|
|
44
105
|
init(nymph) {
|
|
45
106
|
this.nymph = nymph;
|
|
46
107
|
if (this.nymph.driver == null) {
|
|
47
108
|
throw new Error("Tilmeld can't be configured before Nymph.");
|
|
48
109
|
}
|
|
110
|
+
// Configure the classes.
|
|
49
111
|
if (nymph.parent) {
|
|
112
|
+
// This is a cloned Nymph, it should have classes already.
|
|
50
113
|
this.User = this.nymph.getEntityClass(User_1.default);
|
|
51
114
|
this.Group = this.nymph.getEntityClass(Group_1.default);
|
|
52
115
|
}
|
|
53
116
|
else {
|
|
117
|
+
// This is a new Nymph, add the classes.
|
|
54
118
|
this.User = this.nymph.addEntityClass(User_1.default);
|
|
55
119
|
this.Group = this.nymph.addEntityClass(Group_1.default);
|
|
56
120
|
}
|
|
121
|
+
// Clone the current user so its Nymph instance is correct.
|
|
57
122
|
if (this.currentUser) {
|
|
58
123
|
const currentUserGUID = this.currentUser.guid;
|
|
59
124
|
const currentUserCDate = this.currentUser.cdate;
|
|
@@ -71,8 +136,18 @@ class Tilmeld {
|
|
|
71
136
|
this.currentUser.$updateDataProtection();
|
|
72
137
|
this.currentUser.$updateGroupDataProtection();
|
|
73
138
|
}
|
|
139
|
+
// Set up access control hooks when Nymph is called.
|
|
74
140
|
this.initAccessControl();
|
|
75
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Check to see if the current user has an ability.
|
|
144
|
+
*
|
|
145
|
+
* If `ability` is undefined, it will check to see if a user is currently
|
|
146
|
+
* logged in.
|
|
147
|
+
*
|
|
148
|
+
* @param ability The ability.
|
|
149
|
+
* @returns Whether the user has the given ability.
|
|
150
|
+
*/
|
|
76
151
|
gatekeeper(ability) {
|
|
77
152
|
if (this.currentUser == null || this.gatekeeperCache == null) {
|
|
78
153
|
return false;
|
|
@@ -84,6 +159,7 @@ class Tilmeld {
|
|
|
84
159
|
!!this.currentUser.abilities?.includes('system/admin'));
|
|
85
160
|
}
|
|
86
161
|
initAccessControl() {
|
|
162
|
+
// Check for the skip access control option and add AC selectors.
|
|
87
163
|
const handleQuery = function (nymph, options, selectors) {
|
|
88
164
|
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(nymph);
|
|
89
165
|
if (!options ||
|
|
@@ -93,38 +169,45 @@ class Tilmeld {
|
|
|
93
169
|
if (options?.source === 'client') {
|
|
94
170
|
if ((!tilmeld.currentUser ||
|
|
95
171
|
(!tilmeld.currentUser.abilities?.includes('tilmeld/admin') &&
|
|
96
|
-
!tilmeld.currentUser.abilities?.includes('system/admin'))) &&
|
|
172
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))) && // The user is not an admin.
|
|
97
173
|
((!tilmeld.config.enableUserSearch &&
|
|
98
|
-
options.class?.factorySync() instanceof User_1.default) ||
|
|
174
|
+
options.class?.factorySync() instanceof User_1.default) || // The use is searching for a user and searching is disabled.
|
|
99
175
|
(!tilmeld.config.enableGroupSearch &&
|
|
100
|
-
options.class?.factorySync() instanceof Group_1.default)) &&
|
|
101
|
-
(selectors[0] == null ||
|
|
102
|
-
selectors[0].type !== '&' ||
|
|
103
|
-
(!('guid' in selectors[0]) && !('equal' in selectors[0])) ||
|
|
176
|
+
options.class?.factorySync() instanceof Group_1.default)) && // Or the same, but for a group.
|
|
177
|
+
(selectors[0] == null || // No selector specified.
|
|
178
|
+
selectors[0].type !== '&' || // Or the type is not and.
|
|
179
|
+
(!('guid' in selectors[0]) && !('equal' in selectors[0])) || // Or they're searching for something other than guid or equal.
|
|
104
180
|
('guid' in selectors[0] &&
|
|
105
181
|
(!selectors[0].guid?.length ||
|
|
106
|
-
!selectors[0].guid?.[0]?.length)) ||
|
|
182
|
+
!selectors[0].guid?.[0]?.length)) || // Or they're not searching for a specific guid.
|
|
107
183
|
('equal' in selectors[0] &&
|
|
108
184
|
selectors[0].equal?.[0]?.[0] !== 'username' &&
|
|
109
|
-
selectors[0].equal?.[0]?.[0] !== 'groupname'))
|
|
185
|
+
selectors[0].equal?.[0]?.[0] !== 'groupname')) // Or they're not searching for a specific username/groupname.
|
|
186
|
+
) {
|
|
187
|
+
// If the user is not specifically searching for a GUID or username,
|
|
188
|
+
// and they're not allowed to search, it should fail.
|
|
110
189
|
throw new errors_1.AccessControlError('No permission to search.');
|
|
111
190
|
}
|
|
112
191
|
}
|
|
192
|
+
// Add access control selectors
|
|
113
193
|
tilmeld.addAccessControlSelectors(options, selectors);
|
|
114
194
|
}
|
|
115
195
|
};
|
|
116
196
|
handleQuery.skipOnClone = true;
|
|
197
|
+
// Filter entities being deleted for user permissions.
|
|
117
198
|
const checkPermissionsDelete = async function (nymph, entity) {
|
|
118
199
|
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(nymph);
|
|
119
200
|
if (typeof entity.$tilmeldDeleteSkipAC === 'function' &&
|
|
120
201
|
entity.$tilmeldDeleteSkipAC()) {
|
|
121
202
|
return;
|
|
122
203
|
}
|
|
204
|
+
// Test for permissions.
|
|
123
205
|
if (!tilmeld.checkPermissions(entity, nymph_1.TilmeldAccessLevels.FULL_ACCESS)) {
|
|
124
206
|
throw new errors_1.AccessControlError('No permission to delete.');
|
|
125
207
|
}
|
|
126
208
|
};
|
|
127
209
|
checkPermissionsDelete.skipOnClone = true;
|
|
210
|
+
// Filter entities being deleted for user permissions.
|
|
128
211
|
const checkPermissionsDeleteByID = async function (nymph, guid, className) {
|
|
129
212
|
const entity = await nymph.getEntity({ class: nymph.getEntityClass(className ?? 'Entity') }, { type: '&', guid: guid });
|
|
130
213
|
if (entity != null) {
|
|
@@ -132,6 +215,8 @@ class Tilmeld {
|
|
|
132
215
|
}
|
|
133
216
|
};
|
|
134
217
|
checkPermissionsDeleteByID.skipOnClone = true;
|
|
218
|
+
// Filter entities being saved for user permissions, and filter any
|
|
219
|
+
// disallowed changes to AC properties.
|
|
135
220
|
const checkPermissionsSaveAndFilterAcChanges = async function (nymph, entity) {
|
|
136
221
|
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(nymph);
|
|
137
222
|
if (!entity) {
|
|
@@ -142,6 +227,8 @@ class Tilmeld {
|
|
|
142
227
|
return;
|
|
143
228
|
}
|
|
144
229
|
if (entity.guid != null) {
|
|
230
|
+
// If the entity is not new, check that the user has full access before
|
|
231
|
+
// allowing a change to ac properties.
|
|
145
232
|
const originalAc = entity.$getOriginalAcValues();
|
|
146
233
|
const newAc = entity.$getCurrentAcValues();
|
|
147
234
|
function areAcPropertiesChanged(a, b) {
|
|
@@ -191,6 +278,7 @@ class Tilmeld {
|
|
|
191
278
|
}
|
|
192
279
|
if (areAcPropertiesChanged(originalAc, newAc)) {
|
|
193
280
|
if (!tilmeld.checkPermissions(entity, nymph_1.TilmeldAccessLevels.FULL_ACCESS, undefined, originalAc)) {
|
|
281
|
+
// Only allow changes to AC properties if the user has full access.
|
|
194
282
|
throw new errors_1.AccessControlError('No permission to change access control properties.');
|
|
195
283
|
}
|
|
196
284
|
if ((originalAc.user != null && originalAc.user !== newAc.user) ||
|
|
@@ -207,11 +295,28 @@ class Tilmeld {
|
|
|
207
295
|
}
|
|
208
296
|
}
|
|
209
297
|
}
|
|
298
|
+
// Test for permissions.
|
|
210
299
|
if (!tilmeld.checkPermissions(entity, nymph_1.TilmeldAccessLevels.WRITE_ACCESS)) {
|
|
211
300
|
throw new errors_1.AccessControlError('No permission to write.');
|
|
212
301
|
}
|
|
213
302
|
};
|
|
214
303
|
checkPermissionsSaveAndFilterAcChanges.skipOnClone = true;
|
|
304
|
+
/*
|
|
305
|
+
* Add the current user's "user", "group", and access control to new entity.
|
|
306
|
+
*
|
|
307
|
+
* This occurs right before an entity is saved. It only alters the entity
|
|
308
|
+
* if:
|
|
309
|
+
*
|
|
310
|
+
* - There is a user logged in.
|
|
311
|
+
* - The entity is new (doesn't have a GUID.)
|
|
312
|
+
* - The entity is not a user or group.
|
|
313
|
+
*
|
|
314
|
+
* Default access control is
|
|
315
|
+
*
|
|
316
|
+
* - acUser = TilmeldAccessLevels.FULL_ACCESS
|
|
317
|
+
* - acGroup = TilmeldAccessLevels.READ_ACCESS
|
|
318
|
+
* - acOther = TilmeldAccessLevels.NO_ACCESS
|
|
319
|
+
*/
|
|
215
320
|
const addAccess = async function (nymph, entity) {
|
|
216
321
|
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(nymph);
|
|
217
322
|
const user = tilmeld.currentUser;
|
|
@@ -306,6 +411,12 @@ class Tilmeld {
|
|
|
306
411
|
this.nymph.on('beforeDeleteEntity', checkPermissionsDelete);
|
|
307
412
|
this.nymph.on('beforeDeleteEntityByID', checkPermissionsDeleteByID);
|
|
308
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* Add selectors to a list of options and selectors which will limit results
|
|
416
|
+
* to only entities the current user has access to.
|
|
417
|
+
*
|
|
418
|
+
* @param optionsAndSelectors The options and selectors of the query.
|
|
419
|
+
*/
|
|
309
420
|
addAccessControlSelectors(options, selectors) {
|
|
310
421
|
const user = this.currentUser;
|
|
311
422
|
if (options == null) {
|
|
@@ -314,12 +425,15 @@ class Tilmeld {
|
|
|
314
425
|
else if ('class' in options &&
|
|
315
426
|
(options.class?.factorySync() instanceof User_1.default ||
|
|
316
427
|
options.class?.factorySync() instanceof Group_1.default)) {
|
|
428
|
+
// They are requesting a user/group. Always accessible for reading.
|
|
317
429
|
return;
|
|
318
430
|
}
|
|
319
431
|
if (user == null || user.guid == null) {
|
|
320
432
|
selectors.push({
|
|
321
433
|
type: '|',
|
|
434
|
+
// Other access control is sufficient.
|
|
322
435
|
gte: [['acOther', nymph_1.TilmeldAccessLevels.READ_ACCESS]],
|
|
436
|
+
// The user and group are not set.
|
|
323
437
|
selector: [
|
|
324
438
|
{
|
|
325
439
|
type: '&',
|
|
@@ -334,6 +448,7 @@ class Tilmeld {
|
|
|
334
448
|
type: '&',
|
|
335
449
|
'!defined': [['user', 'group']],
|
|
336
450
|
},
|
|
451
|
+
// It is owned by the user.
|
|
337
452
|
{
|
|
338
453
|
type: '&',
|
|
339
454
|
ref: [['user', user.guid]],
|
|
@@ -344,18 +459,23 @@ class Tilmeld {
|
|
|
344
459
|
const acRefs = [];
|
|
345
460
|
const gid = user.$getGid();
|
|
346
461
|
if (gid != null) {
|
|
462
|
+
// It belongs to the user's primary group.
|
|
347
463
|
groupRefs.push(['group', gid]);
|
|
464
|
+
// User's primary group is listed in acRead, acWrite, or acFull.
|
|
348
465
|
acRefs.push(['acRead', gid]);
|
|
349
466
|
acRefs.push(['acWrite', gid]);
|
|
350
467
|
acRefs.push(['acFull', gid]);
|
|
351
468
|
}
|
|
352
469
|
const gids = user.$getGids();
|
|
353
470
|
for (let curGid of gids ?? []) {
|
|
471
|
+
// It belongs to the user's secondary group.
|
|
354
472
|
groupRefs.push(['group', curGid]);
|
|
473
|
+
// User's secondary group is listed in acRead, acWrite, or acFull.
|
|
355
474
|
acRefs.push(['acRead', curGid]);
|
|
356
475
|
acRefs.push(['acWrite', curGid]);
|
|
357
476
|
acRefs.push(['acFull', curGid]);
|
|
358
477
|
}
|
|
478
|
+
// All the group refs.
|
|
359
479
|
if (groupRefs.length) {
|
|
360
480
|
subSelectors.push({
|
|
361
481
|
type: '&',
|
|
@@ -368,6 +488,7 @@ class Tilmeld {
|
|
|
368
488
|
],
|
|
369
489
|
});
|
|
370
490
|
}
|
|
491
|
+
// All the acRead, acWrite, and acFull refs.
|
|
371
492
|
if (acRefs.length) {
|
|
372
493
|
subSelectors.push({
|
|
373
494
|
type: '|',
|
|
@@ -376,8 +497,11 @@ class Tilmeld {
|
|
|
376
497
|
}
|
|
377
498
|
const selector = {
|
|
378
499
|
type: '|',
|
|
500
|
+
// Other access control is sufficient.
|
|
379
501
|
gte: [['acOther', nymph_1.TilmeldAccessLevels.READ_ACCESS]],
|
|
502
|
+
// The user and group are not set.
|
|
380
503
|
selector: subSelectors,
|
|
504
|
+
// The user is listed in acRead, acWrite, or acFull.
|
|
381
505
|
ref: [
|
|
382
506
|
['acRead', user.guid],
|
|
383
507
|
['acWrite', user.guid],
|
|
@@ -387,12 +511,76 @@ class Tilmeld {
|
|
|
387
511
|
selectors.push(selector);
|
|
388
512
|
}
|
|
389
513
|
}
|
|
514
|
+
/**
|
|
515
|
+
* Check an entity's permissions for a user.
|
|
516
|
+
*
|
|
517
|
+
* This will check the AC (Access Control) properties of the entity. These
|
|
518
|
+
* include the following properties:
|
|
519
|
+
*
|
|
520
|
+
* - acUser
|
|
521
|
+
* - acGroup
|
|
522
|
+
* - acOther
|
|
523
|
+
* - acRead
|
|
524
|
+
* - acWrite
|
|
525
|
+
* - acFull
|
|
526
|
+
*
|
|
527
|
+
* "acUser" refers to the entity's owner, "acGroup" refers to all users in the
|
|
528
|
+
* entity's group and all ancestor groups, and "acOther" refers to any user
|
|
529
|
+
* who doesn't fit these descriptions.
|
|
530
|
+
*
|
|
531
|
+
* Each of these properties should be either NO_ACCESS, READ_ACCESS,
|
|
532
|
+
* WRITE_ACCESS, or FULL_ACCESS.
|
|
533
|
+
*
|
|
534
|
+
* - NO_ACCESS - the user has no access to the entity.
|
|
535
|
+
* - READ_ACCESS, the user has read access to the entity.
|
|
536
|
+
* - WRITE_ACCESS, the user has read and write access to the entity, but can't
|
|
537
|
+
* delete it, change its access controls, or change its ownership.
|
|
538
|
+
* - FULL_ACCESS, the user has read, write, and delete access to the entity,
|
|
539
|
+
* as well as being able to manage its access controls and ownership.
|
|
540
|
+
*
|
|
541
|
+
* These properties default to:
|
|
542
|
+
*
|
|
543
|
+
* - acUser = TilmeldAccessLevels.FULL_ACCESS
|
|
544
|
+
* - acGroup = TilmeldAccessLevels.READ_ACCESS
|
|
545
|
+
* - acOther = TilmeldAccessLevels.NO_ACCESS
|
|
546
|
+
*
|
|
547
|
+
* "acRead", "acWrite", and "acFull" are arrays of users and/or groups that
|
|
548
|
+
* also have those permissions.
|
|
549
|
+
*
|
|
550
|
+
* Only users with FULL_ACCESS have the ability to change any of the ac*,
|
|
551
|
+
* user, and group properties.
|
|
552
|
+
*
|
|
553
|
+
* The following conditions will result in different checks, which determine
|
|
554
|
+
* whether the check passes:
|
|
555
|
+
*
|
|
556
|
+
* - It is a user or group. (True for READ_ACCESS or Tilmeld admins.)
|
|
557
|
+
* - No user is logged in. (Check other AC.)
|
|
558
|
+
* - The entity is the user. (Always true.)
|
|
559
|
+
* - It is the user's primary group. (True for READ_ACCESS.)
|
|
560
|
+
* - The user or its groups are listed in "acRead". (True for READ_ACCESS.)
|
|
561
|
+
* - The user or its groups are listed in "acWrite". (True for READ_ACCESS and
|
|
562
|
+
* WRITE_ACCESS.)
|
|
563
|
+
* - The user or its groups are listed in "acFull". (Always true.)
|
|
564
|
+
* - Its "user" is the user. (It is owned by the user.) (Check user AC.)
|
|
565
|
+
* - Its "group" is the user's primary group. (Check group AC.)
|
|
566
|
+
* - Its "group" is one of the user's secondary groups. (Check group AC.)
|
|
567
|
+
* - Its "group" is a descendant of one of the user's groups. (Check group
|
|
568
|
+
* AC.)
|
|
569
|
+
* - None of the above. (Check other AC.)
|
|
570
|
+
*
|
|
571
|
+
* @param entity The entity to check.
|
|
572
|
+
* @param type The lowest level of permission to consider a pass.
|
|
573
|
+
* @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
|
|
574
|
+
* @param acProperties The acProperties to use instead of getting them from the entity.
|
|
575
|
+
* @returns Whether the current user has at least `type` permission for the entity.
|
|
576
|
+
*/
|
|
390
577
|
checkPermissions(entity, type = nymph_1.TilmeldAccessLevels.READ_ACCESS, user, acProperties) {
|
|
391
578
|
if (!acProperties) {
|
|
392
579
|
acProperties = entity.$getCurrentAcValues();
|
|
393
580
|
}
|
|
394
581
|
let userOrNull = null;
|
|
395
582
|
let userOrEmpty = this.User.factorySync();
|
|
583
|
+
// Calculate the user.
|
|
396
584
|
if (user == null) {
|
|
397
585
|
userOrNull = this.currentUser;
|
|
398
586
|
userOrEmpty = this.User.current(true);
|
|
@@ -401,29 +589,35 @@ class Tilmeld {
|
|
|
401
589
|
userOrNull = user;
|
|
402
590
|
userOrEmpty = user;
|
|
403
591
|
}
|
|
592
|
+
// Users and groups are always readable. Editable by Tilmeld admins.
|
|
404
593
|
if ((entity instanceof User_1.default || entity instanceof Group_1.default) &&
|
|
405
594
|
(type === nymph_1.TilmeldAccessLevels.READ_ACCESS ||
|
|
406
595
|
userOrEmpty.abilities?.includes('tilmeld/admin') ||
|
|
407
596
|
userOrEmpty.abilities?.includes('system/admin'))) {
|
|
408
597
|
return true;
|
|
409
598
|
}
|
|
599
|
+
// Load access control, since we need it now...
|
|
410
600
|
const acUser = acProperties.acUser ?? nymph_1.TilmeldAccessLevels.FULL_ACCESS;
|
|
411
601
|
const acGroup = acProperties.acGroup ?? nymph_1.TilmeldAccessLevels.READ_ACCESS;
|
|
412
602
|
const acOther = acProperties.acOther ?? nymph_1.TilmeldAccessLevels.NO_ACCESS;
|
|
413
603
|
if (userOrNull == null) {
|
|
414
604
|
return acOther >= type;
|
|
415
605
|
}
|
|
606
|
+
// Check if the entity is the user.
|
|
416
607
|
if (userOrEmpty.$is(entity)) {
|
|
417
608
|
return true;
|
|
418
609
|
}
|
|
610
|
+
// Check if the entity is the user's group. Always readable.
|
|
419
611
|
const gid = userOrEmpty.$getGid();
|
|
420
612
|
if (gid != null &&
|
|
421
613
|
entity.guid === gid &&
|
|
422
614
|
type === nymph_1.TilmeldAccessLevels.READ_ACCESS) {
|
|
423
615
|
return true;
|
|
424
616
|
}
|
|
617
|
+
// Calculate all the groups the user belongs to.
|
|
425
618
|
let allGids = gid == null ? [] : [gid];
|
|
426
619
|
allGids = allGids.concat(userOrEmpty.$getGids() ?? []);
|
|
620
|
+
// Check access ac properties.
|
|
427
621
|
const checks = [
|
|
428
622
|
{
|
|
429
623
|
type: nymph_1.TilmeldAccessLevels.FULL_ACCESS,
|
|
@@ -450,6 +644,7 @@ class Tilmeld {
|
|
|
450
644
|
}
|
|
451
645
|
}
|
|
452
646
|
}
|
|
647
|
+
// Check ownership ac properties.
|
|
453
648
|
if (acProperties.user != null && acProperties.user === userOrNull.guid) {
|
|
454
649
|
return acUser >= type;
|
|
455
650
|
}
|
|
@@ -458,8 +653,33 @@ class Tilmeld {
|
|
|
458
653
|
}
|
|
459
654
|
return acOther >= type;
|
|
460
655
|
}
|
|
656
|
+
/**
|
|
657
|
+
* Check a UID's permissions for a user.
|
|
658
|
+
*
|
|
659
|
+
* ## THIS ONLY CHECKS AUTOMATICALLY FOR CLIENT REQUESTS.
|
|
660
|
+
*
|
|
661
|
+
* UID functions on the Node.js side are not checked automatically. This
|
|
662
|
+
* function is only run automatically for UID functions run from the client.
|
|
663
|
+
* You should call this function manually if you're running a UID function on
|
|
664
|
+
* the Node.js side and you want it gated.
|
|
665
|
+
*
|
|
666
|
+
* This will check the Tilmeld config and the user's abilities.
|
|
667
|
+
*
|
|
668
|
+
* - READ_ACCESS, the UID is listed in clientReadableUIDs or the user has the
|
|
669
|
+
* "uid/get/nameofuid" ability.
|
|
670
|
+
* - WRITE_ACCESS, the UID is listed in clientEnabledUIDs or the user has the
|
|
671
|
+
* "uid/new/nameofuid" ability.
|
|
672
|
+
* - FULL_ACCESS, the UID is listed in clientSetabledUIDs or the user has the
|
|
673
|
+
* "uid/set/nameofuid" ability.
|
|
674
|
+
*
|
|
675
|
+
* @param name The UID to check.
|
|
676
|
+
* @param type The lowest level of permission to consider a pass.
|
|
677
|
+
* @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
|
|
678
|
+
* @returns Whether the current user has at least `type` permission for the UID.
|
|
679
|
+
*/
|
|
461
680
|
async checkClientUIDPermissions(name, type = nymph_1.TilmeldAccessLevels.READ_ACCESS, user) {
|
|
462
681
|
let userOrEmpty = this.User.factorySync();
|
|
682
|
+
// Calculate the user.
|
|
463
683
|
if (user == null) {
|
|
464
684
|
userOrEmpty = this.User.current(true);
|
|
465
685
|
}
|
|
@@ -489,12 +709,21 @@ class Tilmeld {
|
|
|
489
709
|
}
|
|
490
710
|
return false;
|
|
491
711
|
}
|
|
712
|
+
/**
|
|
713
|
+
* Fill session user data.
|
|
714
|
+
*
|
|
715
|
+
* @param user The user.
|
|
716
|
+
*/
|
|
492
717
|
async fillSession(user) {
|
|
493
718
|
this.currentUser = user;
|
|
494
719
|
this.gatekeeperCache = await user.$getGatekeeperCache();
|
|
720
|
+
// Now update the data protection on the user and all the groups.
|
|
495
721
|
this.currentUser.$updateDataProtection();
|
|
496
722
|
this.currentUser.$updateGroupDataProtection();
|
|
497
723
|
}
|
|
724
|
+
/**
|
|
725
|
+
* Clear session user data.
|
|
726
|
+
*/
|
|
498
727
|
clearSession() {
|
|
499
728
|
const user = this.currentUser;
|
|
500
729
|
this.currentUser = null;
|
|
@@ -504,6 +733,12 @@ class Tilmeld {
|
|
|
504
733
|
user.$updateGroupDataProtection();
|
|
505
734
|
}
|
|
506
735
|
}
|
|
736
|
+
/**
|
|
737
|
+
* Validate and extract the user from a token.
|
|
738
|
+
*
|
|
739
|
+
* @param token The authentication token.
|
|
740
|
+
* @returns The user on success, null on failure.
|
|
741
|
+
*/
|
|
507
742
|
async extractToken(token) {
|
|
508
743
|
const extract = this.config.jwtExtract(this.config, token);
|
|
509
744
|
if (extract == null) {
|
|
@@ -519,18 +754,38 @@ class Tilmeld {
|
|
|
519
754
|
}
|
|
520
755
|
return user;
|
|
521
756
|
}
|
|
522
|
-
|
|
757
|
+
/**
|
|
758
|
+
* Check for TILMELDAUTH and TILMELDSWITCH tokens, and, if set, authenticate
|
|
759
|
+
* from it/them.
|
|
760
|
+
*
|
|
761
|
+
* You can also call this function after setting `response.locals.user` to the
|
|
762
|
+
* user you want to authenticate. You *should* check for `user.enabled` before
|
|
763
|
+
* setting this variable, unless you explicitly want to log in as a disabled
|
|
764
|
+
* user. (The user must be an instance of the User class for this Tilmeld
|
|
765
|
+
* instance.)
|
|
766
|
+
*
|
|
767
|
+
* This function will set `response.locals.user` to the logged in user on
|
|
768
|
+
* successful authentication.
|
|
769
|
+
*
|
|
770
|
+
* @param skipXsrfToken Skip the XSRF token check.
|
|
771
|
+
* @param skipRenew Skip the token renewal step, even if the token is close to expiration.
|
|
772
|
+
* @returns True if a user was authenticated, false on any failure.
|
|
773
|
+
*/
|
|
774
|
+
async authenticate(skipXsrfToken = false, skipRenew = false) {
|
|
523
775
|
if (this.request == null) {
|
|
524
776
|
return false;
|
|
525
777
|
}
|
|
526
778
|
if (this.response &&
|
|
527
779
|
this.response.locals.user &&
|
|
528
780
|
this.response.locals.user instanceof this.User) {
|
|
781
|
+
// The user has already been authenticated through some other means.
|
|
529
782
|
const user = this.response.locals.user;
|
|
530
783
|
await this.fillSession(user);
|
|
531
784
|
return true;
|
|
532
785
|
}
|
|
533
786
|
const cookies = this.request.cookies ?? {};
|
|
787
|
+
// If a client does't support cookies, they can use the X-TILMELDAUTH header
|
|
788
|
+
// to provide the auth token.
|
|
534
789
|
let fromAuthHeader = false;
|
|
535
790
|
let authToken;
|
|
536
791
|
let switchToken;
|
|
@@ -555,12 +810,16 @@ class Tilmeld {
|
|
|
555
810
|
let switchExtract;
|
|
556
811
|
if (skipXsrfToken ||
|
|
557
812
|
this.request.originalUrl.startsWith(this.config.setupPath)) {
|
|
813
|
+
// The request is for the setup app, or we were told to skip the XSRF
|
|
814
|
+
// check, so don't check for the XSRF token.
|
|
558
815
|
authExtract = this.config.jwtExtract(this.config, authToken);
|
|
559
816
|
switchExtract = switchToken
|
|
560
817
|
? this.config.jwtExtract(this.config, switchToken)
|
|
561
818
|
: null;
|
|
562
819
|
}
|
|
563
820
|
else {
|
|
821
|
+
// The request is for something else, so check for a valid XSRF token,
|
|
822
|
+
// unless the auth token is provided by a header (instead of a cookie).
|
|
564
823
|
const xsrfToken = this.request.header('x-xsrf-token');
|
|
565
824
|
if (xsrfToken == null && !fromAuthHeader) {
|
|
566
825
|
return false;
|
|
@@ -589,6 +848,7 @@ class Tilmeld {
|
|
|
589
848
|
return false;
|
|
590
849
|
}
|
|
591
850
|
if (switchGuid && switchExpire && switchExpire.getTime() > Date.now()) {
|
|
851
|
+
// Load the switch user.
|
|
592
852
|
const switchUser = await this.User.factory(switchGuid);
|
|
593
853
|
if (switchUser.guid == null) {
|
|
594
854
|
await this.logoutSwitch();
|
|
@@ -601,7 +861,10 @@ class Tilmeld {
|
|
|
601
861
|
return true;
|
|
602
862
|
}
|
|
603
863
|
}
|
|
604
|
-
if (
|
|
864
|
+
if (!skipRenew &&
|
|
865
|
+
expire.getTime() < Date.now() + this.config.jwtRenew * 1000) {
|
|
866
|
+
// If the user is less than renew time from needing a new token, give them
|
|
867
|
+
// a new one.
|
|
605
868
|
await this.login(user, fromAuthHeader);
|
|
606
869
|
}
|
|
607
870
|
else {
|
|
@@ -612,6 +875,14 @@ class Tilmeld {
|
|
|
612
875
|
}
|
|
613
876
|
return true;
|
|
614
877
|
}
|
|
878
|
+
/**
|
|
879
|
+
* Logs the given user into the system.
|
|
880
|
+
*
|
|
881
|
+
* @param user The user.
|
|
882
|
+
* @param sendAuthHeader Send the auth token as a custom header.
|
|
883
|
+
* @param sendCookie Send the auth token as a cookie.
|
|
884
|
+
* @returns True on success, false on failure.
|
|
885
|
+
*/
|
|
615
886
|
async login(user, sendAuthHeader, sendCookie = true) {
|
|
616
887
|
if (user.guid != null && user.enabled) {
|
|
617
888
|
if (this.response) {
|
|
@@ -624,7 +895,7 @@ class Tilmeld {
|
|
|
624
895
|
path: this.config.cookiePath,
|
|
625
896
|
maxAge: this.config.jwtExpire * 1000,
|
|
626
897
|
secure: appUrl.protocol === 'https:',
|
|
627
|
-
httpOnly: false,
|
|
898
|
+
httpOnly: false, // Allow JS access (for CSRF protection).
|
|
628
899
|
sameSite: appUrl.protocol === 'https:' ? 'lax' : 'strict',
|
|
629
900
|
});
|
|
630
901
|
}
|
|
@@ -639,6 +910,16 @@ class Tilmeld {
|
|
|
639
910
|
}
|
|
640
911
|
return false;
|
|
641
912
|
}
|
|
913
|
+
/**
|
|
914
|
+
* Adds a switch auth token for the given user.
|
|
915
|
+
*
|
|
916
|
+
* This effectively logs the current user in to the system as the given user.
|
|
917
|
+
*
|
|
918
|
+
* @param user The user.
|
|
919
|
+
* @param sendAuthHeader Send the auth token as a custom header.
|
|
920
|
+
* @param sendCookie Send the auth token as a cookie.
|
|
921
|
+
* @returns True on success, false on failure.
|
|
922
|
+
*/
|
|
642
923
|
async loginSwitch(user, sendAuthHeader, sendCookie = true) {
|
|
643
924
|
if (user.guid != null) {
|
|
644
925
|
if (this.response) {
|
|
@@ -651,7 +932,7 @@ class Tilmeld {
|
|
|
651
932
|
path: this.config.cookiePath,
|
|
652
933
|
maxAge: this.config.jwtSwitchExpire * 1000,
|
|
653
934
|
secure: appUrl.protocol === 'https:',
|
|
654
|
-
httpOnly: false,
|
|
935
|
+
httpOnly: false, // Allow JS access (for PubSub auth token).
|
|
655
936
|
sameSite: appUrl.protocol === 'https:' ? 'lax' : 'strict',
|
|
656
937
|
});
|
|
657
938
|
}
|
|
@@ -666,6 +947,11 @@ class Tilmeld {
|
|
|
666
947
|
}
|
|
667
948
|
return false;
|
|
668
949
|
}
|
|
950
|
+
/**
|
|
951
|
+
* Logs the current user out of the system.
|
|
952
|
+
*
|
|
953
|
+
* @param clearCookie Clear the auth cookie. (Also send a header.)
|
|
954
|
+
*/
|
|
669
955
|
async logout(clearCookie = true) {
|
|
670
956
|
if (this.request && !this.alreadyLoggedOutSwitch) {
|
|
671
957
|
const cookies = this.request.cookies ?? {};
|
|
@@ -686,6 +972,11 @@ class Tilmeld {
|
|
|
686
972
|
this.response.locals.user = null;
|
|
687
973
|
}
|
|
688
974
|
}
|
|
975
|
+
/**
|
|
976
|
+
* Clears the switch user out of the system.
|
|
977
|
+
*
|
|
978
|
+
* @param clearCookie Clear the auth cookie. (Also send a header.)
|
|
979
|
+
*/
|
|
689
980
|
async logoutSwitch(clearCookie = true) {
|
|
690
981
|
this.clearSession();
|
|
691
982
|
if (this.response) {
|
|
@@ -697,7 +988,7 @@ class Tilmeld {
|
|
|
697
988
|
this.response.set('X-TILMELDSWITCH', '');
|
|
698
989
|
}
|
|
699
990
|
this.response.locals.user = null;
|
|
700
|
-
await this.authenticate();
|
|
991
|
+
await this.authenticate(false, true);
|
|
701
992
|
}
|
|
702
993
|
}
|
|
703
994
|
}
|