@nymphjs/tilmeld 1.0.0-beta.9 → 1.0.0-beta.91
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 +414 -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 +181 -15
- package/dist/Group.js +551 -192
- package/dist/Group.js.map +1 -1
- package/dist/Group.test.js +83 -13
- package/dist/Group.test.js.map +1 -1
- package/dist/Tilmeld.d.ts +241 -12
- package/dist/Tilmeld.js +636 -254
- package/dist/Tilmeld.js.map +1 -1
- package/dist/Tilmeld.types.d.ts +2 -2
- package/dist/Tilmeld.types.js +1 -2
- package/dist/User.d.ts +395 -22
- package/dist/User.js +1316 -365
- package/dist/User.js.map +1 -1
- package/dist/User.test.js +132 -13
- package/dist/User.test.js.map +1 -1
- package/dist/conf/d.d.ts +287 -7
- package/dist/conf/d.js +1 -2
- package/dist/conf/defaults.d.ts +1 -1
- package/dist/conf/defaults.js +188 -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 +11 -10
- package/dist/index.js +16 -35
- package/dist/index.js.map +1 -1
- package/dist/tilmeldJoiProps.js +13 -18
- package/dist/tilmeldJoiProps.js.map +1 -1
- package/jest.config.js +11 -2
- package/package.json +34 -31
- package/src/AbleObject.ts +2 -2
- package/src/Group.test.ts +104 -6
- package/src/Group.ts +661 -226
- package/src/Tilmeld.ts +420 -254
- package/src/Tilmeld.types.ts +2 -2
- package/src/User.test.ts +163 -6
- package/src/User.ts +1557 -454
- package/src/conf/d.ts +63 -17
- package/src/conf/defaults.ts +162 -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 +12 -10
- package/src/tilmeldJoiProps.ts +11 -11
- 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,11 @@ 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
|
-
const entity = nymph.
|
|
278
|
+
const entity = await nymph.getEntity(
|
|
221
279
|
{ class: nymph.getEntityClass(className ?? 'Entity') },
|
|
222
|
-
{ type: '&', guid: guid }
|
|
280
|
+
{ type: '&', guid: guid },
|
|
223
281
|
);
|
|
224
282
|
if (entity != null) {
|
|
225
283
|
checkPermissionsDelete(nymph, entity);
|
|
@@ -231,9 +289,9 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
231
289
|
// disallowed changes to AC properties.
|
|
232
290
|
const checkPermissionsSaveAndFilterAcChanges = async function (
|
|
233
291
|
nymph: Nymph,
|
|
234
|
-
entity: EntityInterface & AccessControlData
|
|
292
|
+
entity: EntityInterface & AccessControlData,
|
|
235
293
|
) {
|
|
236
|
-
const tilmeld = nymph
|
|
294
|
+
const tilmeld = enforceTilmeld(nymph);
|
|
237
295
|
if (!entity) {
|
|
238
296
|
return;
|
|
239
297
|
}
|
|
@@ -249,35 +307,9 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
249
307
|
// allowing a change to ac properties.
|
|
250
308
|
|
|
251
309
|
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
|
-
};
|
|
310
|
+
const newAc = entity.$getCurrentAcValues();
|
|
262
311
|
|
|
263
|
-
|
|
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);
|
|
276
|
-
|
|
277
|
-
function areAcPropertiesChanged(
|
|
278
|
-
a: AccessControlData,
|
|
279
|
-
b: AccessControlData
|
|
280
|
-
) {
|
|
312
|
+
function areAcPropertiesChanged(a: ACProperties, b: ACProperties) {
|
|
281
313
|
for (const name of ['user', 'group'] as ('user' | 'group')[]) {
|
|
282
314
|
const aVal = a[name];
|
|
283
315
|
const bVal = b[name];
|
|
@@ -286,7 +318,7 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
286
318
|
} else if (aVal == null || bVal == null) {
|
|
287
319
|
return true;
|
|
288
320
|
}
|
|
289
|
-
if (
|
|
321
|
+
if (aVal !== bVal) {
|
|
290
322
|
return true;
|
|
291
323
|
}
|
|
292
324
|
}
|
|
@@ -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
494
|
if (!(value instanceof User || value instanceof Group)) {
|
|
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 (
|
|
@@ -579,33 +598,23 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
579
598
|
];
|
|
580
599
|
const groupRefs: FormattedSelector['ref'] = [];
|
|
581
600
|
const acRefs: FormattedSelector['ref'] = [];
|
|
582
|
-
|
|
601
|
+
const gid = user.$getGid();
|
|
602
|
+
if (gid != null) {
|
|
583
603
|
// It belongs to the user's primary group.
|
|
584
|
-
groupRefs.push(['group',
|
|
604
|
+
groupRefs.push(['group', gid]);
|
|
585
605
|
// User's primary group is listed in acRead, acWrite, or acFull.
|
|
586
|
-
acRefs.push(['acRead',
|
|
587
|
-
acRefs.push(['acWrite',
|
|
588
|
-
acRefs.push(['acFull',
|
|
606
|
+
acRefs.push(['acRead', gid]);
|
|
607
|
+
acRefs.push(['acWrite', gid]);
|
|
608
|
+
acRefs.push(['acFull', gid]);
|
|
589
609
|
}
|
|
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
|
-
}
|
|
610
|
+
const gids = user.$getGids();
|
|
611
|
+
for (let curGid of gids ?? []) {
|
|
612
|
+
// It belongs to the user's secondary group.
|
|
613
|
+
groupRefs.push(['group', curGid]);
|
|
614
|
+
// User's secondary group is listed in acRead, acWrite, or acFull.
|
|
615
|
+
acRefs.push(['acRead', curGid]);
|
|
616
|
+
acRefs.push(['acWrite', curGid]);
|
|
617
|
+
acRefs.push(['acFull', curGid]);
|
|
609
618
|
}
|
|
610
619
|
// All the group refs.
|
|
611
620
|
if (groupRefs.length) {
|
|
@@ -671,7 +680,7 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
671
680
|
* - FULL_ACCESS, the user has read, write, and delete access to the entity,
|
|
672
681
|
* as well as being able to manage its access controls and ownership.
|
|
673
682
|
*
|
|
674
|
-
* These properties
|
|
683
|
+
* These properties default to:
|
|
675
684
|
*
|
|
676
685
|
* - acUser = TilmeldAccessLevels.FULL_ACCESS
|
|
677
686
|
* - acGroup = TilmeldAccessLevels.READ_ACCESS
|
|
@@ -686,9 +695,9 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
686
695
|
* The following conditions will result in different checks, which determine
|
|
687
696
|
* whether the check passes:
|
|
688
697
|
*
|
|
689
|
-
* - The user has the "system/admin" ability. (Always true.)
|
|
690
698
|
* - It is a user or group. (True for READ_ACCESS or Tilmeld admins.)
|
|
691
|
-
* -
|
|
699
|
+
* - It is a user or group that belongs to a domain the current user is an
|
|
700
|
+
* admin for, and domain support is enabled. (Always true.)
|
|
692
701
|
* - No user is logged in. (Check other AC.)
|
|
693
702
|
* - The entity is the user. (Always true.)
|
|
694
703
|
* - It is the user's primary group. (True for READ_ACCESS.)
|
|
@@ -706,16 +715,17 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
706
715
|
* @param entity The entity to check.
|
|
707
716
|
* @param type The lowest level of permission to consider a pass.
|
|
708
717
|
* @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
|
|
718
|
+
* @param acProperties The acProperties to use instead of getting them from the entity.
|
|
709
719
|
* @returns Whether the current user has at least `type` permission for the entity.
|
|
710
720
|
*/
|
|
711
721
|
public checkPermissions(
|
|
712
|
-
entity: EntityInterface
|
|
722
|
+
entity: EntityInterface,
|
|
713
723
|
type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
|
|
714
|
-
user?: (User & UserData) | false
|
|
724
|
+
user?: (User & UserData) | false,
|
|
725
|
+
acProperties?: ACProperties,
|
|
715
726
|
) {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
return false;
|
|
727
|
+
if (!acProperties) {
|
|
728
|
+
acProperties = entity.$getCurrentAcValues() as ACProperties;
|
|
719
729
|
}
|
|
720
730
|
|
|
721
731
|
let userOrNull: (User & UserData) | null = null;
|
|
@@ -729,30 +739,43 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
729
739
|
userOrEmpty = user;
|
|
730
740
|
}
|
|
731
741
|
|
|
732
|
-
if (userOrEmpty.$gatekeeper('system/admin')) {
|
|
733
|
-
return true;
|
|
734
|
-
}
|
|
735
|
-
|
|
736
742
|
// Users and groups are always readable. Editable by Tilmeld admins.
|
|
737
743
|
if (
|
|
738
744
|
(entity instanceof User || entity instanceof Group) &&
|
|
739
745
|
(type === TilmeldAccessLevels.READ_ACCESS ||
|
|
740
|
-
userOrEmpty
|
|
746
|
+
userOrEmpty.abilities?.includes('tilmeld/admin') ||
|
|
747
|
+
userOrEmpty.abilities?.includes('system/admin'))
|
|
741
748
|
) {
|
|
742
749
|
return true;
|
|
743
750
|
}
|
|
744
751
|
|
|
745
|
-
//
|
|
746
|
-
|
|
747
|
-
|
|
752
|
+
// Users and groups that belong to a domain are readable and editable by
|
|
753
|
+
// that domain's admins.
|
|
754
|
+
if (
|
|
755
|
+
this.config.domainSupport &&
|
|
756
|
+
(entity instanceof User || entity instanceof Group)
|
|
757
|
+
) {
|
|
758
|
+
const [_local, domain] = splitn(
|
|
759
|
+
(entity instanceof User
|
|
760
|
+
? (entity as User & UserData).username
|
|
761
|
+
: (entity as Group & GroupData).groupname) ?? '',
|
|
762
|
+
'@',
|
|
763
|
+
2,
|
|
764
|
+
);
|
|
765
|
+
if (
|
|
766
|
+
domain &&
|
|
767
|
+
userOrEmpty.abilities?.includes(`tilmeld/domain/${domain}/admin`)
|
|
768
|
+
) {
|
|
769
|
+
return true;
|
|
770
|
+
}
|
|
748
771
|
}
|
|
749
772
|
|
|
750
773
|
// Load access control, since we need it now...
|
|
751
|
-
const acUser =
|
|
752
|
-
const acGroup =
|
|
753
|
-
const acOther =
|
|
774
|
+
const acUser = acProperties.acUser ?? TilmeldAccessLevels.FULL_ACCESS;
|
|
775
|
+
const acGroup = acProperties.acGroup ?? TilmeldAccessLevels.READ_ACCESS;
|
|
776
|
+
const acOther = acProperties.acOther ?? TilmeldAccessLevels.NO_ACCESS;
|
|
754
777
|
|
|
755
|
-
if (userOrNull
|
|
778
|
+
if (userOrNull == null) {
|
|
756
779
|
return acOther >= type;
|
|
757
780
|
}
|
|
758
781
|
|
|
@@ -762,45 +785,41 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
762
785
|
}
|
|
763
786
|
|
|
764
787
|
// Check if the entity is the user's group. Always readable.
|
|
788
|
+
const gid = userOrEmpty.$getGid();
|
|
765
789
|
if (
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
userOrEmpty.group.$is(entity) &&
|
|
790
|
+
gid != null &&
|
|
791
|
+
entity.guid === gid &&
|
|
769
792
|
type === TilmeldAccessLevels.READ_ACCESS
|
|
770
793
|
) {
|
|
771
794
|
return true;
|
|
772
795
|
}
|
|
773
796
|
|
|
774
797
|
// Calculate all the groups the user belongs to.
|
|
775
|
-
let
|
|
776
|
-
|
|
777
|
-
allGroups = allGroups.concat(userOrEmpty.$getDescendantGroupsSync());
|
|
798
|
+
let allGids = gid == null ? [] : [gid];
|
|
799
|
+
allGids = allGids.concat(userOrEmpty.$getGids() ?? []);
|
|
778
800
|
|
|
779
801
|
// Check access ac properties.
|
|
780
802
|
const checks = [
|
|
781
803
|
{
|
|
782
804
|
type: TilmeldAccessLevels.FULL_ACCESS,
|
|
783
|
-
array: [...(
|
|
805
|
+
array: [...(acProperties.acFull ?? [])],
|
|
784
806
|
},
|
|
785
807
|
{
|
|
786
808
|
type: TilmeldAccessLevels.WRITE_ACCESS,
|
|
787
|
-
array: [...(
|
|
809
|
+
array: [...(acProperties.acWrite ?? [])],
|
|
788
810
|
},
|
|
789
811
|
{
|
|
790
812
|
type: TilmeldAccessLevels.READ_ACCESS,
|
|
791
|
-
array: [...(
|
|
813
|
+
array: [...(acProperties.acRead ?? [])],
|
|
792
814
|
},
|
|
793
815
|
];
|
|
794
816
|
for (let curCheck of checks) {
|
|
795
817
|
if (type <= curCheck.type) {
|
|
796
|
-
if (
|
|
818
|
+
if (curCheck.array.includes(userOrEmpty.guid)) {
|
|
797
819
|
return true;
|
|
798
820
|
}
|
|
799
|
-
for (let
|
|
800
|
-
if (
|
|
801
|
-
typeof curGroup.$inArray === 'function' &&
|
|
802
|
-
curGroup.$inArray(curCheck.array)
|
|
803
|
-
) {
|
|
821
|
+
for (let curGid of allGids) {
|
|
822
|
+
if (curCheck.array.includes(curGid)) {
|
|
804
823
|
return true;
|
|
805
824
|
}
|
|
806
825
|
}
|
|
@@ -808,18 +827,10 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
808
827
|
}
|
|
809
828
|
|
|
810
829
|
// Check ownership ac properties.
|
|
811
|
-
if (
|
|
812
|
-
entity.user != null &&
|
|
813
|
-
typeof entity.user.$is === 'function' &&
|
|
814
|
-
entity.user.$is(userOrNull)
|
|
815
|
-
) {
|
|
830
|
+
if (acProperties.user != null && acProperties.user === userOrNull.guid) {
|
|
816
831
|
return acUser >= type;
|
|
817
832
|
}
|
|
818
|
-
if (
|
|
819
|
-
entity.group != null &&
|
|
820
|
-
typeof entity.group.$inArray === 'function' &&
|
|
821
|
-
entity.group.$inArray(allGroups)
|
|
822
|
-
) {
|
|
833
|
+
if (acProperties.group != null && allGids.includes(acProperties.group)) {
|
|
823
834
|
return acGroup >= type;
|
|
824
835
|
}
|
|
825
836
|
return acOther >= type;
|
|
@@ -849,10 +860,10 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
849
860
|
* @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
|
|
850
861
|
* @returns Whether the current user has at least `type` permission for the UID.
|
|
851
862
|
*/
|
|
852
|
-
public checkClientUIDPermissions(
|
|
863
|
+
public async checkClientUIDPermissions(
|
|
853
864
|
name: string,
|
|
854
865
|
type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
|
|
855
|
-
user?: (User & UserData) | false
|
|
866
|
+
user?: (User & UserData) | false,
|
|
856
867
|
) {
|
|
857
868
|
let userOrEmpty: User & UserData = this.User.factorySync();
|
|
858
869
|
// Calculate the user.
|
|
@@ -862,30 +873,30 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
862
873
|
userOrEmpty = user;
|
|
863
874
|
}
|
|
864
875
|
|
|
865
|
-
if (userOrEmpty
|
|
876
|
+
if (userOrEmpty.abilities?.includes('system/admin')) {
|
|
866
877
|
return true;
|
|
867
878
|
}
|
|
868
879
|
|
|
869
880
|
if (type === TilmeldAccessLevels.FULL_ACCESS) {
|
|
870
881
|
return (
|
|
871
|
-
this.config.clientSetabledUIDs.
|
|
872
|
-
userOrEmpty.$gatekeeper(`uid/set/${name}`)
|
|
882
|
+
this.config.clientSetabledUIDs.includes(name) ||
|
|
883
|
+
(await userOrEmpty.$gatekeeper(`uid/set/${name}`))
|
|
873
884
|
);
|
|
874
885
|
} else if (type === TilmeldAccessLevels.WRITE_ACCESS) {
|
|
875
886
|
return (
|
|
876
|
-
this.config.clientEnabledUIDs.
|
|
877
|
-
this.config.clientSetabledUIDs.
|
|
878
|
-
userOrEmpty.$gatekeeper(`uid/new/${name}`) ||
|
|
879
|
-
userOrEmpty.$gatekeeper(`uid/set/${name}`)
|
|
887
|
+
this.config.clientEnabledUIDs.includes(name) ||
|
|
888
|
+
this.config.clientSetabledUIDs.includes(name) ||
|
|
889
|
+
(await userOrEmpty.$gatekeeper(`uid/new/${name}`)) ||
|
|
890
|
+
(await userOrEmpty.$gatekeeper(`uid/set/${name}`))
|
|
880
891
|
);
|
|
881
892
|
} else if (type === TilmeldAccessLevels.READ_ACCESS) {
|
|
882
893
|
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}`)
|
|
894
|
+
this.config.clientReadableUIDs.includes(name) ||
|
|
895
|
+
this.config.clientEnabledUIDs.includes(name) ||
|
|
896
|
+
this.config.clientSetabledUIDs.includes(name) ||
|
|
897
|
+
(await userOrEmpty.$gatekeeper(`uid/get/${name}`)) ||
|
|
898
|
+
(await userOrEmpty.$gatekeeper(`uid/new/${name}`)) ||
|
|
899
|
+
(await userOrEmpty.$gatekeeper(`uid/set/${name}`))
|
|
889
900
|
);
|
|
890
901
|
}
|
|
891
902
|
|
|
@@ -897,20 +908,13 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
897
908
|
*
|
|
898
909
|
* @param user The user.
|
|
899
910
|
*/
|
|
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;
|
|
911
|
+
public async fillSession(user: User & UserData) {
|
|
905
912
|
this.currentUser = user;
|
|
913
|
+
this.gatekeeperCache = await user.$getGatekeeperCache();
|
|
914
|
+
|
|
906
915
|
// Now update the data protection on the user and all the groups.
|
|
907
916
|
this.currentUser.$updateDataProtection();
|
|
908
|
-
|
|
909
|
-
this.currentUser.group.$updateDataProtection();
|
|
910
|
-
}
|
|
911
|
-
for (let group of this.currentUser.groups ?? []) {
|
|
912
|
-
group.$updateDataProtection();
|
|
913
|
-
}
|
|
917
|
+
this.currentUser.$updateGroupDataProtection();
|
|
914
918
|
}
|
|
915
919
|
|
|
916
920
|
/**
|
|
@@ -919,14 +923,10 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
919
923
|
public clearSession() {
|
|
920
924
|
const user = this.currentUser;
|
|
921
925
|
this.currentUser = null;
|
|
926
|
+
this.gatekeeperCache = null;
|
|
922
927
|
if (user) {
|
|
923
928
|
user.$updateDataProtection();
|
|
924
|
-
|
|
925
|
-
user.group.$updateDataProtection();
|
|
926
|
-
}
|
|
927
|
-
for (let group of user.groups ?? []) {
|
|
928
|
-
group.$updateDataProtection();
|
|
929
|
-
}
|
|
929
|
+
user.$updateGroupDataProtection();
|
|
930
930
|
}
|
|
931
931
|
}
|
|
932
932
|
|
|
@@ -937,6 +937,7 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
937
937
|
* @returns The user on success, null on failure.
|
|
938
938
|
*/
|
|
939
939
|
public async extractToken(token: string) {
|
|
940
|
+
this.nymph.config.debugInfo('tilmeld', 'Extracting and validating token.');
|
|
940
941
|
const extract = this.config.jwtExtract(this.config, token);
|
|
941
942
|
if (extract == null) {
|
|
942
943
|
return null;
|
|
@@ -948,9 +949,9 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
948
949
|
{
|
|
949
950
|
type: '&',
|
|
950
951
|
guid: guid,
|
|
951
|
-
}
|
|
952
|
+
},
|
|
952
953
|
);
|
|
953
|
-
if (!user || !user.guid
|
|
954
|
+
if (!user || !user.guid) {
|
|
954
955
|
return null;
|
|
955
956
|
}
|
|
956
957
|
|
|
@@ -958,40 +959,73 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
958
959
|
}
|
|
959
960
|
|
|
960
961
|
/**
|
|
961
|
-
* Check for
|
|
962
|
+
* Check for TILMELDAUTH and TILMELDSWITCH tokens, and, if set, authenticate
|
|
963
|
+
* from it/them.
|
|
964
|
+
*
|
|
965
|
+
* You can also call this function after setting `response.locals.user` to the
|
|
966
|
+
* user you want to authenticate. You *should* check for `user.enabled` before
|
|
967
|
+
* setting this variable, unless you explicitly want to log in as a disabled
|
|
968
|
+
* user. (The user must be an instance of the User class for this Tilmeld
|
|
969
|
+
* instance.)
|
|
970
|
+
*
|
|
971
|
+
* This function will set `response.locals.user` to the logged in user on
|
|
972
|
+
* successful authentication.
|
|
962
973
|
*
|
|
963
974
|
* @param skipXsrfToken Skip the XSRF token check.
|
|
975
|
+
* @param skipRenew Skip the token renewal step, even if the token is close to expiration.
|
|
964
976
|
* @returns True if a user was authenticated, false on any failure.
|
|
965
977
|
*/
|
|
966
|
-
public authenticate(skipXsrfToken = false) {
|
|
978
|
+
public async authenticate(skipXsrfToken = false, skipRenew = false) {
|
|
967
979
|
if (this.request == null) {
|
|
968
980
|
return false;
|
|
969
981
|
}
|
|
970
982
|
|
|
983
|
+
if (
|
|
984
|
+
this.response &&
|
|
985
|
+
this.response.locals.user &&
|
|
986
|
+
this.response.locals.user instanceof this.User
|
|
987
|
+
) {
|
|
988
|
+
// The user has already been authenticated through some other means.
|
|
989
|
+
const user = this.response.locals.user;
|
|
990
|
+
await this.fillSession(user);
|
|
991
|
+
return true;
|
|
992
|
+
}
|
|
993
|
+
|
|
971
994
|
const cookies = this.request.cookies ?? {};
|
|
972
995
|
|
|
973
996
|
// If a client does't support cookies, they can use the X-TILMELDAUTH header
|
|
974
997
|
// to provide the auth token.
|
|
975
998
|
let fromAuthHeader = false;
|
|
976
999
|
let authToken: string;
|
|
1000
|
+
let switchToken: string | null;
|
|
977
1001
|
if (this.request.header('x-tilmeldauth') != null) {
|
|
978
1002
|
fromAuthHeader = true;
|
|
979
1003
|
authToken = this.request.header('x-tilmeldauth') as string;
|
|
1004
|
+
switchToken =
|
|
1005
|
+
(this.request.header('x-tilmeldswitch') as string) ||
|
|
1006
|
+
'TILMELDSWITCH' in cookies
|
|
1007
|
+
? cookies.TILMELDSWITCH
|
|
1008
|
+
: null;
|
|
980
1009
|
} else if ('TILMELDAUTH' in cookies) {
|
|
981
1010
|
fromAuthHeader = false;
|
|
982
1011
|
authToken = cookies.TILMELDAUTH;
|
|
1012
|
+
switchToken = 'TILMELDSWITCH' in cookies ? cookies.TILMELDSWITCH : null;
|
|
983
1013
|
} else {
|
|
984
1014
|
return false;
|
|
985
1015
|
}
|
|
986
1016
|
|
|
987
|
-
let
|
|
1017
|
+
let authExtract: { guid: string; issued: Date; expire: Date } | null;
|
|
1018
|
+
let switchExtract: { guid: string; expire: Date } | null;
|
|
988
1019
|
if (
|
|
989
1020
|
skipXsrfToken ||
|
|
990
1021
|
this.request.originalUrl.startsWith(this.config.setupPath)
|
|
991
1022
|
) {
|
|
992
1023
|
// The request is for the setup app, or we were told to skip the XSRF
|
|
993
1024
|
// check, so don't check for the XSRF token.
|
|
994
|
-
|
|
1025
|
+
authExtract = this.config.jwtExtract(this.config, authToken);
|
|
1026
|
+
switchExtract = switchToken
|
|
1027
|
+
? this.config.jwtExtract(this.config, switchToken)
|
|
1028
|
+
: null;
|
|
995
1029
|
} else {
|
|
996
1030
|
// The request is for something else, so check for a valid XSRF token,
|
|
997
1031
|
// unless the auth token is provided by a header (instead of a cookie).
|
|
@@ -1000,28 +1034,65 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
1000
1034
|
return false;
|
|
1001
1035
|
}
|
|
1002
1036
|
|
|
1003
|
-
|
|
1037
|
+
authExtract = this.config.jwtExtract(this.config, authToken, xsrfToken);
|
|
1038
|
+
switchExtract = switchToken
|
|
1039
|
+
? this.config.jwtExtract(this.config, switchToken)
|
|
1040
|
+
: null;
|
|
1004
1041
|
}
|
|
1005
1042
|
|
|
1006
|
-
if (
|
|
1007
|
-
this.logout();
|
|
1043
|
+
if (authExtract == null) {
|
|
1044
|
+
await this.logout();
|
|
1008
1045
|
return false;
|
|
1009
1046
|
}
|
|
1010
|
-
const { guid, expire } =
|
|
1047
|
+
const { guid, issued, expire } = authExtract;
|
|
1048
|
+
const { guid: switchGuid, expire: switchExpire } = switchExtract || {
|
|
1049
|
+
guid: null,
|
|
1050
|
+
expire: null,
|
|
1051
|
+
};
|
|
1011
1052
|
|
|
1012
|
-
const user = this.User.
|
|
1013
|
-
if (
|
|
1014
|
-
|
|
1053
|
+
const user = await this.User.factory(guid);
|
|
1054
|
+
if (
|
|
1055
|
+
user.guid == null ||
|
|
1056
|
+
!user.enabled ||
|
|
1057
|
+
expire.getTime() <= Date.now() ||
|
|
1058
|
+
issued.getTime() > Date.now() ||
|
|
1059
|
+
(user.revokeTokenDate != null && issued.getTime() <= user.revokeTokenDate)
|
|
1060
|
+
) {
|
|
1061
|
+
await this.logout();
|
|
1015
1062
|
return false;
|
|
1016
1063
|
}
|
|
1017
1064
|
|
|
1018
|
-
if (
|
|
1065
|
+
if (switchGuid && switchExpire && switchExpire.getTime() > Date.now()) {
|
|
1066
|
+
// Load the switch user.
|
|
1067
|
+
const switchUser = await this.User.factory(switchGuid);
|
|
1068
|
+
if (switchUser.guid == null) {
|
|
1069
|
+
await this.logoutSwitch();
|
|
1070
|
+
} else {
|
|
1071
|
+
await this.fillSession(switchUser);
|
|
1072
|
+
|
|
1073
|
+
if (this.response) {
|
|
1074
|
+
this.response.locals.user = switchUser;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
return true;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
if (
|
|
1082
|
+
!skipRenew &&
|
|
1083
|
+
expire.getTime() < Date.now() + this.config.jwtRenew * 1000
|
|
1084
|
+
) {
|
|
1019
1085
|
// If the user is less than renew time from needing a new token, give them
|
|
1020
1086
|
// a new one.
|
|
1021
|
-
this.login(user, fromAuthHeader);
|
|
1087
|
+
await this.login(user, fromAuthHeader);
|
|
1022
1088
|
} else {
|
|
1023
|
-
this.fillSession(user);
|
|
1089
|
+
await this.fillSession(user);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
if (this.response) {
|
|
1093
|
+
this.response.locals.user = user;
|
|
1024
1094
|
}
|
|
1095
|
+
|
|
1025
1096
|
return true;
|
|
1026
1097
|
}
|
|
1027
1098
|
|
|
@@ -1029,27 +1100,87 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
1029
1100
|
* Logs the given user into the system.
|
|
1030
1101
|
*
|
|
1031
1102
|
* @param user The user.
|
|
1032
|
-
* @param sendAuthHeader
|
|
1103
|
+
* @param sendAuthHeader Send the auth token as a custom header.
|
|
1104
|
+
* @param sendCookie Send the auth token as a cookie.
|
|
1033
1105
|
* @returns True on success, false on failure.
|
|
1034
1106
|
*/
|
|
1035
|
-
public login(
|
|
1107
|
+
public async login(
|
|
1108
|
+
user: User & UserData,
|
|
1109
|
+
sendAuthHeader: boolean,
|
|
1110
|
+
sendCookie = true,
|
|
1111
|
+
) {
|
|
1036
1112
|
if (user.guid != null && user.enabled) {
|
|
1037
|
-
if (this.response
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1113
|
+
if (this.response) {
|
|
1114
|
+
if (!this.response.headersSent) {
|
|
1115
|
+
const token = this.config.jwtBuilder(this.config, user);
|
|
1116
|
+
|
|
1117
|
+
if (sendCookie) {
|
|
1118
|
+
const appUrl = new URL(this.config.appUrl);
|
|
1119
|
+
this.response.cookie('TILMELDAUTH', token, {
|
|
1120
|
+
domain: this.config.cookieDomain,
|
|
1121
|
+
path: this.config.cookiePath,
|
|
1122
|
+
maxAge: this.config.jwtExpire * 1000,
|
|
1123
|
+
secure: appUrl.protocol === 'https:',
|
|
1124
|
+
httpOnly: false, // Allow JS access (for CSRF protection).
|
|
1125
|
+
sameSite: appUrl.protocol === 'https:' ? 'lax' : 'strict',
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
if (sendAuthHeader) {
|
|
1130
|
+
this.response.set('X-TILMELDAUTH', token);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
this.response.locals.user = user;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
await this.fillSession(user);
|
|
1138
|
+
return true;
|
|
1139
|
+
}
|
|
1140
|
+
return false;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* Adds a switch auth token for the given user.
|
|
1145
|
+
*
|
|
1146
|
+
* This effectively logs the current user in to the system as the given user.
|
|
1147
|
+
*
|
|
1148
|
+
* @param user The user.
|
|
1149
|
+
* @param sendAuthHeader Send the auth token as a custom header.
|
|
1150
|
+
* @param sendCookie Send the auth token as a cookie.
|
|
1151
|
+
* @returns True on success, false on failure.
|
|
1152
|
+
*/
|
|
1153
|
+
public async loginSwitch(
|
|
1154
|
+
user: User & UserData,
|
|
1155
|
+
sendAuthHeader: boolean,
|
|
1156
|
+
sendCookie = true,
|
|
1157
|
+
) {
|
|
1158
|
+
if (user.guid != null) {
|
|
1159
|
+
if (this.response) {
|
|
1160
|
+
if (!this.response.headersSent) {
|
|
1161
|
+
const token = this.config.jwtBuilder(this.config, user, true);
|
|
1162
|
+
|
|
1163
|
+
if (sendCookie) {
|
|
1164
|
+
const appUrl = new URL(this.config.appUrl);
|
|
1165
|
+
this.response.cookie('TILMELDSWITCH', token, {
|
|
1166
|
+
domain: this.config.cookieDomain,
|
|
1167
|
+
path: this.config.cookiePath,
|
|
1168
|
+
maxAge: this.config.jwtSwitchExpire * 1000,
|
|
1169
|
+
secure: appUrl.protocol === 'https:',
|
|
1170
|
+
httpOnly: false, // Allow JS access (for PubSub auth token).
|
|
1171
|
+
sameSite: appUrl.protocol === 'https:' ? 'lax' : 'strict',
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
if (sendAuthHeader) {
|
|
1176
|
+
this.response.set('X-TILMELDSWITCH', token);
|
|
1177
|
+
}
|
|
1050
1178
|
}
|
|
1179
|
+
|
|
1180
|
+
this.response.locals.user = user;
|
|
1051
1181
|
}
|
|
1052
|
-
|
|
1182
|
+
|
|
1183
|
+
await this.fillSession(user);
|
|
1053
1184
|
return true;
|
|
1054
1185
|
}
|
|
1055
1186
|
return false;
|
|
@@ -1057,15 +1188,50 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
1057
1188
|
|
|
1058
1189
|
/**
|
|
1059
1190
|
* Logs the current user out of the system.
|
|
1191
|
+
*
|
|
1192
|
+
* @param clearCookie Clear the auth cookie. (Also send a header.)
|
|
1193
|
+
*/
|
|
1194
|
+
public async logout(clearCookie = true) {
|
|
1195
|
+
if (this.request && !this.alreadyLoggedOutSwitch) {
|
|
1196
|
+
const cookies = this.request.cookies ?? {};
|
|
1197
|
+
|
|
1198
|
+
if ('TILMELDSWITCH' in cookies) {
|
|
1199
|
+
this.alreadyLoggedOutSwitch = true;
|
|
1200
|
+
return await this.logoutSwitch(clearCookie);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
this.clearSession();
|
|
1205
|
+
if (this.response) {
|
|
1206
|
+
if (clearCookie && !this.response.headersSent) {
|
|
1207
|
+
this.response.clearCookie('TILMELDAUTH', {
|
|
1208
|
+
domain: this.config.cookieDomain,
|
|
1209
|
+
path: this.config.cookiePath,
|
|
1210
|
+
});
|
|
1211
|
+
this.response.set('X-TILMELDAUTH', '');
|
|
1212
|
+
}
|
|
1213
|
+
this.response.locals.user = null;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* Clears the switch user out of the system.
|
|
1219
|
+
*
|
|
1220
|
+
* @param clearCookie Clear the auth cookie. (Also send a header.)
|
|
1060
1221
|
*/
|
|
1061
|
-
public
|
|
1222
|
+
public async logoutSwitch(clearCookie = true) {
|
|
1062
1223
|
this.clearSession();
|
|
1063
|
-
if (this.response
|
|
1064
|
-
this.response.
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1224
|
+
if (this.response) {
|
|
1225
|
+
if (clearCookie && !this.response.headersSent) {
|
|
1226
|
+
this.response.clearCookie('TILMELDSWITCH', {
|
|
1227
|
+
domain: this.config.cookieDomain,
|
|
1228
|
+
path: this.config.cookiePath,
|
|
1229
|
+
});
|
|
1230
|
+
this.response.set('X-TILMELDSWITCH', '');
|
|
1231
|
+
}
|
|
1232
|
+
this.response.locals.user = null;
|
|
1233
|
+
|
|
1234
|
+
await this.authenticate(false, true);
|
|
1069
1235
|
}
|
|
1070
1236
|
}
|
|
1071
1237
|
|