@nymphjs/tilmeld 1.0.0-beta.12 → 1.0.0-beta.121
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 +550 -0
- package/README.md +15 -2
- package/dist/AbleObject.d.ts +15 -0
- package/dist/AbleObject.js +18 -6
- package/dist/AbleObject.js.map +1 -1
- package/dist/Group.d.ts +182 -15
- package/dist/Group.js +571 -191
- package/dist/Group.js.map +1 -1
- package/dist/Group.testArtifacts.d.ts +3 -0
- package/dist/Group.testArtifacts.js +120 -0
- package/dist/Group.testArtifacts.js.map +1 -0
- package/dist/Tilmeld.d.ts +241 -12
- package/dist/Tilmeld.js +721 -273
- package/dist/Tilmeld.js.map +1 -1
- package/dist/Tilmeld.types.d.ts +5 -5
- package/dist/Tilmeld.types.js +1 -2
- package/dist/User.d.ts +396 -22
- package/dist/User.js +1386 -367
- package/dist/User.js.map +1 -1
- package/dist/User.testArtifacts.d.ts +3 -0
- package/dist/User.testArtifacts.js +175 -0
- package/dist/User.testArtifacts.js.map +1 -0
- package/dist/conf/defaults.d.ts +1 -1
- package/dist/conf/defaults.js +196 -141
- package/dist/conf/defaults.js.map +1 -1
- package/dist/conf/index.d.ts +355 -2
- package/dist/conf/index.js +2 -8
- package/dist/conf/index.js.map +1 -1
- package/dist/enforceTilmeld.d.ts +3 -0
- package/dist/enforceTilmeld.js +18 -0
- package/dist/enforceTilmeld.js.map +1 -0
- package/dist/errors/AccessControlError.d.ts +2 -1
- package/dist/errors/AccessControlError.js +3 -6
- package/dist/errors/AccessControlError.js.map +1 -1
- package/dist/errors/BadDataError.d.ts +2 -1
- package/dist/errors/BadDataError.js +3 -6
- package/dist/errors/BadDataError.js.map +1 -1
- package/dist/errors/BadEmailError.d.ts +2 -1
- package/dist/errors/BadEmailError.js +3 -6
- package/dist/errors/BadEmailError.js.map +1 -1
- package/dist/errors/BadUsernameError.d.ts +2 -1
- package/dist/errors/BadUsernameError.js +3 -6
- package/dist/errors/BadUsernameError.js.map +1 -1
- package/dist/errors/CouldNotChangeDefaultPrimaryGroupError.js +1 -5
- package/dist/errors/CouldNotChangeDefaultPrimaryGroupError.js.map +1 -1
- package/dist/errors/EmailChangeRateLimitExceededError.js +1 -5
- package/dist/errors/EmailChangeRateLimitExceededError.js.map +1 -1
- package/dist/errors/index.d.ts +6 -6
- package/dist/errors/index.js +6 -22
- package/dist/errors/index.js.map +1 -1
- package/dist/index.d.ts +11 -10
- package/dist/index.js +16 -35
- package/dist/index.js.map +1 -1
- package/dist/testArtifacts.d.ts +19 -0
- package/dist/testArtifacts.js +1173 -0
- package/dist/testArtifacts.js.map +1 -0
- package/dist/tilmeldJoiProps.d.ts +3 -3
- package/dist/tilmeldJoiProps.js +13 -18
- package/dist/tilmeldJoiProps.js.map +1 -1
- package/jest.config.js +31 -5
- package/package.json +41 -34
- package/src/AbleObject.ts +2 -2
- package/src/Group.testArtifacts.ts +159 -0
- package/src/Group.ts +684 -225
- package/src/Tilmeld.ts +547 -283
- package/src/Tilmeld.types.ts +5 -5
- package/src/User.testArtifacts.ts +226 -0
- package/src/User.ts +1635 -461
- package/src/conf/defaults.ts +172 -109
- package/src/conf/index.ts +370 -2
- package/src/enforceTilmeld.ts +20 -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/src/errors/index.ts +6 -6
- package/src/index.ts +12 -10
- package/src/testArtifacts.ts +1979 -0
- package/src/tilmeldJoiProps.ts +8 -23
- package/tsconfig.json +6 -3
- package/typedoc.json +4 -0
- package/dist/Group.test.d.ts +0 -1
- package/dist/Group.test.js +0 -48
- package/dist/Group.test.js.map +0 -1
- package/dist/User.test.d.ts +0 -1
- package/dist/User.test.js +0 -51
- package/dist/User.test.js.map +0 -1
- package/dist/conf/d.d.ts +0 -55
- package/dist/conf/d.js +0 -3
- package/dist/conf/d.js.map +0 -1
- package/src/Group.test.ts +0 -58
- package/src/User.test.ts +0 -61
- package/src/conf/d.ts +0 -303
package/src/Tilmeld.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { URL } from 'node:url';
|
|
1
2
|
import {
|
|
2
3
|
Nymph,
|
|
3
4
|
EntityInterface,
|
|
@@ -5,23 +6,26 @@ import {
|
|
|
5
6
|
Options,
|
|
6
7
|
TilmeldInterface,
|
|
7
8
|
TilmeldAccessLevels,
|
|
9
|
+
TilmeldAccessRequest,
|
|
10
|
+
ACProperties,
|
|
8
11
|
} from '@nymphjs/nymph';
|
|
9
|
-
import { URL } from 'url';
|
|
10
12
|
import { Request, Response } from 'express';
|
|
11
|
-
import { xor } from 'lodash';
|
|
13
|
+
import { xor } from 'lodash-es';
|
|
14
|
+
import { splitn } from '@sciactive/splitn';
|
|
12
15
|
|
|
13
|
-
import { Config, ConfigDefaults as defaults } from './conf';
|
|
14
|
-
import { AccessControlError } from './errors';
|
|
15
|
-
import Group from './Group';
|
|
16
|
-
import User, { UserData } from './User';
|
|
17
|
-
import { AccessControlData } from './Tilmeld.types';
|
|
16
|
+
import { Config, ConfigDefaults as defaults } from './conf/index.js';
|
|
17
|
+
import { AccessControlError } from './errors/index.js';
|
|
18
|
+
import Group, { GroupData } from './Group.js';
|
|
19
|
+
import User, { UserData } from './User.js';
|
|
20
|
+
import { AccessControlData } from './Tilmeld.types.js';
|
|
21
|
+
import { enforceTilmeld } from './enforceTilmeld.js';
|
|
18
22
|
|
|
19
23
|
/**
|
|
20
24
|
* A user and group system for Nymph.js.
|
|
21
25
|
*
|
|
22
26
|
* Written by Hunter Perrin for SciActive.
|
|
23
27
|
*
|
|
24
|
-
* @author Hunter Perrin <hperrin@
|
|
28
|
+
* @author Hunter Perrin <hperrin@port87.com>
|
|
25
29
|
* @copyright SciActive Inc
|
|
26
30
|
* @see http://nymph.io/
|
|
27
31
|
*/
|
|
@@ -69,9 +73,17 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
69
73
|
public response: Response | null = null;
|
|
70
74
|
|
|
71
75
|
/**
|
|
72
|
-
*
|
|
76
|
+
* Used to avoid infinite loop.
|
|
73
77
|
*/
|
|
74
|
-
private
|
|
78
|
+
private alreadyLoggedOutSwitch = false;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Gatekeeper ability cache.
|
|
82
|
+
*
|
|
83
|
+
* Gatekeeper will cache the user's abilities that it calculates, so it can
|
|
84
|
+
* check faster if that user has been checked before.
|
|
85
|
+
*/
|
|
86
|
+
private gatekeeperCache: { [k: string]: true } | null = null;
|
|
75
87
|
|
|
76
88
|
/**
|
|
77
89
|
* Create a new instance of Tilmeld.
|
|
@@ -80,6 +92,30 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
80
92
|
*/
|
|
81
93
|
public constructor(config: Partial<Config> & { jwtSecret: string }) {
|
|
82
94
|
this.config = { ...defaults, ...config };
|
|
95
|
+
|
|
96
|
+
if (
|
|
97
|
+
this.config.emailUsernames &&
|
|
98
|
+
(!this.config.userFields.includes('email') ||
|
|
99
|
+
!this.config.regFields.includes('email'))
|
|
100
|
+
) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
'You must include email in userFields and regFields if you use emailUsernames.',
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (let field of this.config.regFields) {
|
|
107
|
+
if (!this.config.userFields.includes(field)) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`All fields in regFields must also be in userFields: ${field}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (this.config.emailUsernames && this.config.domainSupport) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
'Email usernames and domain support cannot both be enabled.',
|
|
117
|
+
);
|
|
118
|
+
}
|
|
83
119
|
}
|
|
84
120
|
|
|
85
121
|
/**
|
|
@@ -92,7 +128,7 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
92
128
|
tilmeld.request = this.request;
|
|
93
129
|
tilmeld.response = this.response;
|
|
94
130
|
tilmeld.currentUser = this.currentUser;
|
|
95
|
-
tilmeld.
|
|
131
|
+
tilmeld.gatekeeperCache = this.gatekeeperCache;
|
|
96
132
|
|
|
97
133
|
return tilmeld;
|
|
98
134
|
}
|
|
@@ -106,32 +142,47 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
106
142
|
*/
|
|
107
143
|
public init(nymph: Nymph) {
|
|
108
144
|
this.nymph = nymph;
|
|
109
|
-
const tilmeld = this;
|
|
110
145
|
|
|
111
|
-
// Set up access control hooks when Nymph is called.
|
|
112
146
|
if (this.nymph.driver == null) {
|
|
113
147
|
throw new Error("Tilmeld can't be configured before Nymph.");
|
|
114
148
|
}
|
|
115
149
|
|
|
116
150
|
// Configure the classes.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
151
|
+
if (nymph.parent) {
|
|
152
|
+
// This is a cloned Nymph, it should have classes already.
|
|
153
|
+
this.User = this.nymph.getEntityClass(User);
|
|
154
|
+
this.Group = this.nymph.getEntityClass(Group);
|
|
155
|
+
} else {
|
|
156
|
+
// This is a new Nymph, add the classes.
|
|
157
|
+
this.User = this.nymph.addEntityClass(User);
|
|
158
|
+
this.Group = this.nymph.addEntityClass(Group);
|
|
120
159
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
160
|
+
|
|
161
|
+
// Clone the current user so its Nymph instance is correct.
|
|
162
|
+
if (this.currentUser) {
|
|
163
|
+
const currentUserGUID = this.currentUser.guid;
|
|
164
|
+
const currentUserCDate = this.currentUser.cdate;
|
|
165
|
+
const currentUserMDate = this.currentUser.mdate;
|
|
166
|
+
const currentUserTags = this.currentUser.tags;
|
|
167
|
+
const currentUserData = this.currentUser.$getData();
|
|
168
|
+
const currentUserSData = this.currentUser.$getSData();
|
|
169
|
+
|
|
170
|
+
const user = this.User.factorySync();
|
|
171
|
+
user.guid = currentUserGUID;
|
|
172
|
+
user.cdate = currentUserCDate;
|
|
173
|
+
user.mdate = currentUserMDate;
|
|
174
|
+
user.tags = currentUserTags;
|
|
175
|
+
user.$putData(currentUserData, currentUserSData);
|
|
176
|
+
user.$originalEmail = this.currentUser.$originalEmail;
|
|
177
|
+
user.$originalUsername = this.currentUser.$originalUsername;
|
|
178
|
+
this.currentUser = user;
|
|
179
|
+
this.currentUser.$updateDataProtection();
|
|
180
|
+
this.currentUser.$updateGroupDataProtection();
|
|
126
181
|
}
|
|
127
|
-
(NymphUser as any).skipOnClone = true;
|
|
128
|
-
this.Group = this.nymph.addEntityClass(NymphGroup);
|
|
129
182
|
|
|
183
|
+
// Set up access control hooks when Nymph is called.
|
|
184
|
+
this.nymph.config.debugInfo('tilmeld', 'Initializing access controls.');
|
|
130
185
|
this.initAccessControl();
|
|
131
|
-
if (this.request != null && !this.skipAuthenticate) {
|
|
132
|
-
this.authenticate();
|
|
133
|
-
}
|
|
134
|
-
this.skipAuthenticate = false;
|
|
135
186
|
}
|
|
136
187
|
|
|
137
188
|
/**
|
|
@@ -143,11 +194,17 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
143
194
|
* @param ability The ability.
|
|
144
195
|
* @returns Whether the user has the given ability.
|
|
145
196
|
*/
|
|
146
|
-
public gatekeeper(ability?: string)
|
|
147
|
-
if (this.currentUser == null) {
|
|
197
|
+
public gatekeeper(ability?: string) {
|
|
198
|
+
if (this.currentUser == null || this.gatekeeperCache == null) {
|
|
148
199
|
return false;
|
|
149
200
|
}
|
|
150
|
-
|
|
201
|
+
if (ability == null) {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
return (
|
|
205
|
+
(ability in this.gatekeeperCache && !!this.gatekeeperCache[ability]) ||
|
|
206
|
+
!!this.currentUser.abilities?.includes('system/admin')
|
|
207
|
+
);
|
|
151
208
|
}
|
|
152
209
|
|
|
153
210
|
private initAccessControl() {
|
|
@@ -155,9 +212,9 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
155
212
|
const handleQuery = function (
|
|
156
213
|
nymph: Nymph,
|
|
157
214
|
options: Options,
|
|
158
|
-
selectors: FormattedSelector[]
|
|
215
|
+
selectors: FormattedSelector[],
|
|
159
216
|
) {
|
|
160
|
-
const tilmeld = nymph
|
|
217
|
+
const tilmeld = enforceTilmeld(nymph);
|
|
161
218
|
if (
|
|
162
219
|
!options ||
|
|
163
220
|
!('skipAc' in options) ||
|
|
@@ -166,7 +223,9 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
166
223
|
) {
|
|
167
224
|
if (options?.source === 'client') {
|
|
168
225
|
if (
|
|
169
|
-
!tilmeld.
|
|
226
|
+
(!tilmeld.currentUser ||
|
|
227
|
+
(!tilmeld.currentUser.abilities?.includes('tilmeld/admin') &&
|
|
228
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))) && // The user is not an admin.
|
|
170
229
|
((!tilmeld.config.enableUserSearch &&
|
|
171
230
|
options.class?.factorySync() instanceof User) || // The use is searching for a user and searching is disabled.
|
|
172
231
|
(!tilmeld.config.enableGroupSearch &&
|
|
@@ -195,9 +254,9 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
195
254
|
// Filter entities being deleted for user permissions.
|
|
196
255
|
const checkPermissionsDelete = async function (
|
|
197
256
|
nymph: Nymph,
|
|
198
|
-
entity: EntityInterface
|
|
257
|
+
entity: EntityInterface,
|
|
199
258
|
) {
|
|
200
|
-
const tilmeld = nymph
|
|
259
|
+
const tilmeld = enforceTilmeld(nymph);
|
|
201
260
|
if (
|
|
202
261
|
typeof entity.$tilmeldDeleteSkipAC === 'function' &&
|
|
203
262
|
entity.$tilmeldDeleteSkipAC()
|
|
@@ -215,11 +274,14 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
215
274
|
const checkPermissionsDeleteByID = async function (
|
|
216
275
|
nymph: Nymph,
|
|
217
276
|
guid: string,
|
|
218
|
-
className?: string
|
|
277
|
+
className?: string,
|
|
219
278
|
) {
|
|
220
|
-
|
|
279
|
+
if (className === 'User' || className === 'Group') {
|
|
280
|
+
throw new AccessControlError("You can't delete users or groups by ID.");
|
|
281
|
+
}
|
|
282
|
+
const entity = await nymph.getEntity(
|
|
221
283
|
{ class: nymph.getEntityClass(className ?? 'Entity') },
|
|
222
|
-
{ type: '&', guid: guid }
|
|
284
|
+
{ type: '&', guid: guid },
|
|
223
285
|
);
|
|
224
286
|
if (entity != null) {
|
|
225
287
|
checkPermissionsDelete(nymph, entity);
|
|
@@ -231,9 +293,9 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
231
293
|
// disallowed changes to AC properties.
|
|
232
294
|
const checkPermissionsSaveAndFilterAcChanges = async function (
|
|
233
295
|
nymph: Nymph,
|
|
234
|
-
entity: EntityInterface & AccessControlData
|
|
296
|
+
entity: EntityInterface & AccessControlData,
|
|
235
297
|
) {
|
|
236
|
-
const tilmeld = nymph
|
|
298
|
+
const tilmeld = enforceTilmeld(nymph);
|
|
237
299
|
if (!entity) {
|
|
238
300
|
return;
|
|
239
301
|
}
|
|
@@ -249,35 +311,9 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
249
311
|
// allowing a change to ac properties.
|
|
250
312
|
|
|
251
313
|
const originalAc = entity.$getOriginalAcValues();
|
|
252
|
-
const newAc
|
|
253
|
-
user: entity.user ?? undefined,
|
|
254
|
-
group: entity.group ?? undefined,
|
|
255
|
-
acUser: entity.acUser ?? undefined,
|
|
256
|
-
acGroup: entity.acGroup ?? undefined,
|
|
257
|
-
acOther: entity.acOther ?? undefined,
|
|
258
|
-
acRead: entity.acRead ?? undefined,
|
|
259
|
-
acWrite: entity.acWrite ?? undefined,
|
|
260
|
-
acFull: entity.acFull ?? undefined,
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
const setAcProperties = (acValues: { [k: string]: any }) => {
|
|
264
|
-
for (let name in acValues) {
|
|
265
|
-
const value = acValues[name];
|
|
266
|
-
if (value == null) {
|
|
267
|
-
delete entity[name];
|
|
268
|
-
} else {
|
|
269
|
-
entity[name] = value;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
};
|
|
314
|
+
const newAc = entity.$getCurrentAcValues();
|
|
273
315
|
|
|
274
|
-
|
|
275
|
-
setAcProperties(originalAc);
|
|
276
|
-
|
|
277
|
-
function areAcPropertiesChanged(
|
|
278
|
-
a: AccessControlData,
|
|
279
|
-
b: AccessControlData
|
|
280
|
-
) {
|
|
316
|
+
function areAcPropertiesChanged(a: ACProperties, b: ACProperties) {
|
|
281
317
|
for (const name of ['user', 'group'] as ('user' | 'group')[]) {
|
|
282
318
|
const aVal = a[name];
|
|
283
319
|
const bVal = b[name];
|
|
@@ -286,14 +322,12 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
286
322
|
} else if (aVal == null || bVal == null) {
|
|
287
323
|
return true;
|
|
288
324
|
}
|
|
289
|
-
if (
|
|
325
|
+
if (aVal !== bVal) {
|
|
290
326
|
return true;
|
|
291
327
|
}
|
|
292
328
|
}
|
|
293
329
|
for (const name of ['acUser', 'acGroup', 'acOther'] as (
|
|
294
|
-
| '
|
|
295
|
-
| 'acGroup'
|
|
296
|
-
| 'acOther'
|
|
330
|
+
'acUser' | 'acGroup' | 'acOther'
|
|
297
331
|
)[]) {
|
|
298
332
|
const aVal = a[name];
|
|
299
333
|
const bVal = b[name];
|
|
@@ -307,9 +341,7 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
307
341
|
}
|
|
308
342
|
}
|
|
309
343
|
for (const name of ['acRead', 'acWrite', 'acFull'] as (
|
|
310
|
-
| '
|
|
311
|
-
| 'acWrite'
|
|
312
|
-
| 'acFull'
|
|
344
|
+
'acRead' | 'acWrite' | 'acFull'
|
|
313
345
|
)[]) {
|
|
314
346
|
const aVal = a[name];
|
|
315
347
|
const bVal = b[name];
|
|
@@ -321,12 +353,7 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
321
353
|
if (aVal.length !== bVal.length) {
|
|
322
354
|
return true;
|
|
323
355
|
}
|
|
324
|
-
if (
|
|
325
|
-
xor(
|
|
326
|
-
aVal.map((entity) => entity.guid),
|
|
327
|
-
bVal.map((entity) => entity.guid)
|
|
328
|
-
).length
|
|
329
|
-
) {
|
|
356
|
+
if (xor(aVal, bVal).length) {
|
|
330
357
|
return true;
|
|
331
358
|
}
|
|
332
359
|
}
|
|
@@ -335,41 +362,42 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
335
362
|
|
|
336
363
|
if (areAcPropertiesChanged(originalAc, newAc)) {
|
|
337
364
|
if (
|
|
338
|
-
!tilmeld.checkPermissions(
|
|
365
|
+
!tilmeld.checkPermissions(
|
|
366
|
+
entity,
|
|
367
|
+
TilmeldAccessLevels.FULL_ACCESS,
|
|
368
|
+
undefined,
|
|
369
|
+
originalAc,
|
|
370
|
+
)
|
|
339
371
|
) {
|
|
340
372
|
// Only allow changes to AC properties if the user has full access.
|
|
341
373
|
throw new AccessControlError(
|
|
342
|
-
'No permission to change access control properties.'
|
|
374
|
+
'No permission to change access control properties.',
|
|
343
375
|
);
|
|
344
376
|
}
|
|
345
377
|
|
|
346
378
|
if (
|
|
347
|
-
(
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
!tilmeld.gatekeeper('tilmeld/admin')
|
|
379
|
+
(originalAc.user != null && originalAc.user !== newAc.user) ||
|
|
380
|
+
(originalAc.user == null &&
|
|
381
|
+
newAc.user != null &&
|
|
382
|
+
newAc.user !== tilmeld.currentUser?.guid)
|
|
352
383
|
) {
|
|
353
384
|
throw new AccessControlError(
|
|
354
|
-
'No permission to assign to another user.'
|
|
385
|
+
'No permission to assign to another user.',
|
|
355
386
|
);
|
|
356
387
|
}
|
|
357
388
|
|
|
358
389
|
if (
|
|
359
|
-
newAc.group &&
|
|
360
|
-
|
|
390
|
+
newAc.group != null &&
|
|
391
|
+
newAc.group !== originalAc.group &&
|
|
361
392
|
!(
|
|
362
|
-
newAc.group
|
|
363
|
-
|
|
364
|
-
)
|
|
365
|
-
!tilmeld.gatekeeper('tilmeld/admin')
|
|
393
|
+
newAc.group === tilmeld.currentUser?.$getGid() ||
|
|
394
|
+
(tilmeld.currentUser?.$getGids() ?? []).includes(newAc.group)
|
|
395
|
+
)
|
|
366
396
|
) {
|
|
367
397
|
throw new AccessControlError(
|
|
368
|
-
'No permission to assign to another group.'
|
|
398
|
+
'No permission to assign to another group.',
|
|
369
399
|
);
|
|
370
400
|
}
|
|
371
|
-
|
|
372
|
-
setAcProperties(newAc);
|
|
373
401
|
}
|
|
374
402
|
}
|
|
375
403
|
|
|
@@ -398,9 +426,9 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
398
426
|
*/
|
|
399
427
|
const addAccess = async function (
|
|
400
428
|
nymph: Nymph,
|
|
401
|
-
entity: EntityInterface & AccessControlData
|
|
429
|
+
entity: EntityInterface & AccessControlData,
|
|
402
430
|
) {
|
|
403
|
-
const tilmeld = nymph
|
|
431
|
+
const tilmeld = enforceTilmeld(nymph);
|
|
404
432
|
const user = tilmeld.currentUser;
|
|
405
433
|
if (
|
|
406
434
|
user != null &&
|
|
@@ -411,12 +439,11 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
411
439
|
if (entity.user == null) {
|
|
412
440
|
entity.user = user;
|
|
413
441
|
}
|
|
414
|
-
if (
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
entity.group = user.group;
|
|
442
|
+
if (entity.group == null) {
|
|
443
|
+
const group = user.group;
|
|
444
|
+
if (group != null && group.guid != null) {
|
|
445
|
+
entity.group = group;
|
|
446
|
+
}
|
|
420
447
|
}
|
|
421
448
|
if (entity.acUser == null) {
|
|
422
449
|
entity.acUser = TilmeldAccessLevels.FULL_ACCESS;
|
|
@@ -442,7 +469,7 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
442
469
|
|
|
443
470
|
const validate = async function (
|
|
444
471
|
_nymph: Nymph,
|
|
445
|
-
entity: EntityInterface & AccessControlData
|
|
472
|
+
entity: EntityInterface & AccessControlData,
|
|
446
473
|
) {
|
|
447
474
|
if (!(entity instanceof User) && !(entity instanceof Group)) {
|
|
448
475
|
const ownershipAcPropertyValidator = (prop: any) => {
|
|
@@ -452,7 +479,7 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
452
479
|
prop > TilmeldAccessLevels.FULL_ACCESS
|
|
453
480
|
) {
|
|
454
481
|
throw new AccessControlError(
|
|
455
|
-
'Invalid access control property: ' + prop
|
|
482
|
+
'Invalid access control property: ' + prop,
|
|
456
483
|
);
|
|
457
484
|
}
|
|
458
485
|
};
|
|
@@ -460,13 +487,13 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
460
487
|
const accessAcPropertyValidator = (prop: any) => {
|
|
461
488
|
if (!Array.isArray(prop)) {
|
|
462
489
|
throw new AccessControlError(
|
|
463
|
-
'Invalid access control property: ' + prop
|
|
490
|
+
'Invalid access control property: ' + prop,
|
|
464
491
|
);
|
|
465
492
|
}
|
|
466
493
|
prop.forEach((value) => {
|
|
467
|
-
if (
|
|
494
|
+
if (typeof value !== 'string' || !value.match(/^[0-9a-f]{24}$/i)) {
|
|
468
495
|
throw new AccessControlError(
|
|
469
|
-
'Invalid access control property: ' + prop
|
|
496
|
+
'Invalid access control property: ' + prop,
|
|
470
497
|
);
|
|
471
498
|
}
|
|
472
499
|
});
|
|
@@ -475,7 +502,7 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
475
502
|
if ('user' in entity) {
|
|
476
503
|
if (!(entity.user instanceof User)) {
|
|
477
504
|
throw new AccessControlError(
|
|
478
|
-
'Invalid access control property: user'
|
|
505
|
+
'Invalid access control property: user',
|
|
479
506
|
);
|
|
480
507
|
}
|
|
481
508
|
}
|
|
@@ -483,7 +510,7 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
483
510
|
if ('group' in entity) {
|
|
484
511
|
if (!(entity.group instanceof Group)) {
|
|
485
512
|
throw new AccessControlError(
|
|
486
|
-
'Invalid access control property: group'
|
|
513
|
+
'Invalid access control property: group',
|
|
487
514
|
);
|
|
488
515
|
}
|
|
489
516
|
}
|
|
@@ -528,18 +555,10 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
528
555
|
*/
|
|
529
556
|
public addAccessControlSelectors(
|
|
530
557
|
options: Options,
|
|
531
|
-
selectors: FormattedSelector[]
|
|
558
|
+
selectors: FormattedSelector[],
|
|
532
559
|
) {
|
|
533
560
|
const user = this.currentUser;
|
|
534
561
|
|
|
535
|
-
if (
|
|
536
|
-
user != null &&
|
|
537
|
-
(user.abilities?.indexOf('system/admin') ?? -1) !== -1
|
|
538
|
-
) {
|
|
539
|
-
// The user is a system admin, so they can see everything.
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
562
|
if (options == null) {
|
|
544
563
|
throw new Error('No options in argument.');
|
|
545
564
|
} else if (
|
|
@@ -552,59 +571,112 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
552
571
|
}
|
|
553
572
|
|
|
554
573
|
if (user == null || user.guid == null) {
|
|
555
|
-
|
|
556
|
-
|
|
574
|
+
// Access requests for only public entities.
|
|
575
|
+
|
|
576
|
+
const allAccessRequests =
|
|
577
|
+
TilmeldAccessRequest.OTHER_ACCESSIBLE | TilmeldAccessRequest.UNOWNED;
|
|
578
|
+
const requestedAccess = (options.acRequest ?? 0) & allAccessRequests;
|
|
579
|
+
const defaultAccess = requestedAccess === TilmeldAccessRequest.ALL_LEVELS;
|
|
580
|
+
|
|
581
|
+
const selector: FormattedSelector = { type: '|' };
|
|
582
|
+
|
|
583
|
+
if (
|
|
584
|
+
defaultAccess ||
|
|
585
|
+
requestedAccess & TilmeldAccessRequest.OTHER_ACCESSIBLE
|
|
586
|
+
) {
|
|
557
587
|
// Other access control is sufficient.
|
|
558
|
-
gte
|
|
588
|
+
selector.gte = [['acOther', TilmeldAccessLevels.READ_ACCESS]];
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (defaultAccess || requestedAccess & TilmeldAccessRequest.UNOWNED) {
|
|
559
592
|
// The user and group are not set.
|
|
560
|
-
selector
|
|
593
|
+
selector.selector = [
|
|
561
594
|
{
|
|
562
595
|
type: '&',
|
|
563
596
|
'!defined': [['user', 'group']],
|
|
564
597
|
},
|
|
565
|
-
]
|
|
566
|
-
}
|
|
598
|
+
];
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (Object.keys(selector).length <= 1) {
|
|
602
|
+
this.nymph.config.debugError(
|
|
603
|
+
'tilmeld',
|
|
604
|
+
'No access controls were added to the query. This should not be possible.',
|
|
605
|
+
);
|
|
606
|
+
throw new Error('Access control error.');
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
selectors.push(selector);
|
|
567
610
|
} else {
|
|
568
|
-
|
|
569
|
-
|
|
611
|
+
// Access requests for public and private entities.
|
|
612
|
+
|
|
613
|
+
const allAccessRequests =
|
|
614
|
+
TilmeldAccessRequest.USER_OWNED |
|
|
615
|
+
TilmeldAccessRequest.PRIMARY_GROUP_OWNED |
|
|
616
|
+
TilmeldAccessRequest.SECONDARY_GROUP_OWNED |
|
|
617
|
+
TilmeldAccessRequest.USER_ACCESSIBLE |
|
|
618
|
+
TilmeldAccessRequest.PRIMARY_GROUP_ACCESSIBLE |
|
|
619
|
+
TilmeldAccessRequest.SECONDARY_GROUP_ACCESSIBLE |
|
|
620
|
+
TilmeldAccessRequest.OTHER_ACCESSIBLE |
|
|
621
|
+
TilmeldAccessRequest.UNOWNED;
|
|
622
|
+
const requestedAccess = (options.acRequest ?? 0) & allAccessRequests;
|
|
623
|
+
const defaultAccess = requestedAccess === TilmeldAccessRequest.ALL_LEVELS;
|
|
624
|
+
|
|
625
|
+
const subSelectors: FormattedSelector[] = [];
|
|
626
|
+
if (defaultAccess || requestedAccess & TilmeldAccessRequest.UNOWNED) {
|
|
627
|
+
subSelectors.push({
|
|
570
628
|
type: '&',
|
|
629
|
+
// The user and group are not set.
|
|
571
630
|
'!defined': [['user', 'group']],
|
|
572
|
-
}
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
// USER_OWNED is always enforced.
|
|
634
|
+
subSelectors.push(
|
|
573
635
|
// It is owned by the user.
|
|
574
636
|
{
|
|
575
637
|
type: '&',
|
|
576
638
|
ref: [['user', user.guid]],
|
|
577
639
|
gte: [['acUser', TilmeldAccessLevels.READ_ACCESS]],
|
|
578
640
|
},
|
|
579
|
-
|
|
641
|
+
);
|
|
580
642
|
const groupRefs: FormattedSelector['ref'] = [];
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
if (
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
643
|
+
const acContains: FormattedSelector['contain'] = [];
|
|
644
|
+
const gid = user.$getGid();
|
|
645
|
+
if (gid != null) {
|
|
646
|
+
if (
|
|
647
|
+
defaultAccess ||
|
|
648
|
+
requestedAccess & TilmeldAccessRequest.PRIMARY_GROUP_OWNED
|
|
649
|
+
) {
|
|
650
|
+
// It belongs to the user's primary group.
|
|
651
|
+
groupRefs.push(['group', gid]);
|
|
652
|
+
}
|
|
653
|
+
if (
|
|
654
|
+
defaultAccess ||
|
|
655
|
+
requestedAccess & TilmeldAccessRequest.PRIMARY_GROUP_ACCESSIBLE
|
|
656
|
+
) {
|
|
657
|
+
// User's primary group guid is listed in acRead, acWrite, or acFull.
|
|
658
|
+
acContains.push(['acRead', gid]);
|
|
659
|
+
acContains.push(['acWrite', gid]);
|
|
660
|
+
acContains.push(['acFull', gid]);
|
|
598
661
|
}
|
|
599
662
|
}
|
|
600
|
-
|
|
601
|
-
|
|
663
|
+
const gids = user.$getGids();
|
|
664
|
+
for (let curGid of gids ?? []) {
|
|
665
|
+
if (
|
|
666
|
+
defaultAccess ||
|
|
667
|
+
requestedAccess & TilmeldAccessRequest.SECONDARY_GROUP_OWNED
|
|
668
|
+
) {
|
|
602
669
|
// It belongs to the user's secondary group.
|
|
603
|
-
groupRefs.push(['group',
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
670
|
+
groupRefs.push(['group', curGid]);
|
|
671
|
+
}
|
|
672
|
+
if (
|
|
673
|
+
defaultAccess ||
|
|
674
|
+
requestedAccess & TilmeldAccessRequest.SECONDARY_GROUP_ACCESSIBLE
|
|
675
|
+
) {
|
|
676
|
+
// User's secondary group guid is listed in acRead, acWrite, or acFull.
|
|
677
|
+
acContains.push(['acRead', curGid]);
|
|
678
|
+
acContains.push(['acWrite', curGid]);
|
|
679
|
+
acContains.push(['acFull', curGid]);
|
|
608
680
|
}
|
|
609
681
|
}
|
|
610
682
|
// All the group refs.
|
|
@@ -620,26 +692,43 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
620
692
|
],
|
|
621
693
|
});
|
|
622
694
|
}
|
|
623
|
-
// All the acRead, acWrite, and acFull
|
|
624
|
-
if (
|
|
695
|
+
// All the acRead, acWrite, and acFull contains.
|
|
696
|
+
if (acContains.length) {
|
|
625
697
|
subSelectors.push({
|
|
626
698
|
type: '|',
|
|
627
|
-
|
|
699
|
+
contain: acContains,
|
|
628
700
|
});
|
|
629
701
|
}
|
|
630
|
-
const selector: FormattedSelector = {
|
|
631
|
-
|
|
702
|
+
const selector: FormattedSelector = { type: '|' };
|
|
703
|
+
if (
|
|
704
|
+
defaultAccess ||
|
|
705
|
+
requestedAccess & TilmeldAccessRequest.OTHER_ACCESSIBLE
|
|
706
|
+
) {
|
|
632
707
|
// Other access control is sufficient.
|
|
633
|
-
gte
|
|
634
|
-
|
|
635
|
-
|
|
708
|
+
selector.gte = [['acOther', TilmeldAccessLevels.READ_ACCESS]];
|
|
709
|
+
}
|
|
710
|
+
if (subSelectors.length) {
|
|
711
|
+
// One of the sub selectors.
|
|
712
|
+
selector.selector = subSelectors;
|
|
713
|
+
}
|
|
714
|
+
if (
|
|
715
|
+
defaultAccess ||
|
|
716
|
+
requestedAccess & TilmeldAccessRequest.USER_ACCESSIBLE
|
|
717
|
+
) {
|
|
636
718
|
// The user is listed in acRead, acWrite, or acFull.
|
|
637
|
-
|
|
719
|
+
selector.contain = [
|
|
638
720
|
['acRead', user.guid],
|
|
639
721
|
['acWrite', user.guid],
|
|
640
722
|
['acFull', user.guid],
|
|
641
|
-
]
|
|
642
|
-
}
|
|
723
|
+
];
|
|
724
|
+
}
|
|
725
|
+
if (Object.keys(selector).length <= 1) {
|
|
726
|
+
this.nymph.config.debugError(
|
|
727
|
+
'tilmeld',
|
|
728
|
+
'No access controls were added to the query. This should not be possible.',
|
|
729
|
+
);
|
|
730
|
+
throw new Error('Access control error.');
|
|
731
|
+
}
|
|
643
732
|
selectors.push(selector);
|
|
644
733
|
}
|
|
645
734
|
}
|
|
@@ -671,7 +760,7 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
671
760
|
* - FULL_ACCESS, the user has read, write, and delete access to the entity,
|
|
672
761
|
* as well as being able to manage its access controls and ownership.
|
|
673
762
|
*
|
|
674
|
-
* These properties
|
|
763
|
+
* These properties default to:
|
|
675
764
|
*
|
|
676
765
|
* - acUser = TilmeldAccessLevels.FULL_ACCESS
|
|
677
766
|
* - acGroup = TilmeldAccessLevels.READ_ACCESS
|
|
@@ -686,9 +775,9 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
686
775
|
* The following conditions will result in different checks, which determine
|
|
687
776
|
* whether the check passes:
|
|
688
777
|
*
|
|
689
|
-
* - The user has the "system/admin" ability. (Always true.)
|
|
690
778
|
* - It is a user or group. (True for READ_ACCESS or Tilmeld admins.)
|
|
691
|
-
* -
|
|
779
|
+
* - It is a user or group that belongs to a domain the current user is an
|
|
780
|
+
* admin for, and domain support is enabled. (Always true.)
|
|
692
781
|
* - No user is logged in. (Check other AC.)
|
|
693
782
|
* - The entity is the user. (Always true.)
|
|
694
783
|
* - It is the user's primary group. (True for READ_ACCESS.)
|
|
@@ -706,16 +795,17 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
706
795
|
* @param entity The entity to check.
|
|
707
796
|
* @param type The lowest level of permission to consider a pass.
|
|
708
797
|
* @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
|
|
798
|
+
* @param acProperties The acProperties to use instead of getting them from the entity.
|
|
709
799
|
* @returns Whether the current user has at least `type` permission for the entity.
|
|
710
800
|
*/
|
|
711
801
|
public checkPermissions(
|
|
712
|
-
entity: EntityInterface
|
|
802
|
+
entity: EntityInterface,
|
|
713
803
|
type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
|
|
714
|
-
user?: (User & UserData) | false
|
|
804
|
+
user?: (User & UserData) | false,
|
|
805
|
+
acProperties?: ACProperties,
|
|
715
806
|
) {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
return false;
|
|
807
|
+
if (!acProperties) {
|
|
808
|
+
acProperties = entity.$getCurrentAcValues() as ACProperties;
|
|
719
809
|
}
|
|
720
810
|
|
|
721
811
|
let userOrNull: (User & UserData) | null = null;
|
|
@@ -729,30 +819,43 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
729
819
|
userOrEmpty = user;
|
|
730
820
|
}
|
|
731
821
|
|
|
732
|
-
if (userOrEmpty.$gatekeeper('system/admin')) {
|
|
733
|
-
return true;
|
|
734
|
-
}
|
|
735
|
-
|
|
736
822
|
// Users and groups are always readable. Editable by Tilmeld admins.
|
|
737
823
|
if (
|
|
738
824
|
(entity instanceof User || entity instanceof Group) &&
|
|
739
825
|
(type === TilmeldAccessLevels.READ_ACCESS ||
|
|
740
|
-
userOrEmpty
|
|
826
|
+
userOrEmpty.abilities?.includes('tilmeld/admin') ||
|
|
827
|
+
userOrEmpty.abilities?.includes('system/admin'))
|
|
741
828
|
) {
|
|
742
829
|
return true;
|
|
743
830
|
}
|
|
744
831
|
|
|
745
|
-
//
|
|
746
|
-
|
|
747
|
-
|
|
832
|
+
// Users and groups that belong to a domain are readable and editable by
|
|
833
|
+
// that domain's admins.
|
|
834
|
+
if (
|
|
835
|
+
this.config.domainSupport &&
|
|
836
|
+
(entity instanceof User || entity instanceof Group)
|
|
837
|
+
) {
|
|
838
|
+
const [_local, domain] = splitn(
|
|
839
|
+
(entity instanceof User
|
|
840
|
+
? (entity as User & UserData).username
|
|
841
|
+
: (entity as Group & GroupData).groupname) ?? '',
|
|
842
|
+
'@',
|
|
843
|
+
2,
|
|
844
|
+
);
|
|
845
|
+
if (
|
|
846
|
+
domain &&
|
|
847
|
+
userOrEmpty.abilities?.includes(`tilmeld/domain/${domain}/admin`)
|
|
848
|
+
) {
|
|
849
|
+
return true;
|
|
850
|
+
}
|
|
748
851
|
}
|
|
749
852
|
|
|
750
853
|
// Load access control, since we need it now...
|
|
751
|
-
const acUser =
|
|
752
|
-
const acGroup =
|
|
753
|
-
const acOther =
|
|
854
|
+
const acUser = acProperties.acUser ?? TilmeldAccessLevels.FULL_ACCESS;
|
|
855
|
+
const acGroup = acProperties.acGroup ?? TilmeldAccessLevels.READ_ACCESS;
|
|
856
|
+
const acOther = acProperties.acOther ?? TilmeldAccessLevels.NO_ACCESS;
|
|
754
857
|
|
|
755
|
-
if (userOrNull
|
|
858
|
+
if (userOrNull == null) {
|
|
756
859
|
return acOther >= type;
|
|
757
860
|
}
|
|
758
861
|
|
|
@@ -762,45 +865,59 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
762
865
|
}
|
|
763
866
|
|
|
764
867
|
// Check if the entity is the user's group. Always readable.
|
|
868
|
+
const gid = userOrEmpty.$getGid();
|
|
869
|
+
if (
|
|
870
|
+
gid != null &&
|
|
871
|
+
entity.guid === gid &&
|
|
872
|
+
type === TilmeldAccessLevels.READ_ACCESS
|
|
873
|
+
) {
|
|
874
|
+
return true;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Check if the entity is one of the user's secondary groups. Always
|
|
878
|
+
// readable.
|
|
879
|
+
const gids = userOrEmpty.$getGids();
|
|
765
880
|
if (
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
881
|
+
gids != null &&
|
|
882
|
+
entity.guid != null &&
|
|
883
|
+
gids.indexOf(entity.guid) !== -1 &&
|
|
769
884
|
type === TilmeldAccessLevels.READ_ACCESS
|
|
770
885
|
) {
|
|
771
886
|
return true;
|
|
772
887
|
}
|
|
773
888
|
|
|
889
|
+
// At this point, if the entity is a user or group, it should not be
|
|
890
|
+
// accessible.
|
|
891
|
+
if (entity instanceof User || entity instanceof Group) {
|
|
892
|
+
return false;
|
|
893
|
+
}
|
|
894
|
+
|
|
774
895
|
// Calculate all the groups the user belongs to.
|
|
775
|
-
let
|
|
776
|
-
|
|
777
|
-
allGroups = allGroups.concat(userOrEmpty.$getDescendantGroupsSync());
|
|
896
|
+
let allGids = gid == null ? [] : [gid];
|
|
897
|
+
allGids = allGids.concat(gids ?? []);
|
|
778
898
|
|
|
779
899
|
// Check access ac properties.
|
|
780
900
|
const checks = [
|
|
781
901
|
{
|
|
782
902
|
type: TilmeldAccessLevels.FULL_ACCESS,
|
|
783
|
-
array: [...(
|
|
903
|
+
array: [...(acProperties.acFull ?? [])],
|
|
784
904
|
},
|
|
785
905
|
{
|
|
786
906
|
type: TilmeldAccessLevels.WRITE_ACCESS,
|
|
787
|
-
array: [...(
|
|
907
|
+
array: [...(acProperties.acWrite ?? [])],
|
|
788
908
|
},
|
|
789
909
|
{
|
|
790
910
|
type: TilmeldAccessLevels.READ_ACCESS,
|
|
791
|
-
array: [...(
|
|
911
|
+
array: [...(acProperties.acRead ?? [])],
|
|
792
912
|
},
|
|
793
913
|
];
|
|
794
914
|
for (let curCheck of checks) {
|
|
795
915
|
if (type <= curCheck.type) {
|
|
796
|
-
if (
|
|
916
|
+
if (curCheck.array.includes(userOrEmpty.guid)) {
|
|
797
917
|
return true;
|
|
798
918
|
}
|
|
799
|
-
for (let
|
|
800
|
-
if (
|
|
801
|
-
typeof curGroup.$inArray === 'function' &&
|
|
802
|
-
curGroup.$inArray(curCheck.array)
|
|
803
|
-
) {
|
|
919
|
+
for (let curGid of allGids) {
|
|
920
|
+
if (curCheck.array.includes(curGid)) {
|
|
804
921
|
return true;
|
|
805
922
|
}
|
|
806
923
|
}
|
|
@@ -808,18 +925,10 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
808
925
|
}
|
|
809
926
|
|
|
810
927
|
// Check ownership ac properties.
|
|
811
|
-
if (
|
|
812
|
-
entity.user != null &&
|
|
813
|
-
typeof entity.user.$is === 'function' &&
|
|
814
|
-
entity.user.$is(userOrNull)
|
|
815
|
-
) {
|
|
928
|
+
if (acProperties.user != null && acProperties.user === userOrNull.guid) {
|
|
816
929
|
return acUser >= type;
|
|
817
930
|
}
|
|
818
|
-
if (
|
|
819
|
-
entity.group != null &&
|
|
820
|
-
typeof entity.group.$inArray === 'function' &&
|
|
821
|
-
entity.group.$inArray(allGroups)
|
|
822
|
-
) {
|
|
931
|
+
if (acProperties.group != null && allGids.includes(acProperties.group)) {
|
|
823
932
|
return acGroup >= type;
|
|
824
933
|
}
|
|
825
934
|
return acOther >= type;
|
|
@@ -849,10 +958,10 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
849
958
|
* @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
|
|
850
959
|
* @returns Whether the current user has at least `type` permission for the UID.
|
|
851
960
|
*/
|
|
852
|
-
public checkClientUIDPermissions(
|
|
961
|
+
public async checkClientUIDPermissions(
|
|
853
962
|
name: string,
|
|
854
963
|
type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
|
|
855
|
-
user?: (User & UserData) | false
|
|
964
|
+
user?: (User & UserData) | false,
|
|
856
965
|
) {
|
|
857
966
|
let userOrEmpty: User & UserData = this.User.factorySync();
|
|
858
967
|
// Calculate the user.
|
|
@@ -862,30 +971,30 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
862
971
|
userOrEmpty = user;
|
|
863
972
|
}
|
|
864
973
|
|
|
865
|
-
if (userOrEmpty
|
|
974
|
+
if (userOrEmpty.abilities?.includes('system/admin')) {
|
|
866
975
|
return true;
|
|
867
976
|
}
|
|
868
977
|
|
|
869
978
|
if (type === TilmeldAccessLevels.FULL_ACCESS) {
|
|
870
979
|
return (
|
|
871
|
-
this.config.clientSetabledUIDs.
|
|
872
|
-
userOrEmpty.$gatekeeper(`uid/set/${name}`)
|
|
980
|
+
this.config.clientSetabledUIDs.includes(name) ||
|
|
981
|
+
(await userOrEmpty.$gatekeeper(`uid/set/${name}`))
|
|
873
982
|
);
|
|
874
983
|
} else if (type === TilmeldAccessLevels.WRITE_ACCESS) {
|
|
875
984
|
return (
|
|
876
|
-
this.config.clientEnabledUIDs.
|
|
877
|
-
this.config.clientSetabledUIDs.
|
|
878
|
-
userOrEmpty.$gatekeeper(`uid/new/${name}`) ||
|
|
879
|
-
userOrEmpty.$gatekeeper(`uid/set/${name}`)
|
|
985
|
+
this.config.clientEnabledUIDs.includes(name) ||
|
|
986
|
+
this.config.clientSetabledUIDs.includes(name) ||
|
|
987
|
+
(await userOrEmpty.$gatekeeper(`uid/new/${name}`)) ||
|
|
988
|
+
(await userOrEmpty.$gatekeeper(`uid/set/${name}`))
|
|
880
989
|
);
|
|
881
990
|
} else if (type === TilmeldAccessLevels.READ_ACCESS) {
|
|
882
991
|
return (
|
|
883
|
-
this.config.clientReadableUIDs.
|
|
884
|
-
this.config.clientEnabledUIDs.
|
|
885
|
-
this.config.clientSetabledUIDs.
|
|
886
|
-
userOrEmpty.$gatekeeper(`uid/get/${name}`) ||
|
|
887
|
-
userOrEmpty.$gatekeeper(`uid/new/${name}`) ||
|
|
888
|
-
userOrEmpty.$gatekeeper(`uid/set/${name}`)
|
|
992
|
+
this.config.clientReadableUIDs.includes(name) ||
|
|
993
|
+
this.config.clientEnabledUIDs.includes(name) ||
|
|
994
|
+
this.config.clientSetabledUIDs.includes(name) ||
|
|
995
|
+
(await userOrEmpty.$gatekeeper(`uid/get/${name}`)) ||
|
|
996
|
+
(await userOrEmpty.$gatekeeper(`uid/new/${name}`)) ||
|
|
997
|
+
(await userOrEmpty.$gatekeeper(`uid/set/${name}`))
|
|
889
998
|
);
|
|
890
999
|
}
|
|
891
1000
|
|
|
@@ -897,20 +1006,13 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
897
1006
|
*
|
|
898
1007
|
* @param user The user.
|
|
899
1008
|
*/
|
|
900
|
-
public fillSession(user: User & UserData) {
|
|
901
|
-
// Read groups right now, since gatekeeper needs them, so
|
|
902
|
-
// $udpateDataProtection will fail to read them (since it runs gatekeeper).
|
|
903
|
-
const _group = user.group;
|
|
904
|
-
const _groups = user.groups;
|
|
1009
|
+
public async fillSession(user: User & UserData) {
|
|
905
1010
|
this.currentUser = user;
|
|
1011
|
+
this.gatekeeperCache = await user.$getGatekeeperCache();
|
|
1012
|
+
|
|
906
1013
|
// Now update the data protection on the user and all the groups.
|
|
907
1014
|
this.currentUser.$updateDataProtection();
|
|
908
|
-
|
|
909
|
-
this.currentUser.group.$updateDataProtection();
|
|
910
|
-
}
|
|
911
|
-
for (let group of this.currentUser.groups ?? []) {
|
|
912
|
-
group.$updateDataProtection();
|
|
913
|
-
}
|
|
1015
|
+
this.currentUser.$updateGroupDataProtection();
|
|
914
1016
|
}
|
|
915
1017
|
|
|
916
1018
|
/**
|
|
@@ -919,14 +1021,10 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
919
1021
|
public clearSession() {
|
|
920
1022
|
const user = this.currentUser;
|
|
921
1023
|
this.currentUser = null;
|
|
1024
|
+
this.gatekeeperCache = null;
|
|
922
1025
|
if (user) {
|
|
923
1026
|
user.$updateDataProtection();
|
|
924
|
-
|
|
925
|
-
user.group.$updateDataProtection();
|
|
926
|
-
}
|
|
927
|
-
for (let group of user.groups ?? []) {
|
|
928
|
-
group.$updateDataProtection();
|
|
929
|
-
}
|
|
1027
|
+
user.$updateGroupDataProtection();
|
|
930
1028
|
}
|
|
931
1029
|
}
|
|
932
1030
|
|
|
@@ -937,6 +1035,7 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
937
1035
|
* @returns The user on success, null on failure.
|
|
938
1036
|
*/
|
|
939
1037
|
public async extractToken(token: string) {
|
|
1038
|
+
this.nymph.config.debugInfo('tilmeld', 'Extracting and validating token.');
|
|
940
1039
|
const extract = this.config.jwtExtract(this.config, token);
|
|
941
1040
|
if (extract == null) {
|
|
942
1041
|
return null;
|
|
@@ -948,9 +1047,9 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
948
1047
|
{
|
|
949
1048
|
type: '&',
|
|
950
1049
|
guid: guid,
|
|
951
|
-
}
|
|
1050
|
+
},
|
|
952
1051
|
);
|
|
953
|
-
if (!user || !user.guid
|
|
1052
|
+
if (!user || !user.guid) {
|
|
954
1053
|
return null;
|
|
955
1054
|
}
|
|
956
1055
|
|
|
@@ -958,40 +1057,73 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
958
1057
|
}
|
|
959
1058
|
|
|
960
1059
|
/**
|
|
961
|
-
* Check for
|
|
1060
|
+
* Check for TILMELDAUTH and TILMELDSWITCH tokens, and, if set, authenticate
|
|
1061
|
+
* from it/them.
|
|
1062
|
+
*
|
|
1063
|
+
* You can also call this function after setting `response.locals.user` to the
|
|
1064
|
+
* user you want to authenticate. You *should* check for `user.enabled` before
|
|
1065
|
+
* setting this variable, unless you explicitly want to log in as a disabled
|
|
1066
|
+
* user. (The user must be an instance of the User class for this Tilmeld
|
|
1067
|
+
* instance.)
|
|
1068
|
+
*
|
|
1069
|
+
* This function will set `response.locals.user` to the logged in user on
|
|
1070
|
+
* successful authentication.
|
|
962
1071
|
*
|
|
963
1072
|
* @param skipXsrfToken Skip the XSRF token check.
|
|
1073
|
+
* @param skipRenew Skip the token renewal step, even if the token is close to expiration.
|
|
964
1074
|
* @returns True if a user was authenticated, false on any failure.
|
|
965
1075
|
*/
|
|
966
|
-
public authenticate(skipXsrfToken = false) {
|
|
1076
|
+
public async authenticate(skipXsrfToken = false, skipRenew = false) {
|
|
967
1077
|
if (this.request == null) {
|
|
968
1078
|
return false;
|
|
969
1079
|
}
|
|
970
1080
|
|
|
1081
|
+
if (
|
|
1082
|
+
this.response &&
|
|
1083
|
+
this.response.locals.user &&
|
|
1084
|
+
this.response.locals.user instanceof this.User
|
|
1085
|
+
) {
|
|
1086
|
+
// The user has already been authenticated through some other means.
|
|
1087
|
+
const user = this.response.locals.user;
|
|
1088
|
+
await this.fillSession(user);
|
|
1089
|
+
return true;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
971
1092
|
const cookies = this.request.cookies ?? {};
|
|
972
1093
|
|
|
973
1094
|
// If a client does't support cookies, they can use the X-TILMELDAUTH header
|
|
974
1095
|
// to provide the auth token.
|
|
975
1096
|
let fromAuthHeader = false;
|
|
976
1097
|
let authToken: string;
|
|
1098
|
+
let switchToken: string | null;
|
|
977
1099
|
if (this.request.header('x-tilmeldauth') != null) {
|
|
978
1100
|
fromAuthHeader = true;
|
|
979
1101
|
authToken = this.request.header('x-tilmeldauth') as string;
|
|
1102
|
+
switchToken =
|
|
1103
|
+
(this.request.header('x-tilmeldswitch') as string) ||
|
|
1104
|
+
'TILMELDSWITCH' in cookies
|
|
1105
|
+
? cookies.TILMELDSWITCH
|
|
1106
|
+
: null;
|
|
980
1107
|
} else if ('TILMELDAUTH' in cookies) {
|
|
981
1108
|
fromAuthHeader = false;
|
|
982
1109
|
authToken = cookies.TILMELDAUTH;
|
|
1110
|
+
switchToken = 'TILMELDSWITCH' in cookies ? cookies.TILMELDSWITCH : null;
|
|
983
1111
|
} else {
|
|
984
1112
|
return false;
|
|
985
1113
|
}
|
|
986
1114
|
|
|
987
|
-
let
|
|
1115
|
+
let authExtract: { guid: string; issued: Date; expire: Date } | null;
|
|
1116
|
+
let switchExtract: { guid: string; expire: Date } | null;
|
|
988
1117
|
if (
|
|
989
1118
|
skipXsrfToken ||
|
|
990
1119
|
this.request.originalUrl.startsWith(this.config.setupPath)
|
|
991
1120
|
) {
|
|
992
1121
|
// The request is for the setup app, or we were told to skip the XSRF
|
|
993
1122
|
// check, so don't check for the XSRF token.
|
|
994
|
-
|
|
1123
|
+
authExtract = this.config.jwtExtract(this.config, authToken);
|
|
1124
|
+
switchExtract = switchToken
|
|
1125
|
+
? this.config.jwtExtract(this.config, switchToken)
|
|
1126
|
+
: null;
|
|
995
1127
|
} else {
|
|
996
1128
|
// The request is for something else, so check for a valid XSRF token,
|
|
997
1129
|
// unless the auth token is provided by a header (instead of a cookie).
|
|
@@ -1000,28 +1132,65 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
1000
1132
|
return false;
|
|
1001
1133
|
}
|
|
1002
1134
|
|
|
1003
|
-
|
|
1135
|
+
authExtract = this.config.jwtExtract(this.config, authToken, xsrfToken);
|
|
1136
|
+
switchExtract = switchToken
|
|
1137
|
+
? this.config.jwtExtract(this.config, switchToken)
|
|
1138
|
+
: null;
|
|
1004
1139
|
}
|
|
1005
1140
|
|
|
1006
|
-
if (
|
|
1007
|
-
this.logout();
|
|
1141
|
+
if (authExtract == null) {
|
|
1142
|
+
await this.logout();
|
|
1008
1143
|
return false;
|
|
1009
1144
|
}
|
|
1010
|
-
const { guid, expire } =
|
|
1145
|
+
const { guid, issued, expire } = authExtract;
|
|
1146
|
+
const { guid: switchGuid, expire: switchExpire } = switchExtract || {
|
|
1147
|
+
guid: null,
|
|
1148
|
+
expire: null,
|
|
1149
|
+
};
|
|
1011
1150
|
|
|
1012
|
-
const user = this.User.
|
|
1013
|
-
if (
|
|
1014
|
-
|
|
1151
|
+
const user = await this.User.factory(guid);
|
|
1152
|
+
if (
|
|
1153
|
+
user.guid == null ||
|
|
1154
|
+
!user.enabled ||
|
|
1155
|
+
expire.getTime() <= Date.now() ||
|
|
1156
|
+
issued.getTime() > Date.now() ||
|
|
1157
|
+
(user.revokeTokenDate != null && issued.getTime() <= user.revokeTokenDate)
|
|
1158
|
+
) {
|
|
1159
|
+
await this.logout();
|
|
1015
1160
|
return false;
|
|
1016
1161
|
}
|
|
1017
1162
|
|
|
1018
|
-
if (
|
|
1163
|
+
if (switchGuid && switchExpire && switchExpire.getTime() > Date.now()) {
|
|
1164
|
+
// Load the switch user.
|
|
1165
|
+
const switchUser = await this.User.factory(switchGuid);
|
|
1166
|
+
if (switchUser.guid == null) {
|
|
1167
|
+
await this.logoutSwitch();
|
|
1168
|
+
} else {
|
|
1169
|
+
await this.fillSession(switchUser);
|
|
1170
|
+
|
|
1171
|
+
if (this.response) {
|
|
1172
|
+
this.response.locals.user = switchUser;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
return true;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
if (
|
|
1180
|
+
!skipRenew &&
|
|
1181
|
+
expire.getTime() < Date.now() + this.config.jwtRenew * 1000
|
|
1182
|
+
) {
|
|
1019
1183
|
// If the user is less than renew time from needing a new token, give them
|
|
1020
1184
|
// a new one.
|
|
1021
|
-
this.login(user, fromAuthHeader);
|
|
1185
|
+
await this.login(user, fromAuthHeader);
|
|
1022
1186
|
} else {
|
|
1023
|
-
this.fillSession(user);
|
|
1187
|
+
await this.fillSession(user);
|
|
1024
1188
|
}
|
|
1189
|
+
|
|
1190
|
+
if (this.response) {
|
|
1191
|
+
this.response.locals.user = user;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1025
1194
|
return true;
|
|
1026
1195
|
}
|
|
1027
1196
|
|
|
@@ -1029,27 +1198,87 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
1029
1198
|
* Logs the given user into the system.
|
|
1030
1199
|
*
|
|
1031
1200
|
* @param user The user.
|
|
1032
|
-
* @param sendAuthHeader
|
|
1201
|
+
* @param sendAuthHeader Send the auth token as a custom header.
|
|
1202
|
+
* @param sendCookie Send the auth token as a cookie.
|
|
1033
1203
|
* @returns True on success, false on failure.
|
|
1034
1204
|
*/
|
|
1035
|
-
public login(
|
|
1205
|
+
public async login(
|
|
1206
|
+
user: User & UserData,
|
|
1207
|
+
sendAuthHeader: boolean,
|
|
1208
|
+
sendCookie = true,
|
|
1209
|
+
) {
|
|
1036
1210
|
if (user.guid != null && user.enabled) {
|
|
1037
|
-
if (this.response
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1211
|
+
if (this.response) {
|
|
1212
|
+
if (!this.response.headersSent) {
|
|
1213
|
+
const token = this.config.jwtBuilder(this.config, user);
|
|
1214
|
+
|
|
1215
|
+
if (sendCookie) {
|
|
1216
|
+
const appUrl = new URL(this.config.appUrl);
|
|
1217
|
+
this.response.cookie('TILMELDAUTH', token, {
|
|
1218
|
+
domain: this.config.cookieDomain,
|
|
1219
|
+
path: this.config.cookiePath,
|
|
1220
|
+
maxAge: this.config.jwtExpire * 1000,
|
|
1221
|
+
secure: appUrl.protocol === 'https:',
|
|
1222
|
+
httpOnly: false, // Allow JS access (for CSRF protection).
|
|
1223
|
+
sameSite: appUrl.protocol === 'https:' ? 'lax' : 'strict',
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
if (sendAuthHeader) {
|
|
1228
|
+
this.response.set('X-TILMELDAUTH', token);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
this.response.locals.user = user;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
await this.fillSession(user);
|
|
1236
|
+
return true;
|
|
1237
|
+
}
|
|
1238
|
+
return false;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/**
|
|
1242
|
+
* Adds a switch auth token for the given user.
|
|
1243
|
+
*
|
|
1244
|
+
* This effectively logs the current user in to the system as the given user.
|
|
1245
|
+
*
|
|
1246
|
+
* @param user The user.
|
|
1247
|
+
* @param sendAuthHeader Send the auth token as a custom header.
|
|
1248
|
+
* @param sendCookie Send the auth token as a cookie.
|
|
1249
|
+
* @returns True on success, false on failure.
|
|
1250
|
+
*/
|
|
1251
|
+
public async loginSwitch(
|
|
1252
|
+
user: User & UserData,
|
|
1253
|
+
sendAuthHeader: boolean,
|
|
1254
|
+
sendCookie = true,
|
|
1255
|
+
) {
|
|
1256
|
+
if (user.guid != null) {
|
|
1257
|
+
if (this.response) {
|
|
1258
|
+
if (!this.response.headersSent) {
|
|
1259
|
+
const token = this.config.jwtBuilder(this.config, user, true);
|
|
1260
|
+
|
|
1261
|
+
if (sendCookie) {
|
|
1262
|
+
const appUrl = new URL(this.config.appUrl);
|
|
1263
|
+
this.response.cookie('TILMELDSWITCH', token, {
|
|
1264
|
+
domain: this.config.cookieDomain,
|
|
1265
|
+
path: this.config.cookiePath,
|
|
1266
|
+
maxAge: this.config.jwtSwitchExpire * 1000,
|
|
1267
|
+
secure: appUrl.protocol === 'https:',
|
|
1268
|
+
httpOnly: false, // Allow JS access (for PubSub auth token).
|
|
1269
|
+
sameSite: appUrl.protocol === 'https:' ? 'lax' : 'strict',
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
if (sendAuthHeader) {
|
|
1274
|
+
this.response.set('X-TILMELDSWITCH', token);
|
|
1275
|
+
}
|
|
1050
1276
|
}
|
|
1277
|
+
|
|
1278
|
+
this.response.locals.user = user;
|
|
1051
1279
|
}
|
|
1052
|
-
|
|
1280
|
+
|
|
1281
|
+
await this.fillSession(user);
|
|
1053
1282
|
return true;
|
|
1054
1283
|
}
|
|
1055
1284
|
return false;
|
|
@@ -1057,15 +1286,50 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
1057
1286
|
|
|
1058
1287
|
/**
|
|
1059
1288
|
* Logs the current user out of the system.
|
|
1289
|
+
*
|
|
1290
|
+
* @param clearCookie Clear the auth cookie. (Also send a header.)
|
|
1060
1291
|
*/
|
|
1061
|
-
public logout() {
|
|
1292
|
+
public async logout(clearCookie = true) {
|
|
1293
|
+
if (this.request && !this.alreadyLoggedOutSwitch) {
|
|
1294
|
+
const cookies = this.request.cookies ?? {};
|
|
1295
|
+
|
|
1296
|
+
if ('TILMELDSWITCH' in cookies) {
|
|
1297
|
+
this.alreadyLoggedOutSwitch = true;
|
|
1298
|
+
return await this.logoutSwitch(clearCookie);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1062
1302
|
this.clearSession();
|
|
1063
|
-
if (this.response
|
|
1064
|
-
this.response.
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1303
|
+
if (this.response) {
|
|
1304
|
+
if (clearCookie && !this.response.headersSent) {
|
|
1305
|
+
this.response.clearCookie('TILMELDAUTH', {
|
|
1306
|
+
domain: this.config.cookieDomain,
|
|
1307
|
+
path: this.config.cookiePath,
|
|
1308
|
+
});
|
|
1309
|
+
this.response.set('X-TILMELDAUTH', '');
|
|
1310
|
+
}
|
|
1311
|
+
this.response.locals.user = null;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* Clears the switch user out of the system.
|
|
1317
|
+
*
|
|
1318
|
+
* @param clearCookie Clear the auth cookie. (Also send a header.)
|
|
1319
|
+
*/
|
|
1320
|
+
public async logoutSwitch(clearCookie = true) {
|
|
1321
|
+
this.clearSession();
|
|
1322
|
+
if (this.response) {
|
|
1323
|
+
if (clearCookie && !this.response.headersSent) {
|
|
1324
|
+
this.response.clearCookie('TILMELDSWITCH', {
|
|
1325
|
+
domain: this.config.cookieDomain,
|
|
1326
|
+
path: this.config.cookiePath,
|
|
1327
|
+
});
|
|
1328
|
+
this.response.set('X-TILMELDSWITCH', '');
|
|
1329
|
+
}
|
|
1330
|
+
this.response.locals.user = null;
|
|
1331
|
+
|
|
1332
|
+
await this.authenticate(false, true);
|
|
1069
1333
|
}
|
|
1070
1334
|
}
|
|
1071
1335
|
|