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