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