@nymphjs/tilmeld 1.0.0-alpha.2 → 1.0.0-alpha.6
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 +25 -0
- package/README.md +21 -16
- package/dist/Group.d.ts +1 -1
- package/dist/Group.js.map +1 -1
- package/dist/Group.test.js +8 -2
- package/dist/Group.test.js.map +1 -1
- package/dist/Tilmeld.d.ts +6 -7
- package/dist/Tilmeld.js +114 -56
- package/dist/Tilmeld.js.map +1 -1
- package/dist/User.d.ts +1 -1
- package/dist/User.js.map +1 -1
- package/dist/User.test.js +8 -2
- package/dist/User.test.js.map +1 -1
- package/dist/conf/d.d.ts +3 -0
- package/dist/conf/defaults.js +25 -92
- package/dist/conf/defaults.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/tilmeldJoiProps.d.ts +12 -0
- package/dist/tilmeldJoiProps.js +20 -0
- package/dist/tilmeldJoiProps.js.map +1 -0
- package/package.json +6 -6
- package/src/Group.test.ts +10 -2
- package/src/Group.ts +11 -2
- package/src/Tilmeld.ts +200 -76
- package/src/User.test.ts +10 -2
- package/src/User.ts +11 -2
- package/src/conf/d.ts +30 -0
- package/src/conf/defaults.ts +8 -47
- package/src/index.ts +3 -1
- package/src/tilmeldJoiProps.ts +46 -0
package/src/Tilmeld.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
FormattedSelector,
|
|
5
5
|
Options,
|
|
6
6
|
TilmeldInterface,
|
|
7
|
+
TilmeldAccessLevels,
|
|
7
8
|
} from '@nymphjs/nymph';
|
|
8
9
|
import { Request, Response } from 'express';
|
|
9
10
|
|
|
@@ -23,15 +24,10 @@ import { AccessControlData } from './Tilmeld.types';
|
|
|
23
24
|
* @see http://nymph.io/
|
|
24
25
|
*/
|
|
25
26
|
export default class Tilmeld implements TilmeldInterface {
|
|
26
|
-
public static NO_ACCESS = 0;
|
|
27
|
-
public static READ_ACCESS = 1;
|
|
28
|
-
public static WRITE_ACCESS = 2;
|
|
29
|
-
public static FULL_ACCESS = 4;
|
|
30
|
-
|
|
31
27
|
/**
|
|
32
28
|
* The Nymph instance.
|
|
33
29
|
*/
|
|
34
|
-
public nymph: Nymph;
|
|
30
|
+
public nymph: Nymph = {} as Nymph;
|
|
35
31
|
|
|
36
32
|
/**
|
|
37
33
|
* The Tilmeld config.
|
|
@@ -70,30 +66,32 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
70
66
|
public request: Request | null = null;
|
|
71
67
|
public response: Response | null = null;
|
|
72
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Skip the authenticate step of initialization.
|
|
71
|
+
*/
|
|
72
|
+
private skipAuthenticate = false;
|
|
73
|
+
|
|
73
74
|
/**
|
|
74
75
|
* Create a new instance of Tilmeld.
|
|
75
76
|
*
|
|
76
77
|
* @param config The Tilmeld configuration.
|
|
77
78
|
*/
|
|
78
79
|
public constructor(config: Partial<Config> & { jwtSecret: string }) {
|
|
79
|
-
this.nymph = new Nymph();
|
|
80
80
|
this.config = { ...defaults, ...config };
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* If `ability` is undefined, it will check to see if a user is currently
|
|
87
|
-
* logged in.
|
|
88
|
-
*
|
|
89
|
-
* @param ability The ability.
|
|
90
|
-
* @returns Whether the user has the given ability.
|
|
84
|
+
* This is used internally by Nymph. Don't call it yourself.
|
|
85
|
+
* @returns A clone of this instance.
|
|
91
86
|
*/
|
|
92
|
-
public
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
87
|
+
public clone() {
|
|
88
|
+
const tilmeld = new Tilmeld(this.config);
|
|
89
|
+
tilmeld.request = this.request;
|
|
90
|
+
tilmeld.response = this.response;
|
|
91
|
+
tilmeld.currentUser = this.currentUser;
|
|
92
|
+
tilmeld.skipAuthenticate = true;
|
|
93
|
+
|
|
94
|
+
return tilmeld;
|
|
97
95
|
}
|
|
98
96
|
|
|
99
97
|
/**
|
|
@@ -115,28 +113,47 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
115
113
|
class NymphUser extends User {}
|
|
116
114
|
NymphUser.nymph = this.nymph;
|
|
117
115
|
NymphUser.tilmeld = this;
|
|
116
|
+
(NymphUser as any).skipOnClone = true;
|
|
118
117
|
this.User = NymphUser;
|
|
119
|
-
this.nymph.
|
|
118
|
+
this.nymph.addEntityClass(NymphUser);
|
|
120
119
|
class NymphGroup extends Group {}
|
|
121
120
|
NymphGroup.nymph = this.nymph;
|
|
122
121
|
NymphGroup.tilmeld = this;
|
|
122
|
+
(NymphUser as any).skipOnClone = true;
|
|
123
123
|
this.Group = NymphGroup;
|
|
124
|
-
this.nymph.
|
|
124
|
+
this.nymph.addEntityClass(NymphGroup);
|
|
125
125
|
|
|
126
126
|
this.initAccessControl();
|
|
127
|
-
if (this.request != null) {
|
|
127
|
+
if (this.request != null && !this.skipAuthenticate) {
|
|
128
128
|
this.authenticate();
|
|
129
129
|
}
|
|
130
|
+
this.skipAuthenticate = false;
|
|
130
131
|
}
|
|
131
132
|
|
|
132
|
-
|
|
133
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Check to see if the current user has an ability.
|
|
135
|
+
*
|
|
136
|
+
* If `ability` is undefined, it will check to see if a user is currently
|
|
137
|
+
* logged in.
|
|
138
|
+
*
|
|
139
|
+
* @param ability The ability.
|
|
140
|
+
* @returns Whether the user has the given ability.
|
|
141
|
+
*/
|
|
142
|
+
public gatekeeper(ability?: string): boolean {
|
|
143
|
+
if (this.currentUser == null) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
return this.currentUser.$gatekeeper(ability);
|
|
147
|
+
}
|
|
134
148
|
|
|
149
|
+
private initAccessControl() {
|
|
135
150
|
// Check for the skip access control option and add AC selectors.
|
|
136
151
|
const handleQuery = function (
|
|
152
|
+
nymph: Nymph,
|
|
137
153
|
options: Options,
|
|
138
154
|
selectors: FormattedSelector[]
|
|
139
155
|
) {
|
|
156
|
+
const tilmeld = nymph.tilmeld as Tilmeld;
|
|
140
157
|
if (
|
|
141
158
|
!options ||
|
|
142
159
|
!('skipAc' in options) ||
|
|
@@ -145,20 +162,20 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
145
162
|
) {
|
|
146
163
|
if (options?.source === 'client') {
|
|
147
164
|
if (
|
|
148
|
-
!tilmeld.gatekeeper('tilmeld/admin') &&
|
|
165
|
+
!tilmeld.gatekeeper('tilmeld/admin') && // The user is not an admin.
|
|
149
166
|
((!tilmeld.config.enableUserSearch &&
|
|
150
|
-
options.class?.factorySync() instanceof User) ||
|
|
167
|
+
options.class?.factorySync() instanceof User) || // The use is searching for a user and searching is disabled.
|
|
151
168
|
(!tilmeld.config.enableGroupSearch &&
|
|
152
|
-
options.class?.factorySync() instanceof Group)) &&
|
|
153
|
-
(selectors[0] == null ||
|
|
154
|
-
selectors[0].type !== '&' ||
|
|
155
|
-
(!('guid' in selectors[0]) && !('equal' in selectors[0])) ||
|
|
169
|
+
options.class?.factorySync() instanceof Group)) && // Or the same, but for a group.
|
|
170
|
+
(selectors[0] == null || // No selector specified.
|
|
171
|
+
selectors[0].type !== '&' || // Or the type is not and.
|
|
172
|
+
(!('guid' in selectors[0]) && !('equal' in selectors[0])) || // Or they're searching for something other than guid or equal.
|
|
156
173
|
('guid' in selectors[0] &&
|
|
157
|
-
selectors[0].guid?.length
|
|
158
|
-
|
|
174
|
+
(!selectors[0].guid?.length ||
|
|
175
|
+
!selectors[0].guid?.[0]?.length)) || // Or they're not searching for a specific guid.
|
|
159
176
|
('equal' in selectors[0] &&
|
|
160
|
-
selectors[0].equal?.
|
|
161
|
-
selectors[0].equal?.[0]?.[0] !== '
|
|
177
|
+
selectors[0].equal?.[0]?.[0] !== 'username' &&
|
|
178
|
+
selectors[0].equal?.[0]?.[0] !== 'groupname')) // Or they're not searching for a specific username/groupname.
|
|
162
179
|
) {
|
|
163
180
|
// If the user is not specifically searching for a GUID or username,
|
|
164
181
|
// and they're not allowed to search, it should fail.
|
|
@@ -169,9 +186,14 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
169
186
|
tilmeld.addAccessControlSelectors(options, selectors);
|
|
170
187
|
}
|
|
171
188
|
};
|
|
189
|
+
handleQuery.skipOnClone = true;
|
|
172
190
|
|
|
173
191
|
// Filter entities being deleted for user permissions.
|
|
174
|
-
const checkPermissionsDelete = function (
|
|
192
|
+
const checkPermissionsDelete = function (
|
|
193
|
+
nymph: Nymph,
|
|
194
|
+
entity: EntityInterface
|
|
195
|
+
) {
|
|
196
|
+
const tilmeld = nymph.tilmeld as Tilmeld;
|
|
175
197
|
if (
|
|
176
198
|
typeof entity.$tilmeldDeleteSkipAC === 'function' &&
|
|
177
199
|
entity.$tilmeldDeleteSkipAC()
|
|
@@ -179,30 +201,35 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
179
201
|
return;
|
|
180
202
|
}
|
|
181
203
|
// Test for permissions.
|
|
182
|
-
if (tilmeld.checkPermissions(entity,
|
|
204
|
+
if (tilmeld.checkPermissions(entity, TilmeldAccessLevels.FULL_ACCESS)) {
|
|
183
205
|
throw new AccessControlError('No permission to delete.');
|
|
184
206
|
}
|
|
185
207
|
};
|
|
208
|
+
checkPermissionsDelete.skipOnClone = true;
|
|
186
209
|
|
|
187
210
|
// Filter entities being deleted for user permissions.
|
|
188
211
|
const checkPermissionsDeleteByID = function (
|
|
212
|
+
nymph: Nymph,
|
|
189
213
|
guid: string,
|
|
190
214
|
className?: string
|
|
191
215
|
) {
|
|
192
|
-
const entity =
|
|
193
|
-
{ class:
|
|
216
|
+
const entity = nymph.driver.getEntitySync(
|
|
217
|
+
{ class: nymph.getEntityClass(className ?? 'Entity') },
|
|
194
218
|
{ type: '&', guid: guid }
|
|
195
219
|
);
|
|
196
220
|
if (entity != null) {
|
|
197
|
-
checkPermissionsDelete(entity);
|
|
221
|
+
checkPermissionsDelete(nymph, entity);
|
|
198
222
|
}
|
|
199
223
|
};
|
|
224
|
+
checkPermissionsDeleteByID.skipOnClone = true;
|
|
200
225
|
|
|
201
226
|
// Filter entities being saved for user permissions, and filter any
|
|
202
227
|
// disallowed changes to AC properties.
|
|
203
228
|
const checkPermissionsSaveAndFilterAcChanges = function (
|
|
229
|
+
nymph: Nymph,
|
|
204
230
|
entity: EntityInterface & AccessControlData
|
|
205
231
|
) {
|
|
232
|
+
const tilmeld = nymph.tilmeld as Tilmeld;
|
|
206
233
|
if (!entity) {
|
|
207
234
|
return;
|
|
208
235
|
}
|
|
@@ -242,7 +269,7 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
242
269
|
|
|
243
270
|
// Restore original AC properties and check permissions.
|
|
244
271
|
setAcProperties(originalAc);
|
|
245
|
-
if (tilmeld.checkPermissions(entity,
|
|
272
|
+
if (tilmeld.checkPermissions(entity, TilmeldAccessLevels.FULL_ACCESS)) {
|
|
246
273
|
// Only allow changes to AC properties if the user has full access.
|
|
247
274
|
// TODO: only allow changes to `user` and `group` if tilmeld admin or
|
|
248
275
|
// group is user's group.
|
|
@@ -251,10 +278,11 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
251
278
|
}
|
|
252
279
|
|
|
253
280
|
// Test for permissions.
|
|
254
|
-
if (!tilmeld.checkPermissions(entity,
|
|
281
|
+
if (!tilmeld.checkPermissions(entity, TilmeldAccessLevels.WRITE_ACCESS)) {
|
|
255
282
|
throw new AccessControlError('No permission to write.');
|
|
256
283
|
}
|
|
257
284
|
};
|
|
285
|
+
checkPermissionsSaveAndFilterAcChanges.skipOnClone = true;
|
|
258
286
|
|
|
259
287
|
/*
|
|
260
288
|
* Add the current user's "user", "group", and access control to new entity.
|
|
@@ -268,11 +296,15 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
268
296
|
*
|
|
269
297
|
* Default access control is
|
|
270
298
|
*
|
|
271
|
-
* - acUser =
|
|
272
|
-
* - acGroup =
|
|
273
|
-
* - acOther =
|
|
299
|
+
* - acUser = TilmeldAccessLevels.FULL_ACCESS
|
|
300
|
+
* - acGroup = TilmeldAccessLevels.READ_ACCESS
|
|
301
|
+
* - acOther = TilmeldAccessLevels.NO_ACCESS
|
|
274
302
|
*/
|
|
275
|
-
const addAccess = function (
|
|
303
|
+
const addAccess = function (
|
|
304
|
+
nymph: Nymph,
|
|
305
|
+
entity: EntityInterface & AccessControlData
|
|
306
|
+
) {
|
|
307
|
+
const tilmeld = nymph.tilmeld as Tilmeld;
|
|
276
308
|
const user = tilmeld.currentUser;
|
|
277
309
|
if (
|
|
278
310
|
user != null &&
|
|
@@ -291,13 +323,13 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
291
323
|
entity.group = user.group;
|
|
292
324
|
}
|
|
293
325
|
if (entity.acUser == null) {
|
|
294
|
-
entity.acUser =
|
|
326
|
+
entity.acUser = TilmeldAccessLevels.FULL_ACCESS;
|
|
295
327
|
}
|
|
296
328
|
if (entity.acGroup == null) {
|
|
297
|
-
entity.acGroup =
|
|
329
|
+
entity.acGroup = TilmeldAccessLevels.READ_ACCESS;
|
|
298
330
|
}
|
|
299
331
|
if (entity.acOther == null) {
|
|
300
|
-
entity.acOther =
|
|
332
|
+
entity.acOther = TilmeldAccessLevels.NO_ACCESS;
|
|
301
333
|
}
|
|
302
334
|
if (entity.acRead == null) {
|
|
303
335
|
entity.acRead = [];
|
|
@@ -310,39 +342,53 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
310
342
|
}
|
|
311
343
|
}
|
|
312
344
|
};
|
|
345
|
+
addAccess.skipOnClone = true;
|
|
313
346
|
|
|
314
|
-
const validate = function (
|
|
347
|
+
const validate = function (
|
|
348
|
+
_nymph: Nymph,
|
|
349
|
+
entity: EntityInterface & AccessControlData
|
|
350
|
+
) {
|
|
315
351
|
if (!(entity instanceof User) && !(entity instanceof Group)) {
|
|
316
352
|
const ownershipAcPropertyValidator = (prop: any) => {
|
|
317
353
|
if (
|
|
318
354
|
typeof prop != 'number' ||
|
|
319
|
-
prop <
|
|
320
|
-
prop >
|
|
355
|
+
prop < TilmeldAccessLevels.NO_ACCESS ||
|
|
356
|
+
prop > TilmeldAccessLevels.FULL_ACCESS
|
|
321
357
|
) {
|
|
322
|
-
throw new AccessControlError(
|
|
358
|
+
throw new AccessControlError(
|
|
359
|
+
'Invalid access control property: ' + prop
|
|
360
|
+
);
|
|
323
361
|
}
|
|
324
362
|
};
|
|
325
363
|
|
|
326
364
|
const accessAcPropertyValidator = (prop: any) => {
|
|
327
365
|
if (!Array.isArray(prop)) {
|
|
328
|
-
throw new AccessControlError(
|
|
366
|
+
throw new AccessControlError(
|
|
367
|
+
'Invalid access control property: ' + prop
|
|
368
|
+
);
|
|
329
369
|
}
|
|
330
370
|
prop.forEach((value) => {
|
|
331
371
|
if (!(value instanceof User || value instanceof Group)) {
|
|
332
|
-
throw new AccessControlError(
|
|
372
|
+
throw new AccessControlError(
|
|
373
|
+
'Invalid access control property: ' + prop
|
|
374
|
+
);
|
|
333
375
|
}
|
|
334
376
|
});
|
|
335
377
|
};
|
|
336
378
|
|
|
337
379
|
if ('user' in entity) {
|
|
338
380
|
if (!(entity.user instanceof User)) {
|
|
339
|
-
throw new AccessControlError(
|
|
381
|
+
throw new AccessControlError(
|
|
382
|
+
'Invalid access control property: user'
|
|
383
|
+
);
|
|
340
384
|
}
|
|
341
385
|
}
|
|
342
386
|
|
|
343
387
|
if ('group' in entity) {
|
|
344
388
|
if (!(entity.Group instanceof Group)) {
|
|
345
|
-
throw new AccessControlError(
|
|
389
|
+
throw new AccessControlError(
|
|
390
|
+
'Invalid access control property: group'
|
|
391
|
+
);
|
|
346
392
|
}
|
|
347
393
|
}
|
|
348
394
|
|
|
@@ -366,6 +412,7 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
366
412
|
}
|
|
367
413
|
}
|
|
368
414
|
};
|
|
415
|
+
validate.skipOnClone = true;
|
|
369
416
|
|
|
370
417
|
this.nymph.on('query', handleQuery);
|
|
371
418
|
|
|
@@ -412,7 +459,7 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
412
459
|
selectors.push({
|
|
413
460
|
type: '|',
|
|
414
461
|
// Other access control is sufficient.
|
|
415
|
-
gte: [['acOther',
|
|
462
|
+
gte: [['acOther', TilmeldAccessLevels.READ_ACCESS]],
|
|
416
463
|
// The user and group are not set.
|
|
417
464
|
selector: [
|
|
418
465
|
{
|
|
@@ -431,13 +478,13 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
431
478
|
{
|
|
432
479
|
type: '&',
|
|
433
480
|
ref: [['user', user]],
|
|
434
|
-
gte: [['acUser',
|
|
481
|
+
gte: [['acUser', TilmeldAccessLevels.READ_ACCESS]],
|
|
435
482
|
},
|
|
436
483
|
];
|
|
437
484
|
const selector: FormattedSelector = {
|
|
438
485
|
type: '|',
|
|
439
486
|
// Other access control is sufficient.
|
|
440
|
-
gte: [['acOther',
|
|
487
|
+
gte: [['acOther', TilmeldAccessLevels.READ_ACCESS]],
|
|
441
488
|
// The user and group are not set.
|
|
442
489
|
selector: subSelectors,
|
|
443
490
|
// The user is listed in acRead, acWrite, or acFull.
|
|
@@ -481,7 +528,7 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
481
528
|
if (groupRefs.length) {
|
|
482
529
|
subSelectors.push({
|
|
483
530
|
type: '&',
|
|
484
|
-
gte: [['acGroup',
|
|
531
|
+
gte: [['acGroup', TilmeldAccessLevels.READ_ACCESS]],
|
|
485
532
|
selector: [
|
|
486
533
|
{
|
|
487
534
|
type: '|',
|
|
@@ -530,9 +577,9 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
530
577
|
*
|
|
531
578
|
* These properties defaults to:
|
|
532
579
|
*
|
|
533
|
-
* - acUser =
|
|
534
|
-
* - acGroup =
|
|
535
|
-
* - acOther =
|
|
580
|
+
* - acUser = TilmeldAccessLevels.FULL_ACCESS
|
|
581
|
+
* - acGroup = TilmeldAccessLevels.READ_ACCESS
|
|
582
|
+
* - acOther = TilmeldAccessLevels.NO_ACCESS
|
|
536
583
|
*
|
|
537
584
|
* "acRead", "acWrite", and "acFull" are arrays of users and/or groups that
|
|
538
585
|
* also have those permissions.
|
|
@@ -561,13 +608,13 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
561
608
|
* - None of the above. (Check other AC.)
|
|
562
609
|
*
|
|
563
610
|
* @param entity The entity to check.
|
|
564
|
-
* @param type The lowest level of permission to consider a pass.
|
|
611
|
+
* @param type The lowest level of permission to consider a pass.
|
|
565
612
|
* @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
|
|
566
613
|
* @returns Whether the current user has at least `type` permission for the entity.
|
|
567
614
|
*/
|
|
568
615
|
public checkPermissions(
|
|
569
616
|
entity: EntityInterface & AccessControlData,
|
|
570
|
-
type =
|
|
617
|
+
type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
|
|
571
618
|
user?: (User & UserData) | false
|
|
572
619
|
) {
|
|
573
620
|
// Only works for entities.
|
|
@@ -593,7 +640,8 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
593
640
|
// Users and groups are always readable. Editable by Tilmeld admins.
|
|
594
641
|
if (
|
|
595
642
|
(entity instanceof User || entity instanceof Group) &&
|
|
596
|
-
(type ===
|
|
643
|
+
(type === TilmeldAccessLevels.READ_ACCESS ||
|
|
644
|
+
userOrEmpty.$gatekeeper('tilmeld/admin'))
|
|
597
645
|
) {
|
|
598
646
|
return true;
|
|
599
647
|
}
|
|
@@ -604,9 +652,9 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
604
652
|
}
|
|
605
653
|
|
|
606
654
|
// Load access control, since we need it now...
|
|
607
|
-
const acUser = entity.acUser ??
|
|
608
|
-
const acGroup = entity.acGroup ??
|
|
609
|
-
const acOther = entity.acOther ??
|
|
655
|
+
const acUser = entity.acUser ?? TilmeldAccessLevels.FULL_ACCESS;
|
|
656
|
+
const acGroup = entity.acGroup ?? TilmeldAccessLevels.READ_ACCESS;
|
|
657
|
+
const acOther = entity.acOther ?? TilmeldAccessLevels.NO_ACCESS;
|
|
610
658
|
|
|
611
659
|
if (userOrNull === null) {
|
|
612
660
|
return acOther >= type;
|
|
@@ -622,7 +670,7 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
622
670
|
userOrEmpty.group != null &&
|
|
623
671
|
typeof userOrEmpty.group.$is === 'function' &&
|
|
624
672
|
userOrEmpty.group.$is(entity) &&
|
|
625
|
-
type ===
|
|
673
|
+
type === TilmeldAccessLevels.READ_ACCESS
|
|
626
674
|
) {
|
|
627
675
|
return true;
|
|
628
676
|
}
|
|
@@ -634,9 +682,18 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
634
682
|
|
|
635
683
|
// Check access ac properties.
|
|
636
684
|
const checks = [
|
|
637
|
-
{
|
|
638
|
-
|
|
639
|
-
|
|
685
|
+
{
|
|
686
|
+
type: TilmeldAccessLevels.FULL_ACCESS,
|
|
687
|
+
array: [...(entity.acFull ?? [])],
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
type: TilmeldAccessLevels.WRITE_ACCESS,
|
|
691
|
+
array: [...(entity.acWrite ?? [])],
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
type: TilmeldAccessLevels.READ_ACCESS,
|
|
695
|
+
array: [...(entity.acRead ?? [])],
|
|
696
|
+
},
|
|
640
697
|
];
|
|
641
698
|
for (let curCheck of checks) {
|
|
642
699
|
if (type <= curCheck.type) {
|
|
@@ -672,6 +729,73 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
672
729
|
return acOther >= type;
|
|
673
730
|
}
|
|
674
731
|
|
|
732
|
+
/**
|
|
733
|
+
* Check a UID's permissions for a user.
|
|
734
|
+
*
|
|
735
|
+
* ## THIS ONLY CHECKS AUTOMATICALLY FOR CLIENT REQUESTS.
|
|
736
|
+
*
|
|
737
|
+
* UID functions on the Node.js side are not checked automatically. This
|
|
738
|
+
* function is only run automatically for UID functions run from the client.
|
|
739
|
+
* You should call this function manually if you're running a UID function on
|
|
740
|
+
* the Node.js side and you want it gated.
|
|
741
|
+
*
|
|
742
|
+
* This will check the Tilmeld config and the user's abilities.
|
|
743
|
+
*
|
|
744
|
+
* - READ_ACCESS, the UID is listed in clientReadableUIDs or the user has the
|
|
745
|
+
* "uid/get/nameofuid" ability.
|
|
746
|
+
* - WRITE_ACCESS, the UID is listed in clientEnabledUIDs or the user has the
|
|
747
|
+
* "uid/new/nameofuid" ability.
|
|
748
|
+
* - FULL_ACCESS, the UID is listed in clientSetabledUIDs or the user has the
|
|
749
|
+
* "uid/set/nameofuid" ability.
|
|
750
|
+
*
|
|
751
|
+
* @param name The UID to check.
|
|
752
|
+
* @param type The lowest level of permission to consider a pass.
|
|
753
|
+
* @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
|
|
754
|
+
* @returns Whether the current user has at least `type` permission for the UID.
|
|
755
|
+
*/
|
|
756
|
+
public checkClientUIDPermissions(
|
|
757
|
+
name: string,
|
|
758
|
+
type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
|
|
759
|
+
user?: (User & UserData) | false
|
|
760
|
+
) {
|
|
761
|
+
let userOrEmpty: User & UserData = this.User.factorySync();
|
|
762
|
+
// Calculate the user.
|
|
763
|
+
if (user == null) {
|
|
764
|
+
userOrEmpty = this.User.current(true);
|
|
765
|
+
} else if (user !== false) {
|
|
766
|
+
userOrEmpty = user;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (userOrEmpty.$gatekeeper('system/admin')) {
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (type === TilmeldAccessLevels.FULL_ACCESS) {
|
|
774
|
+
return (
|
|
775
|
+
this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
|
|
776
|
+
userOrEmpty.$gatekeeper(`uid/set/${name}`)
|
|
777
|
+
);
|
|
778
|
+
} else if (type === TilmeldAccessLevels.WRITE_ACCESS) {
|
|
779
|
+
return (
|
|
780
|
+
this.config.clientEnabledUIDs.indexOf(name) !== -1 ||
|
|
781
|
+
this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
|
|
782
|
+
userOrEmpty.$gatekeeper(`uid/new/${name}`) ||
|
|
783
|
+
userOrEmpty.$gatekeeper(`uid/set/${name}`)
|
|
784
|
+
);
|
|
785
|
+
} else if (type === TilmeldAccessLevels.READ_ACCESS) {
|
|
786
|
+
return (
|
|
787
|
+
this.config.clientReadableUIDs.indexOf(name) !== -1 ||
|
|
788
|
+
this.config.clientEnabledUIDs.indexOf(name) !== -1 ||
|
|
789
|
+
this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
|
|
790
|
+
userOrEmpty.$gatekeeper(`uid/get/${name}`) ||
|
|
791
|
+
userOrEmpty.$gatekeeper(`uid/new/${name}`) ||
|
|
792
|
+
userOrEmpty.$gatekeeper(`uid/set/${name}`)
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
798
|
+
|
|
675
799
|
/**
|
|
676
800
|
* Fill session user data.
|
|
677
801
|
*
|
|
@@ -754,9 +878,9 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
754
878
|
// to provide the auth token.
|
|
755
879
|
let fromAuthHeader = false;
|
|
756
880
|
let authToken: string;
|
|
757
|
-
if (this.request.header('
|
|
881
|
+
if (this.request.header('x-tilmeldauth') != null) {
|
|
758
882
|
fromAuthHeader = true;
|
|
759
|
-
authToken = this.request.header('
|
|
883
|
+
authToken = this.request.header('x-tilmeldauth') as string;
|
|
760
884
|
} else if ('TILMELDAUTH' in cookies) {
|
|
761
885
|
fromAuthHeader = false;
|
|
762
886
|
authToken = cookies.TILMELDAUTH;
|
|
@@ -775,7 +899,7 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
775
899
|
} else {
|
|
776
900
|
// The request is for something else, so check for a valid XSRF token,
|
|
777
901
|
// unless the auth token is provided by a header (instead of a cookie).
|
|
778
|
-
const xsrfToken = this.request.header('
|
|
902
|
+
const xsrfToken = this.request.header('x-xsrf-token');
|
|
779
903
|
if (xsrfToken == null && !fromAuthHeader) {
|
|
780
904
|
return false;
|
|
781
905
|
}
|
package/src/User.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SQLite3Driver } from '@nymphjs/driver-sqlite3';
|
|
2
|
-
import
|
|
2
|
+
import { Nymph } from '@nymphjs/nymph';
|
|
3
3
|
|
|
4
4
|
import Tilmeld from './Tilmeld';
|
|
5
5
|
import defaults from './conf/defaults';
|
|
@@ -16,7 +16,7 @@ const tilmeld = new Tilmeld({
|
|
|
16
16
|
cancelChangeRedirect: 'http://localhost:8080',
|
|
17
17
|
jwtSecret: 'shhhhh',
|
|
18
18
|
});
|
|
19
|
-
|
|
19
|
+
const _nymph = new Nymph(
|
|
20
20
|
{},
|
|
21
21
|
new SQLite3Driver({
|
|
22
22
|
filename: ':memory:',
|
|
@@ -40,6 +40,14 @@ describe('User', () => {
|
|
|
40
40
|
return newUser;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
it('new user passes validation', async () => {
|
|
44
|
+
const validUser = await makeNewUser();
|
|
45
|
+
|
|
46
|
+
expect(() => {
|
|
47
|
+
defaults.validatorUser(validUser);
|
|
48
|
+
}).not.toThrow();
|
|
49
|
+
});
|
|
50
|
+
|
|
43
51
|
it("doesn't allow unknown keys", async () => {
|
|
44
52
|
const invalidUser = await makeNewUser();
|
|
45
53
|
|
package/src/User.ts
CHANGED
|
@@ -14,9 +14,9 @@ import sha256 from 'crypto-js/sha256';
|
|
|
14
14
|
import md5 from 'crypto-js/md5';
|
|
15
15
|
import { difference } from 'lodash';
|
|
16
16
|
|
|
17
|
+
import type Tilmeld from './Tilmeld';
|
|
17
18
|
import AbleObject from './AbleObject';
|
|
18
19
|
import Group, { GroupData } from './Group';
|
|
19
|
-
import Tilmeld from './Tilmeld';
|
|
20
20
|
import {
|
|
21
21
|
BadDataError,
|
|
22
22
|
BadEmailError,
|
|
@@ -126,11 +126,20 @@ export type UserData = {
|
|
|
126
126
|
passwordTemp?: string;
|
|
127
127
|
};
|
|
128
128
|
|
|
129
|
+
/**
|
|
130
|
+
* A user data model.
|
|
131
|
+
*
|
|
132
|
+
* Written by Hunter Perrin for SciActive.
|
|
133
|
+
*
|
|
134
|
+
* @author Hunter Perrin <hperrin@gmail.com>
|
|
135
|
+
* @copyright SciActive Inc
|
|
136
|
+
* @see http://nymph.io/
|
|
137
|
+
*/
|
|
129
138
|
export default class User extends AbleObject<UserData> {
|
|
130
139
|
/**
|
|
131
140
|
* The instance of Tilmeld to use for queries.
|
|
132
141
|
*/
|
|
133
|
-
|
|
142
|
+
static tilmeld: Tilmeld;
|
|
134
143
|
static ETYPE = 'tilmeld_user';
|
|
135
144
|
static class = 'User';
|
|
136
145
|
|
package/src/conf/d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EmailOptions } from 'email-templates';
|
|
2
|
+
|
|
2
3
|
import type Tilmeld from '../Tilmeld';
|
|
3
4
|
import Group, { GroupData } from '../Group';
|
|
4
5
|
import User, { UserData } from '../User';
|
|
@@ -64,6 +65,35 @@ export interface Config {
|
|
|
64
65
|
* a concern. Same risks as user search if generatePrimary is true.)
|
|
65
66
|
*/
|
|
66
67
|
enableGroupSearch: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* A list of UIDs that can be read from the client by anyone. (getUID)
|
|
70
|
+
*
|
|
71
|
+
* Note: If you'd like to limit the access to logged in users, give them an
|
|
72
|
+
* ability like "uid/get/nameofuid".
|
|
73
|
+
*/
|
|
74
|
+
clientReadableUIDs: string[];
|
|
75
|
+
/**
|
|
76
|
+
* A list of UIDs that can be created from the client by anyone. (newUID)
|
|
77
|
+
*
|
|
78
|
+
* Inclusion in this list implies inclusion in clientReadableUIDs.
|
|
79
|
+
*
|
|
80
|
+
* Note: If you'd like to limit the access to logged in users, give them an
|
|
81
|
+
* ability like "uid/new/nameofuid".
|
|
82
|
+
*/
|
|
83
|
+
clientEnabledUIDs: string[];
|
|
84
|
+
/**
|
|
85
|
+
* A list of UIDs that can be set from the client by anyone. (setUID,
|
|
86
|
+
* renameUID)
|
|
87
|
+
*
|
|
88
|
+
* Inclusion in this list implies inclusion in clientReadableUIDs and
|
|
89
|
+
* clientEnabledUIDs.
|
|
90
|
+
*
|
|
91
|
+
* Note: There is no way to run renameUID from the client.
|
|
92
|
+
*
|
|
93
|
+
* Note: If you'd like to limit the access to logged in users, give them an
|
|
94
|
+
* ability like "uid/set/nameofuid".
|
|
95
|
+
*/
|
|
96
|
+
clientSetabledUIDs: string[];
|
|
67
97
|
/**
|
|
68
98
|
* These will be the available fields for users. (Some fields, like username,
|
|
69
99
|
* can't be excluded.)
|