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