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