@nymphjs/tilmeld 1.0.0-beta.9 → 1.0.0-beta.90
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 +408 -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 +140 -15
- package/dist/Group.js +466 -191
- 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 +239 -12
- package/dist/Tilmeld.js +634 -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 +365 -22
- package/dist/User.js +1265 -364
- 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 +478 -226
- package/src/Tilmeld.ts +418 -254
- package/src/Tilmeld.types.ts +2 -2
- package/src/User.test.ts +163 -6
- package/src/User.ts +1428 -444
- 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,7 @@ 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
|
-
* - The entity has no "user" and no "group". (Always true.)
|
|
692
699
|
* - No user is logged in. (Check other AC.)
|
|
693
700
|
* - The entity is the user. (Always true.)
|
|
694
701
|
* - It is the user's primary group. (True for READ_ACCESS.)
|
|
@@ -706,16 +713,17 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
706
713
|
* @param entity The entity to check.
|
|
707
714
|
* @param type The lowest level of permission to consider a pass.
|
|
708
715
|
* @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
|
|
716
|
+
* @param acProperties The acProperties to use instead of getting them from the entity.
|
|
709
717
|
* @returns Whether the current user has at least `type` permission for the entity.
|
|
710
718
|
*/
|
|
711
719
|
public checkPermissions(
|
|
712
|
-
entity: EntityInterface
|
|
720
|
+
entity: EntityInterface,
|
|
713
721
|
type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
|
|
714
|
-
user?: (User & UserData) | false
|
|
722
|
+
user?: (User & UserData) | false,
|
|
723
|
+
acProperties?: ACProperties,
|
|
715
724
|
) {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
return false;
|
|
725
|
+
if (!acProperties) {
|
|
726
|
+
acProperties = entity.$getCurrentAcValues() as ACProperties;
|
|
719
727
|
}
|
|
720
728
|
|
|
721
729
|
let userOrNull: (User & UserData) | null = null;
|
|
@@ -729,30 +737,43 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
729
737
|
userOrEmpty = user;
|
|
730
738
|
}
|
|
731
739
|
|
|
732
|
-
if (userOrEmpty.$gatekeeper('system/admin')) {
|
|
733
|
-
return true;
|
|
734
|
-
}
|
|
735
|
-
|
|
736
740
|
// Users and groups are always readable. Editable by Tilmeld admins.
|
|
737
741
|
if (
|
|
738
742
|
(entity instanceof User || entity instanceof Group) &&
|
|
739
743
|
(type === TilmeldAccessLevels.READ_ACCESS ||
|
|
740
|
-
userOrEmpty
|
|
744
|
+
userOrEmpty.abilities?.includes('tilmeld/admin') ||
|
|
745
|
+
userOrEmpty.abilities?.includes('system/admin'))
|
|
741
746
|
) {
|
|
742
747
|
return true;
|
|
743
748
|
}
|
|
744
749
|
|
|
745
|
-
//
|
|
746
|
-
|
|
747
|
-
|
|
750
|
+
// Users and groups that belong to a domain are readable and editable by
|
|
751
|
+
// that domain's admins.
|
|
752
|
+
if (
|
|
753
|
+
this.config.domainSupport &&
|
|
754
|
+
(entity instanceof User || entity instanceof Group)
|
|
755
|
+
) {
|
|
756
|
+
const [_local, domain] = splitn(
|
|
757
|
+
(entity instanceof User
|
|
758
|
+
? (entity as User & UserData).username
|
|
759
|
+
: (entity as Group & GroupData).groupname) ?? '',
|
|
760
|
+
'@',
|
|
761
|
+
2,
|
|
762
|
+
);
|
|
763
|
+
if (
|
|
764
|
+
domain &&
|
|
765
|
+
userOrEmpty.abilities?.includes(`tilmeld/domain/${domain}/admin`)
|
|
766
|
+
) {
|
|
767
|
+
return true;
|
|
768
|
+
}
|
|
748
769
|
}
|
|
749
770
|
|
|
750
771
|
// Load access control, since we need it now...
|
|
751
|
-
const acUser =
|
|
752
|
-
const acGroup =
|
|
753
|
-
const acOther =
|
|
772
|
+
const acUser = acProperties.acUser ?? TilmeldAccessLevels.FULL_ACCESS;
|
|
773
|
+
const acGroup = acProperties.acGroup ?? TilmeldAccessLevels.READ_ACCESS;
|
|
774
|
+
const acOther = acProperties.acOther ?? TilmeldAccessLevels.NO_ACCESS;
|
|
754
775
|
|
|
755
|
-
if (userOrNull
|
|
776
|
+
if (userOrNull == null) {
|
|
756
777
|
return acOther >= type;
|
|
757
778
|
}
|
|
758
779
|
|
|
@@ -762,45 +783,41 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
762
783
|
}
|
|
763
784
|
|
|
764
785
|
// Check if the entity is the user's group. Always readable.
|
|
786
|
+
const gid = userOrEmpty.$getGid();
|
|
765
787
|
if (
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
userOrEmpty.group.$is(entity) &&
|
|
788
|
+
gid != null &&
|
|
789
|
+
entity.guid === gid &&
|
|
769
790
|
type === TilmeldAccessLevels.READ_ACCESS
|
|
770
791
|
) {
|
|
771
792
|
return true;
|
|
772
793
|
}
|
|
773
794
|
|
|
774
795
|
// Calculate all the groups the user belongs to.
|
|
775
|
-
let
|
|
776
|
-
|
|
777
|
-
allGroups = allGroups.concat(userOrEmpty.$getDescendantGroupsSync());
|
|
796
|
+
let allGids = gid == null ? [] : [gid];
|
|
797
|
+
allGids = allGids.concat(userOrEmpty.$getGids() ?? []);
|
|
778
798
|
|
|
779
799
|
// Check access ac properties.
|
|
780
800
|
const checks = [
|
|
781
801
|
{
|
|
782
802
|
type: TilmeldAccessLevels.FULL_ACCESS,
|
|
783
|
-
array: [...(
|
|
803
|
+
array: [...(acProperties.acFull ?? [])],
|
|
784
804
|
},
|
|
785
805
|
{
|
|
786
806
|
type: TilmeldAccessLevels.WRITE_ACCESS,
|
|
787
|
-
array: [...(
|
|
807
|
+
array: [...(acProperties.acWrite ?? [])],
|
|
788
808
|
},
|
|
789
809
|
{
|
|
790
810
|
type: TilmeldAccessLevels.READ_ACCESS,
|
|
791
|
-
array: [...(
|
|
811
|
+
array: [...(acProperties.acRead ?? [])],
|
|
792
812
|
},
|
|
793
813
|
];
|
|
794
814
|
for (let curCheck of checks) {
|
|
795
815
|
if (type <= curCheck.type) {
|
|
796
|
-
if (
|
|
816
|
+
if (curCheck.array.includes(userOrEmpty.guid)) {
|
|
797
817
|
return true;
|
|
798
818
|
}
|
|
799
|
-
for (let
|
|
800
|
-
if (
|
|
801
|
-
typeof curGroup.$inArray === 'function' &&
|
|
802
|
-
curGroup.$inArray(curCheck.array)
|
|
803
|
-
) {
|
|
819
|
+
for (let curGid of allGids) {
|
|
820
|
+
if (curCheck.array.includes(curGid)) {
|
|
804
821
|
return true;
|
|
805
822
|
}
|
|
806
823
|
}
|
|
@@ -808,18 +825,10 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
808
825
|
}
|
|
809
826
|
|
|
810
827
|
// Check ownership ac properties.
|
|
811
|
-
if (
|
|
812
|
-
entity.user != null &&
|
|
813
|
-
typeof entity.user.$is === 'function' &&
|
|
814
|
-
entity.user.$is(userOrNull)
|
|
815
|
-
) {
|
|
828
|
+
if (acProperties.user != null && acProperties.user === userOrNull.guid) {
|
|
816
829
|
return acUser >= type;
|
|
817
830
|
}
|
|
818
|
-
if (
|
|
819
|
-
entity.group != null &&
|
|
820
|
-
typeof entity.group.$inArray === 'function' &&
|
|
821
|
-
entity.group.$inArray(allGroups)
|
|
822
|
-
) {
|
|
831
|
+
if (acProperties.group != null && allGids.includes(acProperties.group)) {
|
|
823
832
|
return acGroup >= type;
|
|
824
833
|
}
|
|
825
834
|
return acOther >= type;
|
|
@@ -849,10 +858,10 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
849
858
|
* @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
|
|
850
859
|
* @returns Whether the current user has at least `type` permission for the UID.
|
|
851
860
|
*/
|
|
852
|
-
public checkClientUIDPermissions(
|
|
861
|
+
public async checkClientUIDPermissions(
|
|
853
862
|
name: string,
|
|
854
863
|
type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
|
|
855
|
-
user?: (User & UserData) | false
|
|
864
|
+
user?: (User & UserData) | false,
|
|
856
865
|
) {
|
|
857
866
|
let userOrEmpty: User & UserData = this.User.factorySync();
|
|
858
867
|
// Calculate the user.
|
|
@@ -862,30 +871,30 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
862
871
|
userOrEmpty = user;
|
|
863
872
|
}
|
|
864
873
|
|
|
865
|
-
if (userOrEmpty
|
|
874
|
+
if (userOrEmpty.abilities?.includes('system/admin')) {
|
|
866
875
|
return true;
|
|
867
876
|
}
|
|
868
877
|
|
|
869
878
|
if (type === TilmeldAccessLevels.FULL_ACCESS) {
|
|
870
879
|
return (
|
|
871
|
-
this.config.clientSetabledUIDs.
|
|
872
|
-
userOrEmpty.$gatekeeper(`uid/set/${name}`)
|
|
880
|
+
this.config.clientSetabledUIDs.includes(name) ||
|
|
881
|
+
(await userOrEmpty.$gatekeeper(`uid/set/${name}`))
|
|
873
882
|
);
|
|
874
883
|
} else if (type === TilmeldAccessLevels.WRITE_ACCESS) {
|
|
875
884
|
return (
|
|
876
|
-
this.config.clientEnabledUIDs.
|
|
877
|
-
this.config.clientSetabledUIDs.
|
|
878
|
-
userOrEmpty.$gatekeeper(`uid/new/${name}`) ||
|
|
879
|
-
userOrEmpty.$gatekeeper(`uid/set/${name}`)
|
|
885
|
+
this.config.clientEnabledUIDs.includes(name) ||
|
|
886
|
+
this.config.clientSetabledUIDs.includes(name) ||
|
|
887
|
+
(await userOrEmpty.$gatekeeper(`uid/new/${name}`)) ||
|
|
888
|
+
(await userOrEmpty.$gatekeeper(`uid/set/${name}`))
|
|
880
889
|
);
|
|
881
890
|
} else if (type === TilmeldAccessLevels.READ_ACCESS) {
|
|
882
891
|
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}`)
|
|
892
|
+
this.config.clientReadableUIDs.includes(name) ||
|
|
893
|
+
this.config.clientEnabledUIDs.includes(name) ||
|
|
894
|
+
this.config.clientSetabledUIDs.includes(name) ||
|
|
895
|
+
(await userOrEmpty.$gatekeeper(`uid/get/${name}`)) ||
|
|
896
|
+
(await userOrEmpty.$gatekeeper(`uid/new/${name}`)) ||
|
|
897
|
+
(await userOrEmpty.$gatekeeper(`uid/set/${name}`))
|
|
889
898
|
);
|
|
890
899
|
}
|
|
891
900
|
|
|
@@ -897,20 +906,13 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
897
906
|
*
|
|
898
907
|
* @param user The user.
|
|
899
908
|
*/
|
|
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;
|
|
909
|
+
public async fillSession(user: User & UserData) {
|
|
905
910
|
this.currentUser = user;
|
|
911
|
+
this.gatekeeperCache = await user.$getGatekeeperCache();
|
|
912
|
+
|
|
906
913
|
// Now update the data protection on the user and all the groups.
|
|
907
914
|
this.currentUser.$updateDataProtection();
|
|
908
|
-
|
|
909
|
-
this.currentUser.group.$updateDataProtection();
|
|
910
|
-
}
|
|
911
|
-
for (let group of this.currentUser.groups ?? []) {
|
|
912
|
-
group.$updateDataProtection();
|
|
913
|
-
}
|
|
915
|
+
this.currentUser.$updateGroupDataProtection();
|
|
914
916
|
}
|
|
915
917
|
|
|
916
918
|
/**
|
|
@@ -919,14 +921,10 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
919
921
|
public clearSession() {
|
|
920
922
|
const user = this.currentUser;
|
|
921
923
|
this.currentUser = null;
|
|
924
|
+
this.gatekeeperCache = null;
|
|
922
925
|
if (user) {
|
|
923
926
|
user.$updateDataProtection();
|
|
924
|
-
|
|
925
|
-
user.group.$updateDataProtection();
|
|
926
|
-
}
|
|
927
|
-
for (let group of user.groups ?? []) {
|
|
928
|
-
group.$updateDataProtection();
|
|
929
|
-
}
|
|
927
|
+
user.$updateGroupDataProtection();
|
|
930
928
|
}
|
|
931
929
|
}
|
|
932
930
|
|
|
@@ -937,6 +935,7 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
937
935
|
* @returns The user on success, null on failure.
|
|
938
936
|
*/
|
|
939
937
|
public async extractToken(token: string) {
|
|
938
|
+
this.nymph.config.debugInfo('tilmeld', 'Extracting and validating token.');
|
|
940
939
|
const extract = this.config.jwtExtract(this.config, token);
|
|
941
940
|
if (extract == null) {
|
|
942
941
|
return null;
|
|
@@ -948,9 +947,9 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
948
947
|
{
|
|
949
948
|
type: '&',
|
|
950
949
|
guid: guid,
|
|
951
|
-
}
|
|
950
|
+
},
|
|
952
951
|
);
|
|
953
|
-
if (!user || !user.guid
|
|
952
|
+
if (!user || !user.guid) {
|
|
954
953
|
return null;
|
|
955
954
|
}
|
|
956
955
|
|
|
@@ -958,40 +957,73 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
958
957
|
}
|
|
959
958
|
|
|
960
959
|
/**
|
|
961
|
-
* Check for
|
|
960
|
+
* Check for TILMELDAUTH and TILMELDSWITCH tokens, and, if set, authenticate
|
|
961
|
+
* from it/them.
|
|
962
|
+
*
|
|
963
|
+
* You can also call this function after setting `response.locals.user` to the
|
|
964
|
+
* user you want to authenticate. You *should* check for `user.enabled` before
|
|
965
|
+
* setting this variable, unless you explicitly want to log in as a disabled
|
|
966
|
+
* user. (The user must be an instance of the User class for this Tilmeld
|
|
967
|
+
* instance.)
|
|
968
|
+
*
|
|
969
|
+
* This function will set `response.locals.user` to the logged in user on
|
|
970
|
+
* successful authentication.
|
|
962
971
|
*
|
|
963
972
|
* @param skipXsrfToken Skip the XSRF token check.
|
|
973
|
+
* @param skipRenew Skip the token renewal step, even if the token is close to expiration.
|
|
964
974
|
* @returns True if a user was authenticated, false on any failure.
|
|
965
975
|
*/
|
|
966
|
-
public authenticate(skipXsrfToken = false) {
|
|
976
|
+
public async authenticate(skipXsrfToken = false, skipRenew = false) {
|
|
967
977
|
if (this.request == null) {
|
|
968
978
|
return false;
|
|
969
979
|
}
|
|
970
980
|
|
|
981
|
+
if (
|
|
982
|
+
this.response &&
|
|
983
|
+
this.response.locals.user &&
|
|
984
|
+
this.response.locals.user instanceof this.User
|
|
985
|
+
) {
|
|
986
|
+
// The user has already been authenticated through some other means.
|
|
987
|
+
const user = this.response.locals.user;
|
|
988
|
+
await this.fillSession(user);
|
|
989
|
+
return true;
|
|
990
|
+
}
|
|
991
|
+
|
|
971
992
|
const cookies = this.request.cookies ?? {};
|
|
972
993
|
|
|
973
994
|
// If a client does't support cookies, they can use the X-TILMELDAUTH header
|
|
974
995
|
// to provide the auth token.
|
|
975
996
|
let fromAuthHeader = false;
|
|
976
997
|
let authToken: string;
|
|
998
|
+
let switchToken: string | null;
|
|
977
999
|
if (this.request.header('x-tilmeldauth') != null) {
|
|
978
1000
|
fromAuthHeader = true;
|
|
979
1001
|
authToken = this.request.header('x-tilmeldauth') as string;
|
|
1002
|
+
switchToken =
|
|
1003
|
+
(this.request.header('x-tilmeldswitch') as string) ||
|
|
1004
|
+
'TILMELDSWITCH' in cookies
|
|
1005
|
+
? cookies.TILMELDSWITCH
|
|
1006
|
+
: null;
|
|
980
1007
|
} else if ('TILMELDAUTH' in cookies) {
|
|
981
1008
|
fromAuthHeader = false;
|
|
982
1009
|
authToken = cookies.TILMELDAUTH;
|
|
1010
|
+
switchToken = 'TILMELDSWITCH' in cookies ? cookies.TILMELDSWITCH : null;
|
|
983
1011
|
} else {
|
|
984
1012
|
return false;
|
|
985
1013
|
}
|
|
986
1014
|
|
|
987
|
-
let
|
|
1015
|
+
let authExtract: { guid: string; issued: Date; expire: Date } | null;
|
|
1016
|
+
let switchExtract: { guid: string; expire: Date } | null;
|
|
988
1017
|
if (
|
|
989
1018
|
skipXsrfToken ||
|
|
990
1019
|
this.request.originalUrl.startsWith(this.config.setupPath)
|
|
991
1020
|
) {
|
|
992
1021
|
// The request is for the setup app, or we were told to skip the XSRF
|
|
993
1022
|
// check, so don't check for the XSRF token.
|
|
994
|
-
|
|
1023
|
+
authExtract = this.config.jwtExtract(this.config, authToken);
|
|
1024
|
+
switchExtract = switchToken
|
|
1025
|
+
? this.config.jwtExtract(this.config, switchToken)
|
|
1026
|
+
: null;
|
|
995
1027
|
} else {
|
|
996
1028
|
// The request is for something else, so check for a valid XSRF token,
|
|
997
1029
|
// unless the auth token is provided by a header (instead of a cookie).
|
|
@@ -1000,28 +1032,65 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
1000
1032
|
return false;
|
|
1001
1033
|
}
|
|
1002
1034
|
|
|
1003
|
-
|
|
1035
|
+
authExtract = this.config.jwtExtract(this.config, authToken, xsrfToken);
|
|
1036
|
+
switchExtract = switchToken
|
|
1037
|
+
? this.config.jwtExtract(this.config, switchToken)
|
|
1038
|
+
: null;
|
|
1004
1039
|
}
|
|
1005
1040
|
|
|
1006
|
-
if (
|
|
1007
|
-
this.logout();
|
|
1041
|
+
if (authExtract == null) {
|
|
1042
|
+
await this.logout();
|
|
1008
1043
|
return false;
|
|
1009
1044
|
}
|
|
1010
|
-
const { guid, expire } =
|
|
1045
|
+
const { guid, issued, expire } = authExtract;
|
|
1046
|
+
const { guid: switchGuid, expire: switchExpire } = switchExtract || {
|
|
1047
|
+
guid: null,
|
|
1048
|
+
expire: null,
|
|
1049
|
+
};
|
|
1011
1050
|
|
|
1012
|
-
const user = this.User.
|
|
1013
|
-
if (
|
|
1014
|
-
|
|
1051
|
+
const user = await this.User.factory(guid);
|
|
1052
|
+
if (
|
|
1053
|
+
user.guid == null ||
|
|
1054
|
+
!user.enabled ||
|
|
1055
|
+
expire.getTime() <= Date.now() ||
|
|
1056
|
+
issued.getTime() > Date.now() ||
|
|
1057
|
+
(user.revokeTokenDate != null && issued.getTime() <= user.revokeTokenDate)
|
|
1058
|
+
) {
|
|
1059
|
+
await this.logout();
|
|
1015
1060
|
return false;
|
|
1016
1061
|
}
|
|
1017
1062
|
|
|
1018
|
-
if (
|
|
1063
|
+
if (switchGuid && switchExpire && switchExpire.getTime() > Date.now()) {
|
|
1064
|
+
// Load the switch user.
|
|
1065
|
+
const switchUser = await this.User.factory(switchGuid);
|
|
1066
|
+
if (switchUser.guid == null) {
|
|
1067
|
+
await this.logoutSwitch();
|
|
1068
|
+
} else {
|
|
1069
|
+
await this.fillSession(switchUser);
|
|
1070
|
+
|
|
1071
|
+
if (this.response) {
|
|
1072
|
+
this.response.locals.user = switchUser;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
return true;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (
|
|
1080
|
+
!skipRenew &&
|
|
1081
|
+
expire.getTime() < Date.now() + this.config.jwtRenew * 1000
|
|
1082
|
+
) {
|
|
1019
1083
|
// If the user is less than renew time from needing a new token, give them
|
|
1020
1084
|
// a new one.
|
|
1021
|
-
this.login(user, fromAuthHeader);
|
|
1085
|
+
await this.login(user, fromAuthHeader);
|
|
1022
1086
|
} else {
|
|
1023
|
-
this.fillSession(user);
|
|
1087
|
+
await this.fillSession(user);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
if (this.response) {
|
|
1091
|
+
this.response.locals.user = user;
|
|
1024
1092
|
}
|
|
1093
|
+
|
|
1025
1094
|
return true;
|
|
1026
1095
|
}
|
|
1027
1096
|
|
|
@@ -1029,27 +1098,87 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
1029
1098
|
* Logs the given user into the system.
|
|
1030
1099
|
*
|
|
1031
1100
|
* @param user The user.
|
|
1032
|
-
* @param sendAuthHeader
|
|
1101
|
+
* @param sendAuthHeader Send the auth token as a custom header.
|
|
1102
|
+
* @param sendCookie Send the auth token as a cookie.
|
|
1033
1103
|
* @returns True on success, false on failure.
|
|
1034
1104
|
*/
|
|
1035
|
-
public login(
|
|
1105
|
+
public async login(
|
|
1106
|
+
user: User & UserData,
|
|
1107
|
+
sendAuthHeader: boolean,
|
|
1108
|
+
sendCookie = true,
|
|
1109
|
+
) {
|
|
1036
1110
|
if (user.guid != null && user.enabled) {
|
|
1037
|
-
if (this.response
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1111
|
+
if (this.response) {
|
|
1112
|
+
if (!this.response.headersSent) {
|
|
1113
|
+
const token = this.config.jwtBuilder(this.config, user);
|
|
1114
|
+
|
|
1115
|
+
if (sendCookie) {
|
|
1116
|
+
const appUrl = new URL(this.config.appUrl);
|
|
1117
|
+
this.response.cookie('TILMELDAUTH', token, {
|
|
1118
|
+
domain: this.config.cookieDomain,
|
|
1119
|
+
path: this.config.cookiePath,
|
|
1120
|
+
maxAge: this.config.jwtExpire * 1000,
|
|
1121
|
+
secure: appUrl.protocol === 'https:',
|
|
1122
|
+
httpOnly: false, // Allow JS access (for CSRF protection).
|
|
1123
|
+
sameSite: appUrl.protocol === 'https:' ? 'lax' : 'strict',
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
if (sendAuthHeader) {
|
|
1128
|
+
this.response.set('X-TILMELDAUTH', token);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
this.response.locals.user = user;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
await this.fillSession(user);
|
|
1136
|
+
return true;
|
|
1137
|
+
}
|
|
1138
|
+
return false;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* Adds a switch auth token for the given user.
|
|
1143
|
+
*
|
|
1144
|
+
* This effectively logs the current user in to the system as the given user.
|
|
1145
|
+
*
|
|
1146
|
+
* @param user The user.
|
|
1147
|
+
* @param sendAuthHeader Send the auth token as a custom header.
|
|
1148
|
+
* @param sendCookie Send the auth token as a cookie.
|
|
1149
|
+
* @returns True on success, false on failure.
|
|
1150
|
+
*/
|
|
1151
|
+
public async loginSwitch(
|
|
1152
|
+
user: User & UserData,
|
|
1153
|
+
sendAuthHeader: boolean,
|
|
1154
|
+
sendCookie = true,
|
|
1155
|
+
) {
|
|
1156
|
+
if (user.guid != null) {
|
|
1157
|
+
if (this.response) {
|
|
1158
|
+
if (!this.response.headersSent) {
|
|
1159
|
+
const token = this.config.jwtBuilder(this.config, user, true);
|
|
1160
|
+
|
|
1161
|
+
if (sendCookie) {
|
|
1162
|
+
const appUrl = new URL(this.config.appUrl);
|
|
1163
|
+
this.response.cookie('TILMELDSWITCH', token, {
|
|
1164
|
+
domain: this.config.cookieDomain,
|
|
1165
|
+
path: this.config.cookiePath,
|
|
1166
|
+
maxAge: this.config.jwtSwitchExpire * 1000,
|
|
1167
|
+
secure: appUrl.protocol === 'https:',
|
|
1168
|
+
httpOnly: false, // Allow JS access (for PubSub auth token).
|
|
1169
|
+
sameSite: appUrl.protocol === 'https:' ? 'lax' : 'strict',
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
if (sendAuthHeader) {
|
|
1174
|
+
this.response.set('X-TILMELDSWITCH', token);
|
|
1175
|
+
}
|
|
1050
1176
|
}
|
|
1177
|
+
|
|
1178
|
+
this.response.locals.user = user;
|
|
1051
1179
|
}
|
|
1052
|
-
|
|
1180
|
+
|
|
1181
|
+
await this.fillSession(user);
|
|
1053
1182
|
return true;
|
|
1054
1183
|
}
|
|
1055
1184
|
return false;
|
|
@@ -1057,15 +1186,50 @@ export default class Tilmeld implements TilmeldInterface {
|
|
|
1057
1186
|
|
|
1058
1187
|
/**
|
|
1059
1188
|
* Logs the current user out of the system.
|
|
1189
|
+
*
|
|
1190
|
+
* @param clearCookie Clear the auth cookie. (Also send a header.)
|
|
1191
|
+
*/
|
|
1192
|
+
public async logout(clearCookie = true) {
|
|
1193
|
+
if (this.request && !this.alreadyLoggedOutSwitch) {
|
|
1194
|
+
const cookies = this.request.cookies ?? {};
|
|
1195
|
+
|
|
1196
|
+
if ('TILMELDSWITCH' in cookies) {
|
|
1197
|
+
this.alreadyLoggedOutSwitch = true;
|
|
1198
|
+
return await this.logoutSwitch(clearCookie);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
this.clearSession();
|
|
1203
|
+
if (this.response) {
|
|
1204
|
+
if (clearCookie && !this.response.headersSent) {
|
|
1205
|
+
this.response.clearCookie('TILMELDAUTH', {
|
|
1206
|
+
domain: this.config.cookieDomain,
|
|
1207
|
+
path: this.config.cookiePath,
|
|
1208
|
+
});
|
|
1209
|
+
this.response.set('X-TILMELDAUTH', '');
|
|
1210
|
+
}
|
|
1211
|
+
this.response.locals.user = null;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/**
|
|
1216
|
+
* Clears the switch user out of the system.
|
|
1217
|
+
*
|
|
1218
|
+
* @param clearCookie Clear the auth cookie. (Also send a header.)
|
|
1060
1219
|
*/
|
|
1061
|
-
public
|
|
1220
|
+
public async logoutSwitch(clearCookie = true) {
|
|
1062
1221
|
this.clearSession();
|
|
1063
|
-
if (this.response
|
|
1064
|
-
this.response.
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1222
|
+
if (this.response) {
|
|
1223
|
+
if (clearCookie && !this.response.headersSent) {
|
|
1224
|
+
this.response.clearCookie('TILMELDSWITCH', {
|
|
1225
|
+
domain: this.config.cookieDomain,
|
|
1226
|
+
path: this.config.cookiePath,
|
|
1227
|
+
});
|
|
1228
|
+
this.response.set('X-TILMELDSWITCH', '');
|
|
1229
|
+
}
|
|
1230
|
+
this.response.locals.user = null;
|
|
1231
|
+
|
|
1232
|
+
await this.authenticate(false, true);
|
|
1069
1233
|
}
|
|
1070
1234
|
}
|
|
1071
1235
|
|