@nymphjs/tilmeld 1.0.0-beta.7 → 1.0.0-beta.71
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 +330 -0
- package/README.md +14 -1
- package/dist/AbleObject.d.ts +15 -0
- package/dist/AbleObject.js +16 -1
- package/dist/AbleObject.js.map +1 -1
- package/dist/Group.d.ts +130 -10
- package/dist/Group.js +311 -129
- package/dist/Group.js.map +1 -1
- package/dist/Group.test.js +77 -2
- package/dist/Group.test.js.map +1 -1
- package/dist/Tilmeld.d.ts +237 -9
- package/dist/Tilmeld.js +569 -201
- package/dist/Tilmeld.js.map +1 -1
- package/dist/User.d.ts +344 -16
- package/dist/User.js +979 -313
- package/dist/User.js.map +1 -1
- package/dist/User.test.js +63 -2
- package/dist/User.test.js.map +1 -1
- package/dist/conf/d.d.ts +250 -3
- package/dist/conf/defaults.js +146 -98
- package/dist/conf/defaults.js.map +1 -1
- package/dist/enforceTilmeld.d.ts +3 -0
- package/dist/enforceTilmeld.js +22 -0
- package/dist/enforceTilmeld.js.map +1 -0
- package/dist/errors/AccessControlError.d.ts +2 -1
- package/dist/errors/AccessControlError.js +3 -2
- package/dist/errors/AccessControlError.js.map +1 -1
- package/dist/errors/BadDataError.d.ts +2 -1
- package/dist/errors/BadDataError.js +3 -2
- package/dist/errors/BadDataError.js.map +1 -1
- package/dist/errors/BadEmailError.d.ts +2 -1
- package/dist/errors/BadEmailError.js +3 -2
- package/dist/errors/BadEmailError.js.map +1 -1
- package/dist/errors/BadUsernameError.d.ts +2 -1
- package/dist/errors/BadUsernameError.js +3 -2
- package/dist/errors/BadUsernameError.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +30 -26
- package/src/AbleObject.ts +2 -2
- package/src/Group.test.ts +102 -4
- package/src/Group.ts +274 -158
- package/src/Tilmeld.ts +383 -249
- package/src/User.test.ts +80 -4
- package/src/User.ts +1061 -382
- package/src/conf/d.ts +26 -13
- package/src/conf/defaults.ts +151 -104
- 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/index.ts +2 -0
- package/src/tilmeldJoiProps.ts +9 -9
- package/tsconfig.json +3 -3
- package/typedoc.json +4 -0
package/dist/Tilmeld.js
CHANGED
|
@@ -10,104 +10,220 @@ const conf_1 = require("./conf");
|
|
|
10
10
|
const errors_1 = require("./errors");
|
|
11
11
|
const Group_1 = __importDefault(require("./Group"));
|
|
12
12
|
const User_1 = __importDefault(require("./User"));
|
|
13
|
+
const enforceTilmeld_1 = require("./enforceTilmeld");
|
|
14
|
+
/**
|
|
15
|
+
* A user and group system for Nymph.js.
|
|
16
|
+
*
|
|
17
|
+
* Written by Hunter Perrin for SciActive.
|
|
18
|
+
*
|
|
19
|
+
* @author Hunter Perrin <hperrin@gmail.com>
|
|
20
|
+
* @copyright SciActive Inc
|
|
21
|
+
* @see http://nymph.io/
|
|
22
|
+
*/
|
|
13
23
|
class Tilmeld {
|
|
24
|
+
/**
|
|
25
|
+
* The Nymph instance.
|
|
26
|
+
*/
|
|
27
|
+
nymph = {};
|
|
28
|
+
/**
|
|
29
|
+
* The Tilmeld config.
|
|
30
|
+
*/
|
|
31
|
+
config;
|
|
32
|
+
/**
|
|
33
|
+
* The currently logged in user.
|
|
34
|
+
*/
|
|
35
|
+
currentUser = null;
|
|
36
|
+
/**
|
|
37
|
+
* The user class for this instance of Tilmeld.
|
|
38
|
+
*/
|
|
39
|
+
User = User_1.default;
|
|
40
|
+
/**
|
|
41
|
+
* The group class for this instance of Tilmeld.
|
|
42
|
+
*/
|
|
43
|
+
Group = Group_1.default;
|
|
44
|
+
/**
|
|
45
|
+
* If you will be performing authentication functions (logging in/out), you
|
|
46
|
+
* should set these so Tilmeld can read and write cookies and headers.
|
|
47
|
+
*
|
|
48
|
+
* If you want the user to be authenticated with the cookie and/or header they
|
|
49
|
+
* provide, you should set at least the request. It's better to set both, so
|
|
50
|
+
* the JWT can be updated if needed.
|
|
51
|
+
*
|
|
52
|
+
* After you set these, call `authenticate()` to read user authentication data
|
|
53
|
+
* from them and fill the user's session.
|
|
54
|
+
*
|
|
55
|
+
* If you want to support cookie based authentication (which still requires an
|
|
56
|
+
* XSRF token for security), you should enable the cookie parser middleware.
|
|
57
|
+
*/
|
|
58
|
+
request = null;
|
|
59
|
+
response = null;
|
|
60
|
+
/**
|
|
61
|
+
* Used to avoid infinite loop.
|
|
62
|
+
*/
|
|
63
|
+
alreadyLoggedOutSwitch = false;
|
|
64
|
+
/**
|
|
65
|
+
* Gatekeeper ability cache.
|
|
66
|
+
*
|
|
67
|
+
* Gatekeeper will cache the user's abilities that it calculates, so it can
|
|
68
|
+
* check faster if that user has been checked before.
|
|
69
|
+
*/
|
|
70
|
+
gatekeeperCache = null;
|
|
71
|
+
/**
|
|
72
|
+
* Create a new instance of Tilmeld.
|
|
73
|
+
*
|
|
74
|
+
* @param config The Tilmeld configuration.
|
|
75
|
+
*/
|
|
14
76
|
constructor(config) {
|
|
15
|
-
this.nymph = {};
|
|
16
|
-
this.currentUser = null;
|
|
17
|
-
this.User = User_1.default;
|
|
18
|
-
this.Group = Group_1.default;
|
|
19
|
-
this.request = null;
|
|
20
|
-
this.response = null;
|
|
21
|
-
this.skipAuthenticate = false;
|
|
22
77
|
this.config = { ...conf_1.ConfigDefaults, ...config };
|
|
78
|
+
if (this.config.emailUsernames &&
|
|
79
|
+
(!this.config.userFields.includes('email') ||
|
|
80
|
+
!this.config.regFields.includes('email'))) {
|
|
81
|
+
throw new Error('You must include email in userFields and regFields if you use emailUsernames.');
|
|
82
|
+
}
|
|
83
|
+
for (let field of this.config.regFields) {
|
|
84
|
+
if (!this.config.userFields.includes(field)) {
|
|
85
|
+
throw new Error(`All fields in regFields must also be in userFields: ${field}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
23
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* This is used internally by Nymph. Don't call it yourself.
|
|
91
|
+
*
|
|
92
|
+
* @returns A clone of this instance.
|
|
93
|
+
*/
|
|
24
94
|
clone() {
|
|
25
95
|
const tilmeld = new Tilmeld(this.config);
|
|
26
96
|
tilmeld.request = this.request;
|
|
27
97
|
tilmeld.response = this.response;
|
|
28
98
|
tilmeld.currentUser = this.currentUser;
|
|
29
|
-
tilmeld.
|
|
99
|
+
tilmeld.gatekeeperCache = this.gatekeeperCache;
|
|
30
100
|
return tilmeld;
|
|
31
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Initialize Tilmeld.
|
|
104
|
+
*
|
|
105
|
+
* This is meant to be called internally by Nymph. Don't call this directly.
|
|
106
|
+
*
|
|
107
|
+
* @param nymph The Nymph instance.
|
|
108
|
+
*/
|
|
32
109
|
init(nymph) {
|
|
33
110
|
this.nymph = nymph;
|
|
34
|
-
const tilmeld = this;
|
|
35
111
|
if (this.nymph.driver == null) {
|
|
36
112
|
throw new Error("Tilmeld can't be configured before Nymph.");
|
|
37
113
|
}
|
|
38
|
-
|
|
114
|
+
// Configure the classes.
|
|
115
|
+
if (nymph.parent) {
|
|
116
|
+
// This is a cloned Nymph, it should have classes already.
|
|
117
|
+
this.User = this.nymph.getEntityClass(User_1.default);
|
|
118
|
+
this.Group = this.nymph.getEntityClass(Group_1.default);
|
|
39
119
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
120
|
+
else {
|
|
121
|
+
// This is a new Nymph, add the classes.
|
|
122
|
+
this.User = this.nymph.addEntityClass(User_1.default);
|
|
123
|
+
this.Group = this.nymph.addEntityClass(Group_1.default);
|
|
124
|
+
}
|
|
125
|
+
// Clone the current user so its Nymph instance is correct.
|
|
126
|
+
if (this.currentUser) {
|
|
127
|
+
const currentUserGUID = this.currentUser.guid;
|
|
128
|
+
const currentUserCDate = this.currentUser.cdate;
|
|
129
|
+
const currentUserMDate = this.currentUser.mdate;
|
|
130
|
+
const currentUserTags = this.currentUser.tags;
|
|
131
|
+
const currentUserData = this.currentUser.$getData();
|
|
132
|
+
const currentUserSData = this.currentUser.$getSData();
|
|
133
|
+
const user = this.User.factorySync();
|
|
134
|
+
user.guid = currentUserGUID;
|
|
135
|
+
user.cdate = currentUserCDate;
|
|
136
|
+
user.mdate = currentUserMDate;
|
|
137
|
+
user.tags = currentUserTags;
|
|
138
|
+
user.$putData(currentUserData, currentUserSData);
|
|
139
|
+
this.currentUser = user;
|
|
140
|
+
this.currentUser.$updateDataProtection();
|
|
141
|
+
this.currentUser.$updateGroupDataProtection();
|
|
142
|
+
}
|
|
143
|
+
// Set up access control hooks when Nymph is called.
|
|
144
|
+
this.nymph.config.debugLog('tilmeld', 'Initializing access controls.');
|
|
50
145
|
this.initAccessControl();
|
|
51
|
-
if (this.request != null && !this.skipAuthenticate) {
|
|
52
|
-
this.authenticate();
|
|
53
|
-
}
|
|
54
|
-
this.skipAuthenticate = false;
|
|
55
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Check to see if the current user has an ability.
|
|
149
|
+
*
|
|
150
|
+
* If `ability` is undefined, it will check to see if a user is currently
|
|
151
|
+
* logged in.
|
|
152
|
+
*
|
|
153
|
+
* @param ability The ability.
|
|
154
|
+
* @returns Whether the user has the given ability.
|
|
155
|
+
*/
|
|
56
156
|
gatekeeper(ability) {
|
|
57
|
-
if (this.currentUser == null) {
|
|
157
|
+
if (this.currentUser == null || this.gatekeeperCache == null) {
|
|
58
158
|
return false;
|
|
59
159
|
}
|
|
60
|
-
|
|
160
|
+
if (ability == null) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
return ((ability in this.gatekeeperCache && !!this.gatekeeperCache[ability]) ||
|
|
164
|
+
!!this.currentUser.abilities?.includes('system/admin'));
|
|
61
165
|
}
|
|
62
166
|
initAccessControl() {
|
|
167
|
+
// Check for the skip access control option and add AC selectors.
|
|
63
168
|
const handleQuery = function (nymph, options, selectors) {
|
|
64
|
-
const tilmeld = nymph
|
|
169
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(nymph);
|
|
65
170
|
if (!options ||
|
|
66
171
|
!('skipAc' in options) ||
|
|
67
172
|
!options.skipAc ||
|
|
68
173
|
options.source === 'client') {
|
|
69
174
|
if (options?.source === 'client') {
|
|
70
|
-
if (!tilmeld.
|
|
175
|
+
if ((!tilmeld.currentUser ||
|
|
176
|
+
(!tilmeld.currentUser.abilities?.includes('tilmeld/admin') &&
|
|
177
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))) && // The user is not an admin.
|
|
71
178
|
((!tilmeld.config.enableUserSearch &&
|
|
72
|
-
options.class?.factorySync() instanceof User_1.default) ||
|
|
179
|
+
options.class?.factorySync() instanceof User_1.default) || // The use is searching for a user and searching is disabled.
|
|
73
180
|
(!tilmeld.config.enableGroupSearch &&
|
|
74
|
-
options.class?.factorySync() instanceof Group_1.default)) &&
|
|
75
|
-
(selectors[0] == null ||
|
|
76
|
-
selectors[0].type !== '&' ||
|
|
77
|
-
(!('guid' in selectors[0]) && !('equal' in selectors[0])) ||
|
|
181
|
+
options.class?.factorySync() instanceof Group_1.default)) && // Or the same, but for a group.
|
|
182
|
+
(selectors[0] == null || // No selector specified.
|
|
183
|
+
selectors[0].type !== '&' || // Or the type is not and.
|
|
184
|
+
(!('guid' in selectors[0]) && !('equal' in selectors[0])) || // Or they're searching for something other than guid or equal.
|
|
78
185
|
('guid' in selectors[0] &&
|
|
79
186
|
(!selectors[0].guid?.length ||
|
|
80
|
-
!selectors[0].guid?.[0]?.length)) ||
|
|
187
|
+
!selectors[0].guid?.[0]?.length)) || // Or they're not searching for a specific guid.
|
|
81
188
|
('equal' in selectors[0] &&
|
|
82
189
|
selectors[0].equal?.[0]?.[0] !== 'username' &&
|
|
83
|
-
selectors[0].equal?.[0]?.[0] !== 'groupname'))
|
|
190
|
+
selectors[0].equal?.[0]?.[0] !== 'groupname')) // Or they're not searching for a specific username/groupname.
|
|
191
|
+
) {
|
|
192
|
+
// If the user is not specifically searching for a GUID or username,
|
|
193
|
+
// and they're not allowed to search, it should fail.
|
|
84
194
|
throw new errors_1.AccessControlError('No permission to search.');
|
|
85
195
|
}
|
|
86
196
|
}
|
|
197
|
+
// Add access control selectors
|
|
87
198
|
tilmeld.addAccessControlSelectors(options, selectors);
|
|
88
199
|
}
|
|
89
200
|
};
|
|
90
201
|
handleQuery.skipOnClone = true;
|
|
202
|
+
// Filter entities being deleted for user permissions.
|
|
91
203
|
const checkPermissionsDelete = async function (nymph, entity) {
|
|
92
|
-
const tilmeld = nymph
|
|
204
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(nymph);
|
|
93
205
|
if (typeof entity.$tilmeldDeleteSkipAC === 'function' &&
|
|
94
206
|
entity.$tilmeldDeleteSkipAC()) {
|
|
95
207
|
return;
|
|
96
208
|
}
|
|
209
|
+
// Test for permissions.
|
|
97
210
|
if (!tilmeld.checkPermissions(entity, nymph_1.TilmeldAccessLevels.FULL_ACCESS)) {
|
|
98
211
|
throw new errors_1.AccessControlError('No permission to delete.');
|
|
99
212
|
}
|
|
100
213
|
};
|
|
101
214
|
checkPermissionsDelete.skipOnClone = true;
|
|
215
|
+
// Filter entities being deleted for user permissions.
|
|
102
216
|
const checkPermissionsDeleteByID = async function (nymph, guid, className) {
|
|
103
|
-
const entity = nymph.
|
|
217
|
+
const entity = await nymph.getEntity({ class: nymph.getEntityClass(className ?? 'Entity') }, { type: '&', guid: guid });
|
|
104
218
|
if (entity != null) {
|
|
105
219
|
checkPermissionsDelete(nymph, entity);
|
|
106
220
|
}
|
|
107
221
|
};
|
|
108
222
|
checkPermissionsDeleteByID.skipOnClone = true;
|
|
223
|
+
// Filter entities being saved for user permissions, and filter any
|
|
224
|
+
// disallowed changes to AC properties.
|
|
109
225
|
const checkPermissionsSaveAndFilterAcChanges = async function (nymph, entity) {
|
|
110
|
-
const tilmeld = nymph
|
|
226
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(nymph);
|
|
111
227
|
if (!entity) {
|
|
112
228
|
return;
|
|
113
229
|
}
|
|
@@ -116,29 +232,10 @@ class Tilmeld {
|
|
|
116
232
|
return;
|
|
117
233
|
}
|
|
118
234
|
if (entity.guid != null) {
|
|
235
|
+
// If the entity is not new, check that the user has full access before
|
|
236
|
+
// allowing a change to ac properties.
|
|
119
237
|
const originalAc = entity.$getOriginalAcValues();
|
|
120
|
-
const newAc =
|
|
121
|
-
user: entity.user ?? undefined,
|
|
122
|
-
group: entity.group ?? undefined,
|
|
123
|
-
acUser: entity.acUser ?? undefined,
|
|
124
|
-
acGroup: entity.acGroup ?? undefined,
|
|
125
|
-
acOther: entity.acOther ?? undefined,
|
|
126
|
-
acRead: entity.acRead ?? undefined,
|
|
127
|
-
acWrite: entity.acWrite ?? undefined,
|
|
128
|
-
acFull: entity.acFull ?? undefined,
|
|
129
|
-
};
|
|
130
|
-
const setAcProperties = (acValues) => {
|
|
131
|
-
for (let name in acValues) {
|
|
132
|
-
const value = acValues[name];
|
|
133
|
-
if (value == null) {
|
|
134
|
-
delete entity[name];
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
entity[name] = value;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
setAcProperties(originalAc);
|
|
238
|
+
const newAc = entity.$getCurrentAcValues();
|
|
142
239
|
function areAcPropertiesChanged(a, b) {
|
|
143
240
|
for (const name of ['user', 'group']) {
|
|
144
241
|
const aVal = a[name];
|
|
@@ -149,7 +246,7 @@ class Tilmeld {
|
|
|
149
246
|
else if (aVal == null || bVal == null) {
|
|
150
247
|
return true;
|
|
151
248
|
}
|
|
152
|
-
if (
|
|
249
|
+
if (aVal !== bVal) {
|
|
153
250
|
return true;
|
|
154
251
|
}
|
|
155
252
|
}
|
|
@@ -178,40 +275,55 @@ class Tilmeld {
|
|
|
178
275
|
if (aVal.length !== bVal.length) {
|
|
179
276
|
return true;
|
|
180
277
|
}
|
|
181
|
-
if ((0, lodash_1.xor)(aVal
|
|
278
|
+
if ((0, lodash_1.xor)(aVal, bVal).length) {
|
|
182
279
|
return true;
|
|
183
280
|
}
|
|
184
281
|
}
|
|
185
282
|
return false;
|
|
186
283
|
}
|
|
187
284
|
if (areAcPropertiesChanged(originalAc, newAc)) {
|
|
188
|
-
if (!tilmeld.checkPermissions(entity, nymph_1.TilmeldAccessLevels.FULL_ACCESS)) {
|
|
285
|
+
if (!tilmeld.checkPermissions(entity, nymph_1.TilmeldAccessLevels.FULL_ACCESS, undefined, originalAc)) {
|
|
286
|
+
// Only allow changes to AC properties if the user has full access.
|
|
189
287
|
throw new errors_1.AccessControlError('No permission to change access control properties.');
|
|
190
288
|
}
|
|
191
|
-
if ((
|
|
192
|
-
(
|
|
193
|
-
newAc.user &&
|
|
194
|
-
|
|
195
|
-
!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
289
|
+
if ((originalAc.user != null && originalAc.user !== newAc.user) ||
|
|
290
|
+
(originalAc.user == null &&
|
|
291
|
+
newAc.user != null &&
|
|
292
|
+
newAc.user !== tilmeld.currentUser?.guid)) {
|
|
196
293
|
throw new errors_1.AccessControlError('No permission to assign to another user.');
|
|
197
294
|
}
|
|
198
|
-
if (newAc.group &&
|
|
199
|
-
|
|
200
|
-
!(newAc.group
|
|
201
|
-
|
|
202
|
-
!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
295
|
+
if (newAc.group != null &&
|
|
296
|
+
newAc.group !== originalAc.group &&
|
|
297
|
+
!(newAc.group === tilmeld.currentUser?.$getGid() ||
|
|
298
|
+
(tilmeld.currentUser?.$getGids() ?? []).includes(newAc.group))) {
|
|
203
299
|
throw new errors_1.AccessControlError('No permission to assign to another group.');
|
|
204
300
|
}
|
|
205
|
-
setAcProperties(newAc);
|
|
206
301
|
}
|
|
207
302
|
}
|
|
303
|
+
// Test for permissions.
|
|
208
304
|
if (!tilmeld.checkPermissions(entity, nymph_1.TilmeldAccessLevels.WRITE_ACCESS)) {
|
|
209
305
|
throw new errors_1.AccessControlError('No permission to write.');
|
|
210
306
|
}
|
|
211
307
|
};
|
|
212
308
|
checkPermissionsSaveAndFilterAcChanges.skipOnClone = true;
|
|
309
|
+
/*
|
|
310
|
+
* Add the current user's "user", "group", and access control to new entity.
|
|
311
|
+
*
|
|
312
|
+
* This occurs right before an entity is saved. It only alters the entity
|
|
313
|
+
* if:
|
|
314
|
+
*
|
|
315
|
+
* - There is a user logged in.
|
|
316
|
+
* - The entity is new (doesn't have a GUID.)
|
|
317
|
+
* - The entity is not a user or group.
|
|
318
|
+
*
|
|
319
|
+
* Default access control is
|
|
320
|
+
*
|
|
321
|
+
* - acUser = TilmeldAccessLevels.FULL_ACCESS
|
|
322
|
+
* - acGroup = TilmeldAccessLevels.READ_ACCESS
|
|
323
|
+
* - acOther = TilmeldAccessLevels.NO_ACCESS
|
|
324
|
+
*/
|
|
213
325
|
const addAccess = async function (nymph, entity) {
|
|
214
|
-
const tilmeld = nymph
|
|
326
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(nymph);
|
|
215
327
|
const user = tilmeld.currentUser;
|
|
216
328
|
if (user != null &&
|
|
217
329
|
entity.guid == null &&
|
|
@@ -220,10 +332,11 @@ class Tilmeld {
|
|
|
220
332
|
if (entity.user == null) {
|
|
221
333
|
entity.user = user;
|
|
222
334
|
}
|
|
223
|
-
if (entity.group == null
|
|
224
|
-
user.group
|
|
225
|
-
|
|
226
|
-
|
|
335
|
+
if (entity.group == null) {
|
|
336
|
+
const group = user.group;
|
|
337
|
+
if (group != null && group.guid != null) {
|
|
338
|
+
entity.group = group;
|
|
339
|
+
}
|
|
227
340
|
}
|
|
228
341
|
if (entity.acUser == null) {
|
|
229
342
|
entity.acUser = nymph_1.TilmeldAccessLevels.FULL_ACCESS;
|
|
@@ -303,24 +416,29 @@ class Tilmeld {
|
|
|
303
416
|
this.nymph.on('beforeDeleteEntity', checkPermissionsDelete);
|
|
304
417
|
this.nymph.on('beforeDeleteEntityByID', checkPermissionsDeleteByID);
|
|
305
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* Add selectors to a list of options and selectors which will limit results
|
|
421
|
+
* to only entities the current user has access to.
|
|
422
|
+
*
|
|
423
|
+
* @param optionsAndSelectors The options and selectors of the query.
|
|
424
|
+
*/
|
|
306
425
|
addAccessControlSelectors(options, selectors) {
|
|
307
426
|
const user = this.currentUser;
|
|
308
|
-
if (user != null &&
|
|
309
|
-
(user.abilities?.indexOf('system/admin') ?? -1) !== -1) {
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
427
|
if (options == null) {
|
|
313
428
|
throw new Error('No options in argument.');
|
|
314
429
|
}
|
|
315
430
|
else if ('class' in options &&
|
|
316
431
|
(options.class?.factorySync() instanceof User_1.default ||
|
|
317
432
|
options.class?.factorySync() instanceof Group_1.default)) {
|
|
433
|
+
// They are requesting a user/group. Always accessible for reading.
|
|
318
434
|
return;
|
|
319
435
|
}
|
|
320
436
|
if (user == null || user.guid == null) {
|
|
321
437
|
selectors.push({
|
|
322
438
|
type: '|',
|
|
439
|
+
// Other access control is sufficient.
|
|
323
440
|
gte: [['acOther', nymph_1.TilmeldAccessLevels.READ_ACCESS]],
|
|
441
|
+
// The user and group are not set.
|
|
324
442
|
selector: [
|
|
325
443
|
{
|
|
326
444
|
type: '&',
|
|
@@ -335,6 +453,7 @@ class Tilmeld {
|
|
|
335
453
|
type: '&',
|
|
336
454
|
'!defined': [['user', 'group']],
|
|
337
455
|
},
|
|
456
|
+
// It is owned by the user.
|
|
338
457
|
{
|
|
339
458
|
type: '&',
|
|
340
459
|
ref: [['user', user.guid]],
|
|
@@ -343,28 +462,25 @@ class Tilmeld {
|
|
|
343
462
|
];
|
|
344
463
|
const groupRefs = [];
|
|
345
464
|
const acRefs = [];
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
465
|
+
const gid = user.$getGid();
|
|
466
|
+
if (gid != null) {
|
|
467
|
+
// It belongs to the user's primary group.
|
|
468
|
+
groupRefs.push(['group', gid]);
|
|
469
|
+
// User's primary group is listed in acRead, acWrite, or acFull.
|
|
470
|
+
acRefs.push(['acRead', gid]);
|
|
471
|
+
acRefs.push(['acWrite', gid]);
|
|
472
|
+
acRefs.push(['acFull', gid]);
|
|
351
473
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
for (let curDescendantGroup of user.$getDescendantGroupsSync()) {
|
|
361
|
-
if (curDescendantGroup != null && curDescendantGroup.guid != null) {
|
|
362
|
-
groupRefs.push(['group', curDescendantGroup.guid]);
|
|
363
|
-
acRefs.push(['acRead', curDescendantGroup.guid]);
|
|
364
|
-
acRefs.push(['acWrite', curDescendantGroup.guid]);
|
|
365
|
-
acRefs.push(['acFull', curDescendantGroup.guid]);
|
|
366
|
-
}
|
|
474
|
+
const gids = user.$getGids();
|
|
475
|
+
for (let curGid of gids ?? []) {
|
|
476
|
+
// It belongs to the user's secondary group.
|
|
477
|
+
groupRefs.push(['group', curGid]);
|
|
478
|
+
// User's secondary group is listed in acRead, acWrite, or acFull.
|
|
479
|
+
acRefs.push(['acRead', curGid]);
|
|
480
|
+
acRefs.push(['acWrite', curGid]);
|
|
481
|
+
acRefs.push(['acFull', curGid]);
|
|
367
482
|
}
|
|
483
|
+
// All the group refs.
|
|
368
484
|
if (groupRefs.length) {
|
|
369
485
|
subSelectors.push({
|
|
370
486
|
type: '&',
|
|
@@ -377,6 +493,7 @@ class Tilmeld {
|
|
|
377
493
|
],
|
|
378
494
|
});
|
|
379
495
|
}
|
|
496
|
+
// All the acRead, acWrite, and acFull refs.
|
|
380
497
|
if (acRefs.length) {
|
|
381
498
|
subSelectors.push({
|
|
382
499
|
type: '|',
|
|
@@ -385,8 +502,11 @@ class Tilmeld {
|
|
|
385
502
|
}
|
|
386
503
|
const selector = {
|
|
387
504
|
type: '|',
|
|
505
|
+
// Other access control is sufficient.
|
|
388
506
|
gte: [['acOther', nymph_1.TilmeldAccessLevels.READ_ACCESS]],
|
|
507
|
+
// The user and group are not set.
|
|
389
508
|
selector: subSelectors,
|
|
509
|
+
// The user is listed in acRead, acWrite, or acFull.
|
|
390
510
|
ref: [
|
|
391
511
|
['acRead', user.guid],
|
|
392
512
|
['acWrite', user.guid],
|
|
@@ -396,12 +516,76 @@ class Tilmeld {
|
|
|
396
516
|
selectors.push(selector);
|
|
397
517
|
}
|
|
398
518
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
519
|
+
/**
|
|
520
|
+
* Check an entity's permissions for a user.
|
|
521
|
+
*
|
|
522
|
+
* This will check the AC (Access Control) properties of the entity. These
|
|
523
|
+
* include the following properties:
|
|
524
|
+
*
|
|
525
|
+
* - acUser
|
|
526
|
+
* - acGroup
|
|
527
|
+
* - acOther
|
|
528
|
+
* - acRead
|
|
529
|
+
* - acWrite
|
|
530
|
+
* - acFull
|
|
531
|
+
*
|
|
532
|
+
* "acUser" refers to the entity's owner, "acGroup" refers to all users in the
|
|
533
|
+
* entity's group and all ancestor groups, and "acOther" refers to any user
|
|
534
|
+
* who doesn't fit these descriptions.
|
|
535
|
+
*
|
|
536
|
+
* Each of these properties should be either NO_ACCESS, READ_ACCESS,
|
|
537
|
+
* WRITE_ACCESS, or FULL_ACCESS.
|
|
538
|
+
*
|
|
539
|
+
* - NO_ACCESS - the user has no access to the entity.
|
|
540
|
+
* - READ_ACCESS, the user has read access to the entity.
|
|
541
|
+
* - WRITE_ACCESS, the user has read and write access to the entity, but can't
|
|
542
|
+
* delete it, change its access controls, or change its ownership.
|
|
543
|
+
* - FULL_ACCESS, the user has read, write, and delete access to the entity,
|
|
544
|
+
* as well as being able to manage its access controls and ownership.
|
|
545
|
+
*
|
|
546
|
+
* These properties default to:
|
|
547
|
+
*
|
|
548
|
+
* - acUser = TilmeldAccessLevels.FULL_ACCESS
|
|
549
|
+
* - acGroup = TilmeldAccessLevels.READ_ACCESS
|
|
550
|
+
* - acOther = TilmeldAccessLevels.NO_ACCESS
|
|
551
|
+
*
|
|
552
|
+
* "acRead", "acWrite", and "acFull" are arrays of users and/or groups that
|
|
553
|
+
* also have those permissions.
|
|
554
|
+
*
|
|
555
|
+
* Only users with FULL_ACCESS have the ability to change any of the ac*,
|
|
556
|
+
* user, and group properties.
|
|
557
|
+
*
|
|
558
|
+
* The following conditions will result in different checks, which determine
|
|
559
|
+
* whether the check passes:
|
|
560
|
+
*
|
|
561
|
+
* - It is a user or group. (True for READ_ACCESS or Tilmeld admins.)
|
|
562
|
+
* - No user is logged in. (Check other AC.)
|
|
563
|
+
* - The entity is the user. (Always true.)
|
|
564
|
+
* - It is the user's primary group. (True for READ_ACCESS.)
|
|
565
|
+
* - The user or its groups are listed in "acRead". (True for READ_ACCESS.)
|
|
566
|
+
* - The user or its groups are listed in "acWrite". (True for READ_ACCESS and
|
|
567
|
+
* WRITE_ACCESS.)
|
|
568
|
+
* - The user or its groups are listed in "acFull". (Always true.)
|
|
569
|
+
* - Its "user" is the user. (It is owned by the user.) (Check user AC.)
|
|
570
|
+
* - Its "group" is the user's primary group. (Check group AC.)
|
|
571
|
+
* - Its "group" is one of the user's secondary groups. (Check group AC.)
|
|
572
|
+
* - Its "group" is a descendant of one of the user's groups. (Check group
|
|
573
|
+
* AC.)
|
|
574
|
+
* - None of the above. (Check other AC.)
|
|
575
|
+
*
|
|
576
|
+
* @param entity The entity to check.
|
|
577
|
+
* @param type The lowest level of permission to consider a pass.
|
|
578
|
+
* @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
|
|
579
|
+
* @param acProperties The acProperties to use instead of getting them from the entity.
|
|
580
|
+
* @returns Whether the current user has at least `type` permission for the entity.
|
|
581
|
+
*/
|
|
582
|
+
checkPermissions(entity, type = nymph_1.TilmeldAccessLevels.READ_ACCESS, user, acProperties) {
|
|
583
|
+
if (!acProperties) {
|
|
584
|
+
acProperties = entity.$getCurrentAcValues();
|
|
402
585
|
}
|
|
403
586
|
let userOrNull = null;
|
|
404
587
|
let userOrEmpty = this.User.factorySync();
|
|
588
|
+
// Calculate the user.
|
|
405
589
|
if (user == null) {
|
|
406
590
|
userOrNull = this.currentUser;
|
|
407
591
|
userOrEmpty = this.User.current(true);
|
|
@@ -410,131 +594,158 @@ class Tilmeld {
|
|
|
410
594
|
userOrNull = user;
|
|
411
595
|
userOrEmpty = user;
|
|
412
596
|
}
|
|
413
|
-
|
|
414
|
-
return true;
|
|
415
|
-
}
|
|
597
|
+
// Users and groups are always readable. Editable by Tilmeld admins.
|
|
416
598
|
if ((entity instanceof User_1.default || entity instanceof Group_1.default) &&
|
|
417
599
|
(type === nymph_1.TilmeldAccessLevels.READ_ACCESS ||
|
|
418
|
-
userOrEmpty
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
if (entity.user == null && entity.group == null) {
|
|
600
|
+
userOrEmpty.abilities?.includes('tilmeld/admin') ||
|
|
601
|
+
userOrEmpty.abilities?.includes('system/admin'))) {
|
|
422
602
|
return true;
|
|
423
603
|
}
|
|
424
|
-
|
|
425
|
-
const
|
|
426
|
-
const
|
|
427
|
-
|
|
604
|
+
// Load access control, since we need it now...
|
|
605
|
+
const acUser = acProperties.acUser ?? nymph_1.TilmeldAccessLevels.FULL_ACCESS;
|
|
606
|
+
const acGroup = acProperties.acGroup ?? nymph_1.TilmeldAccessLevels.READ_ACCESS;
|
|
607
|
+
const acOther = acProperties.acOther ?? nymph_1.TilmeldAccessLevels.NO_ACCESS;
|
|
608
|
+
if (userOrNull == null) {
|
|
428
609
|
return acOther >= type;
|
|
429
610
|
}
|
|
611
|
+
// Check if the entity is the user.
|
|
430
612
|
if (userOrEmpty.$is(entity)) {
|
|
431
613
|
return true;
|
|
432
614
|
}
|
|
433
|
-
if
|
|
434
|
-
|
|
435
|
-
|
|
615
|
+
// Check if the entity is the user's group. Always readable.
|
|
616
|
+
const gid = userOrEmpty.$getGid();
|
|
617
|
+
if (gid != null &&
|
|
618
|
+
entity.guid === gid &&
|
|
436
619
|
type === nymph_1.TilmeldAccessLevels.READ_ACCESS) {
|
|
437
620
|
return true;
|
|
438
621
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
622
|
+
// Calculate all the groups the user belongs to.
|
|
623
|
+
let allGids = gid == null ? [] : [gid];
|
|
624
|
+
allGids = allGids.concat(userOrEmpty.$getGids() ?? []);
|
|
625
|
+
// Check access ac properties.
|
|
442
626
|
const checks = [
|
|
443
627
|
{
|
|
444
628
|
type: nymph_1.TilmeldAccessLevels.FULL_ACCESS,
|
|
445
|
-
array: [...(
|
|
629
|
+
array: [...(acProperties.acFull ?? [])],
|
|
446
630
|
},
|
|
447
631
|
{
|
|
448
632
|
type: nymph_1.TilmeldAccessLevels.WRITE_ACCESS,
|
|
449
|
-
array: [...(
|
|
633
|
+
array: [...(acProperties.acWrite ?? [])],
|
|
450
634
|
},
|
|
451
635
|
{
|
|
452
636
|
type: nymph_1.TilmeldAccessLevels.READ_ACCESS,
|
|
453
|
-
array: [...(
|
|
637
|
+
array: [...(acProperties.acRead ?? [])],
|
|
454
638
|
},
|
|
455
639
|
];
|
|
456
640
|
for (let curCheck of checks) {
|
|
457
641
|
if (type <= curCheck.type) {
|
|
458
|
-
if (
|
|
642
|
+
if (curCheck.array.includes(userOrEmpty.guid)) {
|
|
459
643
|
return true;
|
|
460
644
|
}
|
|
461
|
-
for (let
|
|
462
|
-
if (
|
|
463
|
-
curGroup.$inArray(curCheck.array)) {
|
|
645
|
+
for (let curGid of allGids) {
|
|
646
|
+
if (curCheck.array.includes(curGid)) {
|
|
464
647
|
return true;
|
|
465
648
|
}
|
|
466
649
|
}
|
|
467
650
|
}
|
|
468
651
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
entity.user.$is(userOrNull)) {
|
|
652
|
+
// Check ownership ac properties.
|
|
653
|
+
if (acProperties.user != null && acProperties.user === userOrNull.guid) {
|
|
472
654
|
return acUser >= type;
|
|
473
655
|
}
|
|
474
|
-
if (
|
|
475
|
-
typeof entity.group.$inArray === 'function' &&
|
|
476
|
-
entity.group.$inArray(allGroups)) {
|
|
656
|
+
if (acProperties.group != null && allGids.includes(acProperties.group)) {
|
|
477
657
|
return acGroup >= type;
|
|
478
658
|
}
|
|
479
659
|
return acOther >= type;
|
|
480
660
|
}
|
|
481
|
-
|
|
661
|
+
/**
|
|
662
|
+
* Check a UID's permissions for a user.
|
|
663
|
+
*
|
|
664
|
+
* ## THIS ONLY CHECKS AUTOMATICALLY FOR CLIENT REQUESTS.
|
|
665
|
+
*
|
|
666
|
+
* UID functions on the Node.js side are not checked automatically. This
|
|
667
|
+
* function is only run automatically for UID functions run from the client.
|
|
668
|
+
* You should call this function manually if you're running a UID function on
|
|
669
|
+
* the Node.js side and you want it gated.
|
|
670
|
+
*
|
|
671
|
+
* This will check the Tilmeld config and the user's abilities.
|
|
672
|
+
*
|
|
673
|
+
* - READ_ACCESS, the UID is listed in clientReadableUIDs or the user has the
|
|
674
|
+
* "uid/get/nameofuid" ability.
|
|
675
|
+
* - WRITE_ACCESS, the UID is listed in clientEnabledUIDs or the user has the
|
|
676
|
+
* "uid/new/nameofuid" ability.
|
|
677
|
+
* - FULL_ACCESS, the UID is listed in clientSetabledUIDs or the user has the
|
|
678
|
+
* "uid/set/nameofuid" ability.
|
|
679
|
+
*
|
|
680
|
+
* @param name The UID to check.
|
|
681
|
+
* @param type The lowest level of permission to consider a pass.
|
|
682
|
+
* @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
|
|
683
|
+
* @returns Whether the current user has at least `type` permission for the UID.
|
|
684
|
+
*/
|
|
685
|
+
async checkClientUIDPermissions(name, type = nymph_1.TilmeldAccessLevels.READ_ACCESS, user) {
|
|
482
686
|
let userOrEmpty = this.User.factorySync();
|
|
687
|
+
// Calculate the user.
|
|
483
688
|
if (user == null) {
|
|
484
689
|
userOrEmpty = this.User.current(true);
|
|
485
690
|
}
|
|
486
691
|
else if (user !== false) {
|
|
487
692
|
userOrEmpty = user;
|
|
488
693
|
}
|
|
489
|
-
if (userOrEmpty
|
|
694
|
+
if (userOrEmpty.abilities?.includes('system/admin')) {
|
|
490
695
|
return true;
|
|
491
696
|
}
|
|
492
697
|
if (type === nymph_1.TilmeldAccessLevels.FULL_ACCESS) {
|
|
493
|
-
return (this.config.clientSetabledUIDs.
|
|
494
|
-
userOrEmpty.$gatekeeper(`uid/set/${name}`));
|
|
698
|
+
return (this.config.clientSetabledUIDs.includes(name) ||
|
|
699
|
+
(await userOrEmpty.$gatekeeper(`uid/set/${name}`)));
|
|
495
700
|
}
|
|
496
701
|
else if (type === nymph_1.TilmeldAccessLevels.WRITE_ACCESS) {
|
|
497
|
-
return (this.config.clientEnabledUIDs.
|
|
498
|
-
this.config.clientSetabledUIDs.
|
|
499
|
-
userOrEmpty.$gatekeeper(`uid/new/${name}`) ||
|
|
500
|
-
userOrEmpty.$gatekeeper(`uid/set/${name}`));
|
|
702
|
+
return (this.config.clientEnabledUIDs.includes(name) ||
|
|
703
|
+
this.config.clientSetabledUIDs.includes(name) ||
|
|
704
|
+
(await userOrEmpty.$gatekeeper(`uid/new/${name}`)) ||
|
|
705
|
+
(await userOrEmpty.$gatekeeper(`uid/set/${name}`)));
|
|
501
706
|
}
|
|
502
707
|
else if (type === nymph_1.TilmeldAccessLevels.READ_ACCESS) {
|
|
503
|
-
return (this.config.clientReadableUIDs.
|
|
504
|
-
this.config.clientEnabledUIDs.
|
|
505
|
-
this.config.clientSetabledUIDs.
|
|
506
|
-
userOrEmpty.$gatekeeper(`uid/get/${name}`) ||
|
|
507
|
-
userOrEmpty.$gatekeeper(`uid/new/${name}`) ||
|
|
508
|
-
userOrEmpty.$gatekeeper(`uid/set/${name}`));
|
|
708
|
+
return (this.config.clientReadableUIDs.includes(name) ||
|
|
709
|
+
this.config.clientEnabledUIDs.includes(name) ||
|
|
710
|
+
this.config.clientSetabledUIDs.includes(name) ||
|
|
711
|
+
(await userOrEmpty.$gatekeeper(`uid/get/${name}`)) ||
|
|
712
|
+
(await userOrEmpty.$gatekeeper(`uid/new/${name}`)) ||
|
|
713
|
+
(await userOrEmpty.$gatekeeper(`uid/set/${name}`)));
|
|
509
714
|
}
|
|
510
715
|
return false;
|
|
511
716
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
717
|
+
/**
|
|
718
|
+
* Fill session user data.
|
|
719
|
+
*
|
|
720
|
+
* @param user The user.
|
|
721
|
+
*/
|
|
722
|
+
async fillSession(user) {
|
|
515
723
|
this.currentUser = user;
|
|
724
|
+
this.gatekeeperCache = await user.$getGatekeeperCache();
|
|
725
|
+
// Now update the data protection on the user and all the groups.
|
|
516
726
|
this.currentUser.$updateDataProtection();
|
|
517
|
-
|
|
518
|
-
this.currentUser.group.$updateDataProtection();
|
|
519
|
-
}
|
|
520
|
-
for (let group of this.currentUser.groups ?? []) {
|
|
521
|
-
group.$updateDataProtection();
|
|
522
|
-
}
|
|
727
|
+
this.currentUser.$updateGroupDataProtection();
|
|
523
728
|
}
|
|
729
|
+
/**
|
|
730
|
+
* Clear session user data.
|
|
731
|
+
*/
|
|
524
732
|
clearSession() {
|
|
525
733
|
const user = this.currentUser;
|
|
526
734
|
this.currentUser = null;
|
|
735
|
+
this.gatekeeperCache = null;
|
|
527
736
|
if (user) {
|
|
528
737
|
user.$updateDataProtection();
|
|
529
|
-
|
|
530
|
-
user.group.$updateDataProtection();
|
|
531
|
-
}
|
|
532
|
-
for (let group of user.groups ?? []) {
|
|
533
|
-
group.$updateDataProtection();
|
|
534
|
-
}
|
|
738
|
+
user.$updateGroupDataProtection();
|
|
535
739
|
}
|
|
536
740
|
}
|
|
741
|
+
/**
|
|
742
|
+
* Validate and extract the user from a token.
|
|
743
|
+
*
|
|
744
|
+
* @param token The authentication token.
|
|
745
|
+
* @returns The user on success, null on failure.
|
|
746
|
+
*/
|
|
537
747
|
async extractToken(token) {
|
|
748
|
+
this.nymph.config.debugLog('tilmeld', 'Extracting and validating token.');
|
|
538
749
|
const extract = this.config.jwtExtract(this.config, token);
|
|
539
750
|
if (extract == null) {
|
|
540
751
|
return null;
|
|
@@ -544,89 +755,246 @@ class Tilmeld {
|
|
|
544
755
|
type: '&',
|
|
545
756
|
guid: guid,
|
|
546
757
|
});
|
|
547
|
-
if (!user || !user.guid
|
|
758
|
+
if (!user || !user.guid) {
|
|
548
759
|
return null;
|
|
549
760
|
}
|
|
550
761
|
return user;
|
|
551
762
|
}
|
|
552
|
-
|
|
763
|
+
/**
|
|
764
|
+
* Check for TILMELDAUTH and TILMELDSWITCH tokens, and, if set, authenticate
|
|
765
|
+
* from it/them.
|
|
766
|
+
*
|
|
767
|
+
* You can also call this function after setting `response.locals.user` to the
|
|
768
|
+
* user you want to authenticate. You *should* check for `user.enabled` before
|
|
769
|
+
* setting this variable, unless you explicitly want to log in as a disabled
|
|
770
|
+
* user. (The user must be an instance of the User class for this Tilmeld
|
|
771
|
+
* instance.)
|
|
772
|
+
*
|
|
773
|
+
* This function will set `response.locals.user` to the logged in user on
|
|
774
|
+
* successful authentication.
|
|
775
|
+
*
|
|
776
|
+
* @param skipXsrfToken Skip the XSRF token check.
|
|
777
|
+
* @param skipRenew Skip the token renewal step, even if the token is close to expiration.
|
|
778
|
+
* @returns True if a user was authenticated, false on any failure.
|
|
779
|
+
*/
|
|
780
|
+
async authenticate(skipXsrfToken = false, skipRenew = false) {
|
|
553
781
|
if (this.request == null) {
|
|
554
782
|
return false;
|
|
555
783
|
}
|
|
784
|
+
if (this.response &&
|
|
785
|
+
this.response.locals.user &&
|
|
786
|
+
this.response.locals.user instanceof this.User) {
|
|
787
|
+
// The user has already been authenticated through some other means.
|
|
788
|
+
const user = this.response.locals.user;
|
|
789
|
+
await this.fillSession(user);
|
|
790
|
+
return true;
|
|
791
|
+
}
|
|
556
792
|
const cookies = this.request.cookies ?? {};
|
|
793
|
+
// If a client does't support cookies, they can use the X-TILMELDAUTH header
|
|
794
|
+
// to provide the auth token.
|
|
557
795
|
let fromAuthHeader = false;
|
|
558
796
|
let authToken;
|
|
797
|
+
let switchToken;
|
|
559
798
|
if (this.request.header('x-tilmeldauth') != null) {
|
|
560
799
|
fromAuthHeader = true;
|
|
561
800
|
authToken = this.request.header('x-tilmeldauth');
|
|
801
|
+
switchToken =
|
|
802
|
+
this.request.header('x-tilmeldswitch') ||
|
|
803
|
+
'TILMELDSWITCH' in cookies
|
|
804
|
+
? cookies.TILMELDSWITCH
|
|
805
|
+
: null;
|
|
562
806
|
}
|
|
563
807
|
else if ('TILMELDAUTH' in cookies) {
|
|
564
808
|
fromAuthHeader = false;
|
|
565
809
|
authToken = cookies.TILMELDAUTH;
|
|
810
|
+
switchToken = 'TILMELDSWITCH' in cookies ? cookies.TILMELDSWITCH : null;
|
|
566
811
|
}
|
|
567
812
|
else {
|
|
568
813
|
return false;
|
|
569
814
|
}
|
|
570
|
-
let
|
|
815
|
+
let authExtract;
|
|
816
|
+
let switchExtract;
|
|
571
817
|
if (skipXsrfToken ||
|
|
572
818
|
this.request.originalUrl.startsWith(this.config.setupPath)) {
|
|
573
|
-
|
|
819
|
+
// The request is for the setup app, or we were told to skip the XSRF
|
|
820
|
+
// check, so don't check for the XSRF token.
|
|
821
|
+
authExtract = this.config.jwtExtract(this.config, authToken);
|
|
822
|
+
switchExtract = switchToken
|
|
823
|
+
? this.config.jwtExtract(this.config, switchToken)
|
|
824
|
+
: null;
|
|
574
825
|
}
|
|
575
826
|
else {
|
|
827
|
+
// The request is for something else, so check for a valid XSRF token,
|
|
828
|
+
// unless the auth token is provided by a header (instead of a cookie).
|
|
576
829
|
const xsrfToken = this.request.header('x-xsrf-token');
|
|
577
830
|
if (xsrfToken == null && !fromAuthHeader) {
|
|
578
831
|
return false;
|
|
579
832
|
}
|
|
580
|
-
|
|
833
|
+
authExtract = this.config.jwtExtract(this.config, authToken, xsrfToken);
|
|
834
|
+
switchExtract = switchToken
|
|
835
|
+
? this.config.jwtExtract(this.config, switchToken)
|
|
836
|
+
: null;
|
|
581
837
|
}
|
|
582
|
-
if (
|
|
583
|
-
this.logout();
|
|
838
|
+
if (authExtract == null) {
|
|
839
|
+
await this.logout();
|
|
584
840
|
return false;
|
|
585
841
|
}
|
|
586
|
-
const { guid, expire } =
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
842
|
+
const { guid, issued, expire } = authExtract;
|
|
843
|
+
const { guid: switchGuid, expire: switchExpire } = switchExtract || {
|
|
844
|
+
guid: null,
|
|
845
|
+
expire: null,
|
|
846
|
+
};
|
|
847
|
+
const user = await this.User.factory(guid);
|
|
848
|
+
if (user.guid == null ||
|
|
849
|
+
!user.enabled ||
|
|
850
|
+
expire.getTime() <= Date.now() ||
|
|
851
|
+
issued.getTime() > Date.now() ||
|
|
852
|
+
(user.revokeTokenDate != null && issued.getTime() <= user.revokeTokenDate)) {
|
|
853
|
+
await this.logout();
|
|
590
854
|
return false;
|
|
591
855
|
}
|
|
592
|
-
if (
|
|
593
|
-
|
|
856
|
+
if (switchGuid && switchExpire && switchExpire.getTime() > Date.now()) {
|
|
857
|
+
// Load the switch user.
|
|
858
|
+
const switchUser = await this.User.factory(switchGuid);
|
|
859
|
+
if (switchUser.guid == null) {
|
|
860
|
+
await this.logoutSwitch();
|
|
861
|
+
}
|
|
862
|
+
else {
|
|
863
|
+
await this.fillSession(switchUser);
|
|
864
|
+
if (this.response) {
|
|
865
|
+
this.response.locals.user = switchUser;
|
|
866
|
+
}
|
|
867
|
+
return true;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
if (!skipRenew &&
|
|
871
|
+
expire.getTime() < Date.now() + this.config.jwtRenew * 1000) {
|
|
872
|
+
// If the user is less than renew time from needing a new token, give them
|
|
873
|
+
// a new one.
|
|
874
|
+
await this.login(user, fromAuthHeader);
|
|
594
875
|
}
|
|
595
876
|
else {
|
|
596
|
-
this.fillSession(user);
|
|
877
|
+
await this.fillSession(user);
|
|
878
|
+
}
|
|
879
|
+
if (this.response) {
|
|
880
|
+
this.response.locals.user = user;
|
|
597
881
|
}
|
|
598
882
|
return true;
|
|
599
883
|
}
|
|
600
|
-
|
|
884
|
+
/**
|
|
885
|
+
* Logs the given user into the system.
|
|
886
|
+
*
|
|
887
|
+
* @param user The user.
|
|
888
|
+
* @param sendAuthHeader Send the auth token as a custom header.
|
|
889
|
+
* @param sendCookie Send the auth token as a cookie.
|
|
890
|
+
* @returns True on success, false on failure.
|
|
891
|
+
*/
|
|
892
|
+
async login(user, sendAuthHeader, sendCookie = true) {
|
|
601
893
|
if (user.guid != null && user.enabled) {
|
|
602
|
-
if (this.response
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
894
|
+
if (this.response) {
|
|
895
|
+
if (!this.response.headersSent) {
|
|
896
|
+
const token = this.config.jwtBuilder(this.config, user);
|
|
897
|
+
if (sendCookie) {
|
|
898
|
+
const appUrl = new url_1.URL(this.config.appUrl);
|
|
899
|
+
this.response.cookie('TILMELDAUTH', token, {
|
|
900
|
+
domain: this.config.cookieDomain,
|
|
901
|
+
path: this.config.cookiePath,
|
|
902
|
+
maxAge: this.config.jwtExpire * 1000,
|
|
903
|
+
secure: appUrl.protocol === 'https:',
|
|
904
|
+
httpOnly: false, // Allow JS access (for CSRF protection).
|
|
905
|
+
sameSite: appUrl.protocol === 'https:' ? 'lax' : 'strict',
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
if (sendAuthHeader) {
|
|
909
|
+
this.response.set('X-TILMELDAUTH', token);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
this.response.locals.user = user;
|
|
913
|
+
}
|
|
914
|
+
await this.fillSession(user);
|
|
915
|
+
return true;
|
|
916
|
+
}
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Adds a switch auth token for the given user.
|
|
921
|
+
*
|
|
922
|
+
* This effectively logs the current user in to the system as the given user.
|
|
923
|
+
*
|
|
924
|
+
* @param user The user.
|
|
925
|
+
* @param sendAuthHeader Send the auth token as a custom header.
|
|
926
|
+
* @param sendCookie Send the auth token as a cookie.
|
|
927
|
+
* @returns True on success, false on failure.
|
|
928
|
+
*/
|
|
929
|
+
async loginSwitch(user, sendAuthHeader, sendCookie = true) {
|
|
930
|
+
if (user.guid != null) {
|
|
931
|
+
if (this.response) {
|
|
932
|
+
if (!this.response.headersSent) {
|
|
933
|
+
const token = this.config.jwtBuilder(this.config, user, true);
|
|
934
|
+
if (sendCookie) {
|
|
935
|
+
const appUrl = new url_1.URL(this.config.appUrl);
|
|
936
|
+
this.response.cookie('TILMELDSWITCH', token, {
|
|
937
|
+
domain: this.config.cookieDomain,
|
|
938
|
+
path: this.config.cookiePath,
|
|
939
|
+
maxAge: this.config.jwtSwitchExpire * 1000,
|
|
940
|
+
secure: appUrl.protocol === 'https:',
|
|
941
|
+
httpOnly: false, // Allow JS access (for PubSub auth token).
|
|
942
|
+
sameSite: appUrl.protocol === 'https:' ? 'lax' : 'strict',
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
if (sendAuthHeader) {
|
|
946
|
+
this.response.set('X-TILMELDSWITCH', token);
|
|
947
|
+
}
|
|
615
948
|
}
|
|
949
|
+
this.response.locals.user = user;
|
|
616
950
|
}
|
|
617
|
-
this.fillSession(user);
|
|
951
|
+
await this.fillSession(user);
|
|
618
952
|
return true;
|
|
619
953
|
}
|
|
620
954
|
return false;
|
|
621
955
|
}
|
|
622
|
-
|
|
956
|
+
/**
|
|
957
|
+
* Logs the current user out of the system.
|
|
958
|
+
*
|
|
959
|
+
* @param clearCookie Clear the auth cookie. (Also send a header.)
|
|
960
|
+
*/
|
|
961
|
+
async logout(clearCookie = true) {
|
|
962
|
+
if (this.request && !this.alreadyLoggedOutSwitch) {
|
|
963
|
+
const cookies = this.request.cookies ?? {};
|
|
964
|
+
if ('TILMELDSWITCH' in cookies) {
|
|
965
|
+
this.alreadyLoggedOutSwitch = true;
|
|
966
|
+
return await this.logoutSwitch(clearCookie);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
623
969
|
this.clearSession();
|
|
624
|
-
if (this.response
|
|
625
|
-
this.response.
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
970
|
+
if (this.response) {
|
|
971
|
+
if (clearCookie && !this.response.headersSent) {
|
|
972
|
+
this.response.clearCookie('TILMELDAUTH', {
|
|
973
|
+
domain: this.config.cookieDomain,
|
|
974
|
+
path: this.config.cookiePath,
|
|
975
|
+
});
|
|
976
|
+
this.response.set('X-TILMELDAUTH', '');
|
|
977
|
+
}
|
|
978
|
+
this.response.locals.user = null;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Clears the switch user out of the system.
|
|
983
|
+
*
|
|
984
|
+
* @param clearCookie Clear the auth cookie. (Also send a header.)
|
|
985
|
+
*/
|
|
986
|
+
async logoutSwitch(clearCookie = true) {
|
|
987
|
+
this.clearSession();
|
|
988
|
+
if (this.response) {
|
|
989
|
+
if (clearCookie && !this.response.headersSent) {
|
|
990
|
+
this.response.clearCookie('TILMELDSWITCH', {
|
|
991
|
+
domain: this.config.cookieDomain,
|
|
992
|
+
path: this.config.cookiePath,
|
|
993
|
+
});
|
|
994
|
+
this.response.set('X-TILMELDSWITCH', '');
|
|
995
|
+
}
|
|
996
|
+
this.response.locals.user = null;
|
|
997
|
+
await this.authenticate(false, true);
|
|
630
998
|
}
|
|
631
999
|
}
|
|
632
1000
|
}
|