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