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