@nymphjs/tilmeld 1.0.0-alpha.2 → 1.0.0-alpha.23
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 +109 -0
- package/README.md +21 -16
- package/dist/Group.d.ts +1 -1
- package/dist/Group.js +11 -6
- 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 +212 -91
- package/dist/Tilmeld.js.map +1 -1
- package/dist/User.d.ts +1 -1
- package/dist/User.js +20 -7
- 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 -101
- 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 +14 -14
- package/src/Group.test.ts +10 -2
- package/src/Group.ts +22 -8
- package/src/Tilmeld.ts +334 -117
- package/src/User.test.ts +10 -2
- package/src/User.ts +31 -9
- package/src/conf/d.ts +30 -0
- package/src/conf/defaults.ts +12 -60
- package/src/index.ts +3 -1
- package/src/tilmeldJoiProps.ts +46 -0
package/src/Tilmeld.ts
CHANGED
|
@@ -4,8 +4,10 @@ import {
|
|
|
4
4
|
FormattedSelector,
|
|
5
5
|
Options,
|
|
6
6
|
TilmeldInterface,
|
|
7
|
+
TilmeldAccessLevels,
|
|
7
8
|
} from '@nymphjs/nymph';
|
|
8
9
|
import { Request, Response } from 'express';
|
|
10
|
+
import { xor } from 'lodash';
|
|
9
11
|
|
|
10
12
|
import { Config, ConfigDefaults as defaults } from './conf';
|
|
11
13
|
import { AccessControlError } from './errors';
|
|
@@ -23,15 +25,10 @@ import { AccessControlData } from './Tilmeld.types';
|
|
|
23
25
|
* @see http://nymph.io/
|
|
24
26
|
*/
|
|
25
27
|
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
28
|
/**
|
|
32
29
|
* The Nymph instance.
|
|
33
30
|
*/
|
|
34
|
-
public nymph: Nymph;
|
|
31
|
+
public nymph: Nymph = {} as Nymph;
|
|
35
32
|
|
|
36
33
|
/**
|
|
37
34
|
* The Tilmeld config.
|
|
@@ -70,30 +67,32 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
70
67
|
public request: Request | null = null;
|
|
71
68
|
public response: Response | null = null;
|
|
72
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Skip the authenticate step of initialization.
|
|
72
|
+
*/
|
|
73
|
+
private skipAuthenticate = false;
|
|
74
|
+
|
|
73
75
|
/**
|
|
74
76
|
* Create a new instance of Tilmeld.
|
|
75
77
|
*
|
|
76
78
|
* @param config The Tilmeld configuration.
|
|
77
79
|
*/
|
|
78
80
|
public constructor(config: Partial<Config> & { jwtSecret: string }) {
|
|
79
|
-
this.nymph = new Nymph();
|
|
80
81
|
this.config = { ...defaults, ...config };
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
/**
|
|
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.
|
|
85
|
+
* This is used internally by Nymph. Don't call it yourself.
|
|
86
|
+
* @returns A clone of this instance.
|
|
91
87
|
*/
|
|
92
|
-
public
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
88
|
+
public clone() {
|
|
89
|
+
const tilmeld = new Tilmeld(this.config);
|
|
90
|
+
tilmeld.request = this.request;
|
|
91
|
+
tilmeld.response = this.response;
|
|
92
|
+
tilmeld.currentUser = this.currentUser;
|
|
93
|
+
tilmeld.skipAuthenticate = true;
|
|
94
|
+
|
|
95
|
+
return tilmeld;
|
|
97
96
|
}
|
|
98
97
|
|
|
99
98
|
/**
|
|
@@ -115,28 +114,47 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
115
114
|
class NymphUser extends User {}
|
|
116
115
|
NymphUser.nymph = this.nymph;
|
|
117
116
|
NymphUser.tilmeld = this;
|
|
117
|
+
(NymphUser as any).skipOnClone = true;
|
|
118
118
|
this.User = NymphUser;
|
|
119
|
-
this.nymph.
|
|
119
|
+
this.nymph.addEntityClass(NymphUser);
|
|
120
120
|
class NymphGroup extends Group {}
|
|
121
121
|
NymphGroup.nymph = this.nymph;
|
|
122
122
|
NymphGroup.tilmeld = this;
|
|
123
|
+
(NymphUser as any).skipOnClone = true;
|
|
123
124
|
this.Group = NymphGroup;
|
|
124
|
-
this.nymph.
|
|
125
|
+
this.nymph.addEntityClass(NymphGroup);
|
|
125
126
|
|
|
126
127
|
this.initAccessControl();
|
|
127
|
-
if (this.request != null) {
|
|
128
|
+
if (this.request != null && !this.skipAuthenticate) {
|
|
128
129
|
this.authenticate();
|
|
129
130
|
}
|
|
131
|
+
this.skipAuthenticate = false;
|
|
130
132
|
}
|
|
131
133
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Check to see if the current user has an ability.
|
|
136
|
+
*
|
|
137
|
+
* If `ability` is undefined, it will check to see if a user is currently
|
|
138
|
+
* logged in.
|
|
139
|
+
*
|
|
140
|
+
* @param ability The ability.
|
|
141
|
+
* @returns Whether the user has the given ability.
|
|
142
|
+
*/
|
|
143
|
+
public gatekeeper(ability?: string): boolean {
|
|
144
|
+
if (this.currentUser == null) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
return this.currentUser.$gatekeeper(ability);
|
|
148
|
+
}
|
|
134
149
|
|
|
150
|
+
private initAccessControl() {
|
|
135
151
|
// Check for the skip access control option and add AC selectors.
|
|
136
152
|
const handleQuery = function (
|
|
153
|
+
nymph: Nymph,
|
|
137
154
|
options: Options,
|
|
138
155
|
selectors: FormattedSelector[]
|
|
139
156
|
) {
|
|
157
|
+
const tilmeld = nymph.tilmeld as Tilmeld;
|
|
140
158
|
if (
|
|
141
159
|
!options ||
|
|
142
160
|
!('skipAc' in options) ||
|
|
@@ -145,20 +163,20 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
145
163
|
) {
|
|
146
164
|
if (options?.source === 'client') {
|
|
147
165
|
if (
|
|
148
|
-
!tilmeld.gatekeeper('tilmeld/admin') &&
|
|
166
|
+
!tilmeld.gatekeeper('tilmeld/admin') && // The user is not an admin.
|
|
149
167
|
((!tilmeld.config.enableUserSearch &&
|
|
150
|
-
options.class?.factorySync() instanceof User) ||
|
|
168
|
+
options.class?.factorySync() instanceof User) || // The use is searching for a user and searching is disabled.
|
|
151
169
|
(!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])) ||
|
|
170
|
+
options.class?.factorySync() instanceof Group)) && // Or the same, but for a group.
|
|
171
|
+
(selectors[0] == null || // No selector specified.
|
|
172
|
+
selectors[0].type !== '&' || // Or the type is not and.
|
|
173
|
+
(!('guid' in selectors[0]) && !('equal' in selectors[0])) || // Or they're searching for something other than guid or equal.
|
|
156
174
|
('guid' in selectors[0] &&
|
|
157
|
-
selectors[0].guid?.length
|
|
158
|
-
|
|
175
|
+
(!selectors[0].guid?.length ||
|
|
176
|
+
!selectors[0].guid?.[0]?.length)) || // Or they're not searching for a specific guid.
|
|
159
177
|
('equal' in selectors[0] &&
|
|
160
|
-
selectors[0].equal?.
|
|
161
|
-
selectors[0].equal?.[0]?.[0] !== '
|
|
178
|
+
selectors[0].equal?.[0]?.[0] !== 'username' &&
|
|
179
|
+
selectors[0].equal?.[0]?.[0] !== 'groupname')) // Or they're not searching for a specific username/groupname.
|
|
162
180
|
) {
|
|
163
181
|
// If the user is not specifically searching for a GUID or username,
|
|
164
182
|
// and they're not allowed to search, it should fail.
|
|
@@ -169,9 +187,14 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
169
187
|
tilmeld.addAccessControlSelectors(options, selectors);
|
|
170
188
|
}
|
|
171
189
|
};
|
|
190
|
+
handleQuery.skipOnClone = true;
|
|
172
191
|
|
|
173
192
|
// Filter entities being deleted for user permissions.
|
|
174
|
-
const checkPermissionsDelete = function (
|
|
193
|
+
const checkPermissionsDelete = function (
|
|
194
|
+
nymph: Nymph,
|
|
195
|
+
entity: EntityInterface
|
|
196
|
+
) {
|
|
197
|
+
const tilmeld = nymph.tilmeld as Tilmeld;
|
|
175
198
|
if (
|
|
176
199
|
typeof entity.$tilmeldDeleteSkipAC === 'function' &&
|
|
177
200
|
entity.$tilmeldDeleteSkipAC()
|
|
@@ -179,30 +202,35 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
179
202
|
return;
|
|
180
203
|
}
|
|
181
204
|
// Test for permissions.
|
|
182
|
-
if (tilmeld.checkPermissions(entity,
|
|
205
|
+
if (!tilmeld.checkPermissions(entity, TilmeldAccessLevels.FULL_ACCESS)) {
|
|
183
206
|
throw new AccessControlError('No permission to delete.');
|
|
184
207
|
}
|
|
185
208
|
};
|
|
209
|
+
checkPermissionsDelete.skipOnClone = true;
|
|
186
210
|
|
|
187
211
|
// Filter entities being deleted for user permissions.
|
|
188
212
|
const checkPermissionsDeleteByID = function (
|
|
213
|
+
nymph: Nymph,
|
|
189
214
|
guid: string,
|
|
190
215
|
className?: string
|
|
191
216
|
) {
|
|
192
|
-
const entity =
|
|
193
|
-
{ class:
|
|
217
|
+
const entity = nymph.driver.getEntitySync(
|
|
218
|
+
{ class: nymph.getEntityClass(className ?? 'Entity') },
|
|
194
219
|
{ type: '&', guid: guid }
|
|
195
220
|
);
|
|
196
221
|
if (entity != null) {
|
|
197
|
-
checkPermissionsDelete(entity);
|
|
222
|
+
checkPermissionsDelete(nymph, entity);
|
|
198
223
|
}
|
|
199
224
|
};
|
|
225
|
+
checkPermissionsDeleteByID.skipOnClone = true;
|
|
200
226
|
|
|
201
227
|
// Filter entities being saved for user permissions, and filter any
|
|
202
228
|
// disallowed changes to AC properties.
|
|
203
229
|
const checkPermissionsSaveAndFilterAcChanges = function (
|
|
230
|
+
nymph: Nymph,
|
|
204
231
|
entity: EntityInterface & AccessControlData
|
|
205
232
|
) {
|
|
233
|
+
const tilmeld = nymph.tilmeld as Tilmeld;
|
|
206
234
|
if (!entity) {
|
|
207
235
|
return;
|
|
208
236
|
}
|
|
@@ -218,15 +246,15 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
218
246
|
// allowing a change to ac properties.
|
|
219
247
|
|
|
220
248
|
const originalAc = entity.$getOriginalAcValues();
|
|
221
|
-
const newAc = {
|
|
222
|
-
user: entity.user ??
|
|
223
|
-
group: entity.group ??
|
|
224
|
-
acUser: entity.acUser ??
|
|
225
|
-
acGroup: entity.acGroup ??
|
|
226
|
-
acOther: entity.acOther ??
|
|
227
|
-
acRead: entity.acRead ??
|
|
228
|
-
acWrite: entity.acWrite ??
|
|
229
|
-
acFull: entity.acFull ??
|
|
249
|
+
const newAc: AccessControlData = {
|
|
250
|
+
user: entity.user ?? undefined,
|
|
251
|
+
group: entity.group ?? undefined,
|
|
252
|
+
acUser: entity.acUser ?? undefined,
|
|
253
|
+
acGroup: entity.acGroup ?? undefined,
|
|
254
|
+
acOther: entity.acOther ?? undefined,
|
|
255
|
+
acRead: entity.acRead ?? undefined,
|
|
256
|
+
acWrite: entity.acWrite ?? undefined,
|
|
257
|
+
acFull: entity.acFull ?? undefined,
|
|
230
258
|
};
|
|
231
259
|
|
|
232
260
|
const setAcProperties = (acValues: { [k: string]: any }) => {
|
|
@@ -242,19 +270,112 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
242
270
|
|
|
243
271
|
// Restore original AC properties and check permissions.
|
|
244
272
|
setAcProperties(originalAc);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
273
|
+
|
|
274
|
+
function areAcPropertiesChanged(
|
|
275
|
+
a: AccessControlData,
|
|
276
|
+
b: AccessControlData
|
|
277
|
+
) {
|
|
278
|
+
for (const name of ['user', 'group'] as ('user' | 'group')[]) {
|
|
279
|
+
const aVal = a[name];
|
|
280
|
+
const bVal = b[name];
|
|
281
|
+
if (aVal == null && bVal == null) {
|
|
282
|
+
continue;
|
|
283
|
+
} else if (aVal == null || bVal == null) {
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
if (!aVal.$is(bVal)) {
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
for (const name of ['acUser', 'acGroup', 'acOther'] as (
|
|
291
|
+
| 'acUser'
|
|
292
|
+
| 'acGroup'
|
|
293
|
+
| 'acOther'
|
|
294
|
+
)[]) {
|
|
295
|
+
const aVal = a[name];
|
|
296
|
+
const bVal = b[name];
|
|
297
|
+
if (aVal == null && bVal == null) {
|
|
298
|
+
continue;
|
|
299
|
+
} else if (aVal == null || bVal == null) {
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
if (aVal !== bVal) {
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
for (const name of ['acRead', 'acWrite', 'acFull'] as (
|
|
307
|
+
| 'acRead'
|
|
308
|
+
| 'acWrite'
|
|
309
|
+
| 'acFull'
|
|
310
|
+
)[]) {
|
|
311
|
+
const aVal = a[name];
|
|
312
|
+
const bVal = b[name];
|
|
313
|
+
if (aVal == null && bVal == null) {
|
|
314
|
+
continue;
|
|
315
|
+
} else if (aVal == null || bVal == null) {
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
if (aVal.length !== bVal.length) {
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
if (
|
|
322
|
+
xor(
|
|
323
|
+
aVal.map((entity) => entity.guid),
|
|
324
|
+
bVal.map((entity) => entity.guid)
|
|
325
|
+
).length
|
|
326
|
+
) {
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (areAcPropertiesChanged(originalAc, newAc)) {
|
|
334
|
+
if (
|
|
335
|
+
!tilmeld.checkPermissions(entity, TilmeldAccessLevels.FULL_ACCESS)
|
|
336
|
+
) {
|
|
337
|
+
// Only allow changes to AC properties if the user has full access.
|
|
338
|
+
throw new AccessControlError(
|
|
339
|
+
'No permission to change access control properties.'
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (
|
|
344
|
+
((originalAc.user && !originalAc.user.$is(newAc.user)) ||
|
|
345
|
+
(!originalAc.user &&
|
|
346
|
+
newAc.user &&
|
|
347
|
+
!newAc.user.$is(tilmeld.currentUser))) &&
|
|
348
|
+
!tilmeld.gatekeeper('tilmeld/admin')
|
|
349
|
+
) {
|
|
350
|
+
throw new AccessControlError(
|
|
351
|
+
'No permission to assign to another user.'
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (
|
|
356
|
+
newAc.group &&
|
|
357
|
+
!newAc.group.$is(originalAc.group) &&
|
|
358
|
+
!(
|
|
359
|
+
newAc.group.$is(tilmeld.currentUser?.group) ||
|
|
360
|
+
newAc.group.$inArray(tilmeld.currentUser?.groups ?? [])
|
|
361
|
+
) &&
|
|
362
|
+
!tilmeld.gatekeeper('tilmeld/admin')
|
|
363
|
+
) {
|
|
364
|
+
throw new AccessControlError(
|
|
365
|
+
'No permission to assign to another group.'
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
249
369
|
setAcProperties(newAc);
|
|
250
370
|
}
|
|
251
371
|
}
|
|
252
372
|
|
|
253
373
|
// Test for permissions.
|
|
254
|
-
if (!tilmeld.checkPermissions(entity,
|
|
374
|
+
if (!tilmeld.checkPermissions(entity, TilmeldAccessLevels.WRITE_ACCESS)) {
|
|
255
375
|
throw new AccessControlError('No permission to write.');
|
|
256
376
|
}
|
|
257
377
|
};
|
|
378
|
+
checkPermissionsSaveAndFilterAcChanges.skipOnClone = true;
|
|
258
379
|
|
|
259
380
|
/*
|
|
260
381
|
* Add the current user's "user", "group", and access control to new entity.
|
|
@@ -268,11 +389,15 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
268
389
|
*
|
|
269
390
|
* Default access control is
|
|
270
391
|
*
|
|
271
|
-
* - acUser =
|
|
272
|
-
* - acGroup =
|
|
273
|
-
* - acOther =
|
|
392
|
+
* - acUser = TilmeldAccessLevels.FULL_ACCESS
|
|
393
|
+
* - acGroup = TilmeldAccessLevels.READ_ACCESS
|
|
394
|
+
* - acOther = TilmeldAccessLevels.NO_ACCESS
|
|
274
395
|
*/
|
|
275
|
-
const addAccess = function (
|
|
396
|
+
const addAccess = function (
|
|
397
|
+
nymph: Nymph,
|
|
398
|
+
entity: EntityInterface & AccessControlData
|
|
399
|
+
) {
|
|
400
|
+
const tilmeld = nymph.tilmeld as Tilmeld;
|
|
276
401
|
const user = tilmeld.currentUser;
|
|
277
402
|
if (
|
|
278
403
|
user != null &&
|
|
@@ -291,13 +416,13 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
291
416
|
entity.group = user.group;
|
|
292
417
|
}
|
|
293
418
|
if (entity.acUser == null) {
|
|
294
|
-
entity.acUser =
|
|
419
|
+
entity.acUser = TilmeldAccessLevels.FULL_ACCESS;
|
|
295
420
|
}
|
|
296
421
|
if (entity.acGroup == null) {
|
|
297
|
-
entity.acGroup =
|
|
422
|
+
entity.acGroup = TilmeldAccessLevels.READ_ACCESS;
|
|
298
423
|
}
|
|
299
424
|
if (entity.acOther == null) {
|
|
300
|
-
entity.acOther =
|
|
425
|
+
entity.acOther = TilmeldAccessLevels.NO_ACCESS;
|
|
301
426
|
}
|
|
302
427
|
if (entity.acRead == null) {
|
|
303
428
|
entity.acRead = [];
|
|
@@ -310,39 +435,53 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
310
435
|
}
|
|
311
436
|
}
|
|
312
437
|
};
|
|
438
|
+
addAccess.skipOnClone = true;
|
|
313
439
|
|
|
314
|
-
const validate = function (
|
|
440
|
+
const validate = function (
|
|
441
|
+
_nymph: Nymph,
|
|
442
|
+
entity: EntityInterface & AccessControlData
|
|
443
|
+
) {
|
|
315
444
|
if (!(entity instanceof User) && !(entity instanceof Group)) {
|
|
316
445
|
const ownershipAcPropertyValidator = (prop: any) => {
|
|
317
446
|
if (
|
|
318
447
|
typeof prop != 'number' ||
|
|
319
|
-
prop <
|
|
320
|
-
prop >
|
|
448
|
+
prop < TilmeldAccessLevels.NO_ACCESS ||
|
|
449
|
+
prop > TilmeldAccessLevels.FULL_ACCESS
|
|
321
450
|
) {
|
|
322
|
-
throw new AccessControlError(
|
|
451
|
+
throw new AccessControlError(
|
|
452
|
+
'Invalid access control property: ' + prop
|
|
453
|
+
);
|
|
323
454
|
}
|
|
324
455
|
};
|
|
325
456
|
|
|
326
457
|
const accessAcPropertyValidator = (prop: any) => {
|
|
327
458
|
if (!Array.isArray(prop)) {
|
|
328
|
-
throw new AccessControlError(
|
|
459
|
+
throw new AccessControlError(
|
|
460
|
+
'Invalid access control property: ' + prop
|
|
461
|
+
);
|
|
329
462
|
}
|
|
330
463
|
prop.forEach((value) => {
|
|
331
464
|
if (!(value instanceof User || value instanceof Group)) {
|
|
332
|
-
throw new AccessControlError(
|
|
465
|
+
throw new AccessControlError(
|
|
466
|
+
'Invalid access control property: ' + prop
|
|
467
|
+
);
|
|
333
468
|
}
|
|
334
469
|
});
|
|
335
470
|
};
|
|
336
471
|
|
|
337
472
|
if ('user' in entity) {
|
|
338
473
|
if (!(entity.user instanceof User)) {
|
|
339
|
-
throw new AccessControlError(
|
|
474
|
+
throw new AccessControlError(
|
|
475
|
+
'Invalid access control property: user'
|
|
476
|
+
);
|
|
340
477
|
}
|
|
341
478
|
}
|
|
342
479
|
|
|
343
480
|
if ('group' in entity) {
|
|
344
|
-
if (!(entity.
|
|
345
|
-
throw new AccessControlError(
|
|
481
|
+
if (!(entity.group instanceof Group)) {
|
|
482
|
+
throw new AccessControlError(
|
|
483
|
+
'Invalid access control property: group'
|
|
484
|
+
);
|
|
346
485
|
}
|
|
347
486
|
}
|
|
348
487
|
|
|
@@ -366,6 +505,7 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
366
505
|
}
|
|
367
506
|
}
|
|
368
507
|
};
|
|
508
|
+
validate.skipOnClone = true;
|
|
369
509
|
|
|
370
510
|
this.nymph.on('query', handleQuery);
|
|
371
511
|
|
|
@@ -408,16 +548,16 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
408
548
|
return;
|
|
409
549
|
}
|
|
410
550
|
|
|
411
|
-
if (user
|
|
551
|
+
if (user == null || user.guid == null) {
|
|
412
552
|
selectors.push({
|
|
413
553
|
type: '|',
|
|
414
554
|
// Other access control is sufficient.
|
|
415
|
-
gte: [['acOther',
|
|
555
|
+
gte: [['acOther', TilmeldAccessLevels.READ_ACCESS]],
|
|
416
556
|
// The user and group are not set.
|
|
417
557
|
selector: [
|
|
418
558
|
{
|
|
419
559
|
type: '&',
|
|
420
|
-
'!defined': ['user', 'group'],
|
|
560
|
+
'!defined': [['user', 'group']],
|
|
421
561
|
},
|
|
422
562
|
],
|
|
423
563
|
});
|
|
@@ -425,63 +565,50 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
425
565
|
const subSelectors: FormattedSelector[] = [
|
|
426
566
|
{
|
|
427
567
|
type: '&',
|
|
428
|
-
'!defined': ['user', 'group'],
|
|
568
|
+
'!defined': [['user', 'group']],
|
|
429
569
|
},
|
|
430
570
|
// It is owned by the user.
|
|
431
571
|
{
|
|
432
572
|
type: '&',
|
|
433
|
-
ref: [['user', user]],
|
|
434
|
-
gte: [['acUser',
|
|
573
|
+
ref: [['user', user.guid]],
|
|
574
|
+
gte: [['acUser', TilmeldAccessLevels.READ_ACCESS]],
|
|
435
575
|
},
|
|
436
576
|
];
|
|
437
|
-
const selector: FormattedSelector = {
|
|
438
|
-
type: '|',
|
|
439
|
-
// Other access control is sufficient.
|
|
440
|
-
gte: [['acOther', Tilmeld.READ_ACCESS]],
|
|
441
|
-
// The user and group are not set.
|
|
442
|
-
selector: subSelectors,
|
|
443
|
-
// The user is listed in acRead, acWrite, or acFull.
|
|
444
|
-
ref: [
|
|
445
|
-
['acRead', user],
|
|
446
|
-
['acWrite', user],
|
|
447
|
-
['acFull', user],
|
|
448
|
-
],
|
|
449
|
-
};
|
|
450
577
|
const groupRefs: FormattedSelector['ref'] = [];
|
|
451
578
|
const acRefs: FormattedSelector['ref'] = [];
|
|
452
579
|
if (user.group != null && user.group.guid != null) {
|
|
453
580
|
// It belongs to the user's primary group.
|
|
454
|
-
groupRefs.push(['group', user.group]);
|
|
581
|
+
groupRefs.push(['group', user.group.guid]);
|
|
455
582
|
// User's primary group is listed in acRead, acWrite, or acFull.
|
|
456
|
-
acRefs.push(['acRead', user.group]);
|
|
457
|
-
acRefs.push(['acWrite', user.group]);
|
|
458
|
-
acRefs.push(['acFull', user.group]);
|
|
583
|
+
acRefs.push(['acRead', user.group.guid]);
|
|
584
|
+
acRefs.push(['acWrite', user.group.guid]);
|
|
585
|
+
acRefs.push(['acFull', user.group.guid]);
|
|
459
586
|
}
|
|
460
587
|
for (let curSecondaryGroup of user.groups ?? []) {
|
|
461
588
|
if (curSecondaryGroup != null && curSecondaryGroup.guid != null) {
|
|
462
589
|
// It belongs to the user's secondary group.
|
|
463
|
-
groupRefs.push(['group', curSecondaryGroup]);
|
|
590
|
+
groupRefs.push(['group', curSecondaryGroup.guid]);
|
|
464
591
|
// User's secondary group is listed in acRead, acWrite, or acFull.
|
|
465
|
-
acRefs.push(['acRead', curSecondaryGroup]);
|
|
466
|
-
acRefs.push(['acWrite', curSecondaryGroup]);
|
|
467
|
-
acRefs.push(['acFull', curSecondaryGroup]);
|
|
592
|
+
acRefs.push(['acRead', curSecondaryGroup.guid]);
|
|
593
|
+
acRefs.push(['acWrite', curSecondaryGroup.guid]);
|
|
594
|
+
acRefs.push(['acFull', curSecondaryGroup.guid]);
|
|
468
595
|
}
|
|
469
596
|
}
|
|
470
597
|
for (let curDescendantGroup of user.$getDescendantGroupsSync()) {
|
|
471
598
|
if (curDescendantGroup != null && curDescendantGroup.guid != null) {
|
|
472
599
|
// It belongs to the user's secondary group.
|
|
473
|
-
groupRefs.push(['group', curDescendantGroup]);
|
|
600
|
+
groupRefs.push(['group', curDescendantGroup.guid]);
|
|
474
601
|
// User's secondary group is listed in acRead, acWrite, or acFull.
|
|
475
|
-
acRefs.push(['acRead', curDescendantGroup]);
|
|
476
|
-
acRefs.push(['acWrite', curDescendantGroup]);
|
|
477
|
-
acRefs.push(['acFull', curDescendantGroup]);
|
|
602
|
+
acRefs.push(['acRead', curDescendantGroup.guid]);
|
|
603
|
+
acRefs.push(['acWrite', curDescendantGroup.guid]);
|
|
604
|
+
acRefs.push(['acFull', curDescendantGroup.guid]);
|
|
478
605
|
}
|
|
479
606
|
}
|
|
480
607
|
// All the group refs.
|
|
481
608
|
if (groupRefs.length) {
|
|
482
609
|
subSelectors.push({
|
|
483
610
|
type: '&',
|
|
484
|
-
gte: [['acGroup',
|
|
611
|
+
gte: [['acGroup', TilmeldAccessLevels.READ_ACCESS]],
|
|
485
612
|
selector: [
|
|
486
613
|
{
|
|
487
614
|
type: '|',
|
|
@@ -497,6 +624,19 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
497
624
|
ref: acRefs,
|
|
498
625
|
});
|
|
499
626
|
}
|
|
627
|
+
const selector: FormattedSelector = {
|
|
628
|
+
type: '|',
|
|
629
|
+
// Other access control is sufficient.
|
|
630
|
+
gte: [['acOther', TilmeldAccessLevels.READ_ACCESS]],
|
|
631
|
+
// The user and group are not set.
|
|
632
|
+
selector: subSelectors,
|
|
633
|
+
// The user is listed in acRead, acWrite, or acFull.
|
|
634
|
+
ref: [
|
|
635
|
+
['acRead', user.guid],
|
|
636
|
+
['acWrite', user.guid],
|
|
637
|
+
['acFull', user.guid],
|
|
638
|
+
],
|
|
639
|
+
};
|
|
500
640
|
selectors.push(selector);
|
|
501
641
|
}
|
|
502
642
|
}
|
|
@@ -530,9 +670,9 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
530
670
|
*
|
|
531
671
|
* These properties defaults to:
|
|
532
672
|
*
|
|
533
|
-
* - acUser =
|
|
534
|
-
* - acGroup =
|
|
535
|
-
* - acOther =
|
|
673
|
+
* - acUser = TilmeldAccessLevels.FULL_ACCESS
|
|
674
|
+
* - acGroup = TilmeldAccessLevels.READ_ACCESS
|
|
675
|
+
* - acOther = TilmeldAccessLevels.NO_ACCESS
|
|
536
676
|
*
|
|
537
677
|
* "acRead", "acWrite", and "acFull" are arrays of users and/or groups that
|
|
538
678
|
* also have those permissions.
|
|
@@ -561,13 +701,13 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
561
701
|
* - None of the above. (Check other AC.)
|
|
562
702
|
*
|
|
563
703
|
* @param entity The entity to check.
|
|
564
|
-
* @param type The lowest level of permission to consider a pass.
|
|
704
|
+
* @param type The lowest level of permission to consider a pass.
|
|
565
705
|
* @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
|
|
566
706
|
* @returns Whether the current user has at least `type` permission for the entity.
|
|
567
707
|
*/
|
|
568
708
|
public checkPermissions(
|
|
569
709
|
entity: EntityInterface & AccessControlData,
|
|
570
|
-
type =
|
|
710
|
+
type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
|
|
571
711
|
user?: (User & UserData) | false
|
|
572
712
|
) {
|
|
573
713
|
// Only works for entities.
|
|
@@ -593,7 +733,8 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
593
733
|
// Users and groups are always readable. Editable by Tilmeld admins.
|
|
594
734
|
if (
|
|
595
735
|
(entity instanceof User || entity instanceof Group) &&
|
|
596
|
-
(type ===
|
|
736
|
+
(type === TilmeldAccessLevels.READ_ACCESS ||
|
|
737
|
+
userOrEmpty.$gatekeeper('tilmeld/admin'))
|
|
597
738
|
) {
|
|
598
739
|
return true;
|
|
599
740
|
}
|
|
@@ -604,9 +745,9 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
604
745
|
}
|
|
605
746
|
|
|
606
747
|
// Load access control, since we need it now...
|
|
607
|
-
const acUser = entity.acUser ??
|
|
608
|
-
const acGroup = entity.acGroup ??
|
|
609
|
-
const acOther = entity.acOther ??
|
|
748
|
+
const acUser = entity.acUser ?? TilmeldAccessLevels.FULL_ACCESS;
|
|
749
|
+
const acGroup = entity.acGroup ?? TilmeldAccessLevels.READ_ACCESS;
|
|
750
|
+
const acOther = entity.acOther ?? TilmeldAccessLevels.NO_ACCESS;
|
|
610
751
|
|
|
611
752
|
if (userOrNull === null) {
|
|
612
753
|
return acOther >= type;
|
|
@@ -622,7 +763,7 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
622
763
|
userOrEmpty.group != null &&
|
|
623
764
|
typeof userOrEmpty.group.$is === 'function' &&
|
|
624
765
|
userOrEmpty.group.$is(entity) &&
|
|
625
|
-
type ===
|
|
766
|
+
type === TilmeldAccessLevels.READ_ACCESS
|
|
626
767
|
) {
|
|
627
768
|
return true;
|
|
628
769
|
}
|
|
@@ -634,9 +775,18 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
634
775
|
|
|
635
776
|
// Check access ac properties.
|
|
636
777
|
const checks = [
|
|
637
|
-
{
|
|
638
|
-
|
|
639
|
-
|
|
778
|
+
{
|
|
779
|
+
type: TilmeldAccessLevels.FULL_ACCESS,
|
|
780
|
+
array: [...(entity.acFull ?? [])],
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
type: TilmeldAccessLevels.WRITE_ACCESS,
|
|
784
|
+
array: [...(entity.acWrite ?? [])],
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
type: TilmeldAccessLevels.READ_ACCESS,
|
|
788
|
+
array: [...(entity.acRead ?? [])],
|
|
789
|
+
},
|
|
640
790
|
];
|
|
641
791
|
for (let curCheck of checks) {
|
|
642
792
|
if (type <= curCheck.type) {
|
|
@@ -672,6 +822,73 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
672
822
|
return acOther >= type;
|
|
673
823
|
}
|
|
674
824
|
|
|
825
|
+
/**
|
|
826
|
+
* Check a UID's permissions for a user.
|
|
827
|
+
*
|
|
828
|
+
* ## THIS ONLY CHECKS AUTOMATICALLY FOR CLIENT REQUESTS.
|
|
829
|
+
*
|
|
830
|
+
* UID functions on the Node.js side are not checked automatically. This
|
|
831
|
+
* function is only run automatically for UID functions run from the client.
|
|
832
|
+
* You should call this function manually if you're running a UID function on
|
|
833
|
+
* the Node.js side and you want it gated.
|
|
834
|
+
*
|
|
835
|
+
* This will check the Tilmeld config and the user's abilities.
|
|
836
|
+
*
|
|
837
|
+
* - READ_ACCESS, the UID is listed in clientReadableUIDs or the user has the
|
|
838
|
+
* "uid/get/nameofuid" ability.
|
|
839
|
+
* - WRITE_ACCESS, the UID is listed in clientEnabledUIDs or the user has the
|
|
840
|
+
* "uid/new/nameofuid" ability.
|
|
841
|
+
* - FULL_ACCESS, the UID is listed in clientSetabledUIDs or the user has the
|
|
842
|
+
* "uid/set/nameofuid" ability.
|
|
843
|
+
*
|
|
844
|
+
* @param name The UID to check.
|
|
845
|
+
* @param type The lowest level of permission to consider a pass.
|
|
846
|
+
* @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
|
|
847
|
+
* @returns Whether the current user has at least `type` permission for the UID.
|
|
848
|
+
*/
|
|
849
|
+
public checkClientUIDPermissions(
|
|
850
|
+
name: string,
|
|
851
|
+
type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
|
|
852
|
+
user?: (User & UserData) | false
|
|
853
|
+
) {
|
|
854
|
+
let userOrEmpty: User & UserData = this.User.factorySync();
|
|
855
|
+
// Calculate the user.
|
|
856
|
+
if (user == null) {
|
|
857
|
+
userOrEmpty = this.User.current(true);
|
|
858
|
+
} else if (user !== false) {
|
|
859
|
+
userOrEmpty = user;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
if (userOrEmpty.$gatekeeper('system/admin')) {
|
|
863
|
+
return true;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (type === TilmeldAccessLevels.FULL_ACCESS) {
|
|
867
|
+
return (
|
|
868
|
+
this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
|
|
869
|
+
userOrEmpty.$gatekeeper(`uid/set/${name}`)
|
|
870
|
+
);
|
|
871
|
+
} else if (type === TilmeldAccessLevels.WRITE_ACCESS) {
|
|
872
|
+
return (
|
|
873
|
+
this.config.clientEnabledUIDs.indexOf(name) !== -1 ||
|
|
874
|
+
this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
|
|
875
|
+
userOrEmpty.$gatekeeper(`uid/new/${name}`) ||
|
|
876
|
+
userOrEmpty.$gatekeeper(`uid/set/${name}`)
|
|
877
|
+
);
|
|
878
|
+
} else if (type === TilmeldAccessLevels.READ_ACCESS) {
|
|
879
|
+
return (
|
|
880
|
+
this.config.clientReadableUIDs.indexOf(name) !== -1 ||
|
|
881
|
+
this.config.clientEnabledUIDs.indexOf(name) !== -1 ||
|
|
882
|
+
this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
|
|
883
|
+
userOrEmpty.$gatekeeper(`uid/get/${name}`) ||
|
|
884
|
+
userOrEmpty.$gatekeeper(`uid/new/${name}`) ||
|
|
885
|
+
userOrEmpty.$gatekeeper(`uid/set/${name}`)
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
return false;
|
|
890
|
+
}
|
|
891
|
+
|
|
675
892
|
/**
|
|
676
893
|
* Fill session user data.
|
|
677
894
|
*
|
|
@@ -754,9 +971,9 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
754
971
|
// to provide the auth token.
|
|
755
972
|
let fromAuthHeader = false;
|
|
756
973
|
let authToken: string;
|
|
757
|
-
if (this.request.header('
|
|
974
|
+
if (this.request.header('x-tilmeldauth') != null) {
|
|
758
975
|
fromAuthHeader = true;
|
|
759
|
-
authToken = this.request.header('
|
|
976
|
+
authToken = this.request.header('x-tilmeldauth') as string;
|
|
760
977
|
} else if ('TILMELDAUTH' in cookies) {
|
|
761
978
|
fromAuthHeader = false;
|
|
762
979
|
authToken = cookies.TILMELDAUTH;
|
|
@@ -775,7 +992,7 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
775
992
|
} else {
|
|
776
993
|
// The request is for something else, so check for a valid XSRF token,
|
|
777
994
|
// unless the auth token is provided by a header (instead of a cookie).
|
|
778
|
-
const xsrfToken = this.request.header('
|
|
995
|
+
const xsrfToken = this.request.header('x-xsrf-token');
|
|
779
996
|
if (xsrfToken == null && !fromAuthHeader) {
|
|
780
997
|
return false;
|
|
781
998
|
}
|