@nymphjs/tilmeld 1.0.0-beta.3 → 1.0.0-beta.30

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/src/Tilmeld.ts CHANGED
@@ -15,6 +15,7 @@ import { AccessControlError } from './errors';
15
15
  import Group from './Group';
16
16
  import User, { UserData } from './User';
17
17
  import { AccessControlData } from './Tilmeld.types';
18
+ import { enforceTilmeld } from './enforceTilmeld';
18
19
 
19
20
  /**
20
21
  * A user and group system for Nymph.js.
@@ -73,6 +74,11 @@ export default class Tilmeld implements TilmeldInterface {
73
74
  */
74
75
  private skipAuthenticate = false;
75
76
 
77
+ /**
78
+ * Used to avoid infinite loop.
79
+ */
80
+ private alreadyLoggedOutSwitch = false;
81
+
76
82
  /**
77
83
  * Create a new instance of Tilmeld.
78
84
  *
@@ -84,6 +90,7 @@ export default class Tilmeld implements TilmeldInterface {
84
90
 
85
91
  /**
86
92
  * This is used internally by Nymph. Don't call it yourself.
93
+ *
87
94
  * @returns A clone of this instance.
88
95
  */
89
96
  public clone() {
@@ -106,25 +113,40 @@ export default class Tilmeld implements TilmeldInterface {
106
113
  public init(nymph: Nymph) {
107
114
  this.nymph = nymph;
108
115
 
109
- // Set up access control hooks when Nymph is called.
110
116
  if (this.nymph.driver == null) {
111
117
  throw new Error("Tilmeld can't be configured before Nymph.");
112
118
  }
113
119
 
114
120
  // Configure the classes.
115
- class NymphUser extends User {}
116
- NymphUser.nymph = this.nymph;
117
- NymphUser.tilmeld = this;
118
- (NymphUser as any).skipOnClone = true;
119
- this.User = NymphUser;
120
- this.nymph.addEntityClass(NymphUser);
121
- class NymphGroup extends Group {}
122
- NymphGroup.nymph = this.nymph;
123
- NymphGroup.tilmeld = this;
124
- (NymphUser as any).skipOnClone = true;
125
- this.Group = NymphGroup;
126
- this.nymph.addEntityClass(NymphGroup);
121
+ if (nymph.parent) {
122
+ // This is a cloned Nymph, it should have classes already.
123
+ this.User = this.nymph.getEntityClass(User);
124
+ this.Group = this.nymph.getEntityClass(Group);
125
+ } else {
126
+ // This is a new Nymph, add the classes.
127
+ this.User = this.nymph.addEntityClass(User);
128
+ this.Group = this.nymph.addEntityClass(Group);
129
+ }
130
+
131
+ // Clone the current user so its Nymph instance is correct.
132
+ if (this.currentUser) {
133
+ const currentUserGUID = this.currentUser.guid;
134
+ const currentUserCDate = this.currentUser.cdate;
135
+ const currentUserMDate = this.currentUser.mdate;
136
+ const currentUserTags = this.currentUser.tags;
137
+ const currentUserData = this.currentUser.$getData();
138
+ const currentUserSData = this.currentUser.$getSData();
139
+
140
+ const user = this.User.factorySync();
141
+ user.guid = currentUserGUID;
142
+ user.cdate = currentUserCDate;
143
+ user.mdate = currentUserMDate;
144
+ user.tags = currentUserTags;
145
+ user.$putData(currentUserData, currentUserSData);
146
+ this.fillSession(user);
147
+ }
127
148
 
149
+ // Set up access control hooks when Nymph is called.
128
150
  this.initAccessControl();
129
151
  if (this.request != null && !this.skipAuthenticate) {
130
152
  this.authenticate();
@@ -155,7 +177,7 @@ export default class Tilmeld implements TilmeldInterface {
155
177
  options: Options,
156
178
  selectors: FormattedSelector[]
157
179
  ) {
158
- const tilmeld = nymph.tilmeld as Tilmeld;
180
+ const tilmeld = enforceTilmeld(nymph);
159
181
  if (
160
182
  !options ||
161
183
  !('skipAc' in options) ||
@@ -195,7 +217,7 @@ export default class Tilmeld implements TilmeldInterface {
195
217
  nymph: Nymph,
196
218
  entity: EntityInterface
197
219
  ) {
198
- const tilmeld = nymph.tilmeld as Tilmeld;
220
+ const tilmeld = enforceTilmeld(nymph);
199
221
  if (
200
222
  typeof entity.$tilmeldDeleteSkipAC === 'function' &&
201
223
  entity.$tilmeldDeleteSkipAC()
@@ -231,7 +253,7 @@ export default class Tilmeld implements TilmeldInterface {
231
253
  nymph: Nymph,
232
254
  entity: EntityInterface & AccessControlData
233
255
  ) {
234
- const tilmeld = nymph.tilmeld as Tilmeld;
256
+ const tilmeld = enforceTilmeld(nymph);
235
257
  if (!entity) {
236
258
  return;
237
259
  }
@@ -342,11 +364,10 @@ export default class Tilmeld implements TilmeldInterface {
342
364
  }
343
365
 
344
366
  if (
345
- ((originalAc.user && !originalAc.user.$is(newAc.user)) ||
346
- (!originalAc.user &&
347
- newAc.user &&
348
- !newAc.user.$is(tilmeld.currentUser))) &&
349
- !tilmeld.gatekeeper('tilmeld/admin')
367
+ (originalAc.user && !originalAc.user.$is(newAc.user)) ||
368
+ (!originalAc.user &&
369
+ newAc.user &&
370
+ !newAc.user.$is(tilmeld.currentUser))
350
371
  ) {
351
372
  throw new AccessControlError(
352
373
  'No permission to assign to another user.'
@@ -359,8 +380,7 @@ export default class Tilmeld implements TilmeldInterface {
359
380
  !(
360
381
  newAc.group.$is(tilmeld.currentUser?.group) ||
361
382
  newAc.group.$inArray(tilmeld.currentUser?.groups ?? [])
362
- ) &&
363
- !tilmeld.gatekeeper('tilmeld/admin')
383
+ )
364
384
  ) {
365
385
  throw new AccessControlError(
366
386
  'No permission to assign to another group.'
@@ -398,7 +418,7 @@ export default class Tilmeld implements TilmeldInterface {
398
418
  nymph: Nymph,
399
419
  entity: EntityInterface & AccessControlData
400
420
  ) {
401
- const tilmeld = nymph.tilmeld as Tilmeld;
421
+ const tilmeld = enforceTilmeld(nymph);
402
422
  const user = tilmeld.currentUser;
403
423
  if (
404
424
  user != null &&
@@ -530,14 +550,6 @@ export default class Tilmeld implements TilmeldInterface {
530
550
  ) {
531
551
  const user = this.currentUser;
532
552
 
533
- if (
534
- user != null &&
535
- (user.abilities?.indexOf('system/admin') ?? -1) !== -1
536
- ) {
537
- // The user is a system admin, so they can see everything.
538
- return;
539
- }
540
-
541
553
  if (options == null) {
542
554
  throw new Error('No options in argument.');
543
555
  } else if (
@@ -669,7 +681,7 @@ export default class Tilmeld implements TilmeldInterface {
669
681
  * - FULL_ACCESS, the user has read, write, and delete access to the entity,
670
682
  * as well as being able to manage its access controls and ownership.
671
683
  *
672
- * These properties defaults to:
684
+ * These properties default to:
673
685
  *
674
686
  * - acUser = TilmeldAccessLevels.FULL_ACCESS
675
687
  * - acGroup = TilmeldAccessLevels.READ_ACCESS
@@ -684,9 +696,7 @@ export default class Tilmeld implements TilmeldInterface {
684
696
  * The following conditions will result in different checks, which determine
685
697
  * whether the check passes:
686
698
  *
687
- * - The user has the "system/admin" ability. (Always true.)
688
699
  * - It is a user or group. (True for READ_ACCESS or Tilmeld admins.)
689
- * - The entity has no "user" and no "group". (Always true.)
690
700
  * - No user is logged in. (Check other AC.)
691
701
  * - The entity is the user. (Always true.)
692
702
  * - It is the user's primary group. (True for READ_ACCESS.)
@@ -727,30 +737,22 @@ export default class Tilmeld implements TilmeldInterface {
727
737
  userOrEmpty = user;
728
738
  }
729
739
 
730
- if (userOrEmpty.$gatekeeper('system/admin')) {
731
- return true;
732
- }
733
-
734
740
  // Users and groups are always readable. Editable by Tilmeld admins.
735
741
  if (
736
742
  (entity instanceof User || entity instanceof Group) &&
737
743
  (type === TilmeldAccessLevels.READ_ACCESS ||
738
- userOrEmpty.$gatekeeper('tilmeld/admin'))
744
+ userOrEmpty.abilities?.includes('tilmeld/admin') ||
745
+ userOrEmpty.abilities?.includes('system/admin'))
739
746
  ) {
740
747
  return true;
741
748
  }
742
749
 
743
- // Entities with no owners are always editable.
744
- if (entity.user == null && entity.group == null) {
745
- return true;
746
- }
747
-
748
750
  // Load access control, since we need it now...
749
751
  const acUser = entity.acUser ?? TilmeldAccessLevels.FULL_ACCESS;
750
752
  const acGroup = entity.acGroup ?? TilmeldAccessLevels.READ_ACCESS;
751
753
  const acOther = entity.acOther ?? TilmeldAccessLevels.NO_ACCESS;
752
754
 
753
- if (userOrNull === null) {
755
+ if (userOrNull == null) {
754
756
  return acOther >= type;
755
757
  }
756
758
 
@@ -860,7 +862,7 @@ export default class Tilmeld implements TilmeldInterface {
860
862
  userOrEmpty = user;
861
863
  }
862
864
 
863
- if (userOrEmpty.$gatekeeper('system/admin')) {
865
+ if (userOrEmpty.abilities?.includes('system/admin')) {
864
866
  return true;
865
867
  }
866
868
 
@@ -948,7 +950,7 @@ export default class Tilmeld implements TilmeldInterface {
948
950
  guid: guid,
949
951
  }
950
952
  );
951
- if (!user || !user.guid || !user.enabled) {
953
+ if (!user || !user.guid) {
952
954
  return null;
953
955
  }
954
956
 
@@ -956,7 +958,17 @@ export default class Tilmeld implements TilmeldInterface {
956
958
  }
957
959
 
958
960
  /**
959
- * Check for a TILMELDAUTH token, and, if set, authenticate from it.
961
+ * Check for TILMELDAUTH and TILMELDSWITCH tokens, and, if set, authenticate
962
+ * from it/them.
963
+ *
964
+ * You can also call this function after setting `response.locals.user` to the
965
+ * user you want to authenticate. You *should* check for `user.enabled` before
966
+ * setting this variable, unless you explicitly want to log in as a disabled
967
+ * user. (The user must be an instance of the User class for this Tilmeld
968
+ * instance.)
969
+ *
970
+ * This function will set `response.locals.user` to the logged in user on
971
+ * successful authentication.
960
972
  *
961
973
  * @param skipXsrfToken Skip the XSRF token check.
962
974
  * @returns True if a user was authenticated, false on any failure.
@@ -966,30 +978,52 @@ export default class Tilmeld implements TilmeldInterface {
966
978
  return false;
967
979
  }
968
980
 
981
+ if (
982
+ this.response &&
983
+ this.response.locals.user &&
984
+ this.response.locals.user instanceof this.User
985
+ ) {
986
+ // The user has already been authenticated through some other means.
987
+ const user = this.response.locals.user;
988
+ this.fillSession(user);
989
+ return true;
990
+ }
991
+
969
992
  const cookies = this.request.cookies ?? {};
970
993
 
971
994
  // If a client does't support cookies, they can use the X-TILMELDAUTH header
972
995
  // to provide the auth token.
973
996
  let fromAuthHeader = false;
974
997
  let authToken: string;
998
+ let switchToken: string | null;
975
999
  if (this.request.header('x-tilmeldauth') != null) {
976
1000
  fromAuthHeader = true;
977
1001
  authToken = this.request.header('x-tilmeldauth') as string;
1002
+ switchToken =
1003
+ (this.request.header('x-tilmeldswitch') as string) ||
1004
+ 'TILMELDSWITCH' in cookies
1005
+ ? cookies.TILMELDSWITCH
1006
+ : null;
978
1007
  } else if ('TILMELDAUTH' in cookies) {
979
1008
  fromAuthHeader = false;
980
1009
  authToken = cookies.TILMELDAUTH;
1010
+ switchToken = 'TILMELDSWITCH' in cookies ? cookies.TILMELDSWITCH : null;
981
1011
  } else {
982
1012
  return false;
983
1013
  }
984
1014
 
985
- let extract: { guid: string; expire: Date } | null;
1015
+ let authExtract: { guid: string; issued: Date; expire: Date } | null;
1016
+ let switchExtract: { guid: string; expire: Date } | null;
986
1017
  if (
987
1018
  skipXsrfToken ||
988
1019
  this.request.originalUrl.startsWith(this.config.setupPath)
989
1020
  ) {
990
1021
  // The request is for the setup app, or we were told to skip the XSRF
991
1022
  // check, so don't check for the XSRF token.
992
- extract = this.config.jwtExtract(this.config, authToken);
1023
+ authExtract = this.config.jwtExtract(this.config, authToken);
1024
+ switchExtract = switchToken
1025
+ ? this.config.jwtExtract(this.config, switchToken)
1026
+ : null;
993
1027
  } else {
994
1028
  // The request is for something else, so check for a valid XSRF token,
995
1029
  // unless the auth token is provided by a header (instead of a cookie).
@@ -998,28 +1032,62 @@ export default class Tilmeld implements TilmeldInterface {
998
1032
  return false;
999
1033
  }
1000
1034
 
1001
- extract = this.config.jwtExtract(this.config, authToken, xsrfToken);
1035
+ authExtract = this.config.jwtExtract(this.config, authToken, xsrfToken);
1036
+ switchExtract = switchToken
1037
+ ? this.config.jwtExtract(this.config, switchToken)
1038
+ : null;
1002
1039
  }
1003
1040
 
1004
- if (extract == null) {
1041
+ if (authExtract == null) {
1005
1042
  this.logout();
1006
1043
  return false;
1007
1044
  }
1008
- const { guid, expire } = extract;
1045
+ const { guid, issued, expire } = authExtract;
1046
+ const { guid: switchGuid, expire: switchExpire } = switchExtract || {
1047
+ guid: null,
1048
+ expire: null,
1049
+ };
1009
1050
 
1010
1051
  const user = this.User.factorySync(guid);
1011
- if (user.guid == null || !user.enabled) {
1052
+ if (
1053
+ user.guid == null ||
1054
+ !user.enabled ||
1055
+ expire.getTime() <= Date.now() ||
1056
+ issued.getTime() > Date.now() ||
1057
+ (user.revokeTokenDate != null && issued.getTime() <= user.revokeTokenDate)
1058
+ ) {
1012
1059
  this.logout();
1013
1060
  return false;
1014
1061
  }
1015
1062
 
1016
- if (expire.valueOf() < Date.now() + this.config.jwtRenew * 1000) {
1063
+ if (switchGuid && switchExpire && switchExpire.getTime() > Date.now()) {
1064
+ // Load the switch user.
1065
+ const switchUser = this.User.factorySync(switchGuid);
1066
+ if (switchUser.guid == null) {
1067
+ this.logoutSwitch();
1068
+ } else {
1069
+ this.fillSession(switchUser);
1070
+
1071
+ if (this.response) {
1072
+ this.response.locals.user = switchUser;
1073
+ }
1074
+
1075
+ return true;
1076
+ }
1077
+ }
1078
+
1079
+ if (expire.getTime() < Date.now() + this.config.jwtRenew * 1000) {
1017
1080
  // If the user is less than renew time from needing a new token, give them
1018
1081
  // a new one.
1019
1082
  this.login(user, fromAuthHeader);
1020
1083
  } else {
1021
1084
  this.fillSession(user);
1022
1085
  }
1086
+
1087
+ if (this.response) {
1088
+ this.response.locals.user = user;
1089
+ }
1090
+
1023
1091
  return true;
1024
1092
  }
1025
1093
 
@@ -1027,26 +1095,86 @@ export default class Tilmeld implements TilmeldInterface {
1027
1095
  * Logs the given user into the system.
1028
1096
  *
1029
1097
  * @param user The user.
1030
- * @param sendAuthHeader When true, a custom header with the auth token will be sent.
1098
+ * @param sendAuthHeader Send the auth token as a custom header.
1099
+ * @param sendCookie Send the auth token as a cookie.
1031
1100
  * @returns True on success, false on failure.
1032
1101
  */
1033
- public login(user: User & UserData, sendAuthHeader: boolean) {
1102
+ public login(
1103
+ user: User & UserData,
1104
+ sendAuthHeader: boolean,
1105
+ sendCookie = true
1106
+ ) {
1034
1107
  if (user.guid != null && user.enabled) {
1035
- if (this.response && !this.response.headersSent) {
1036
- const token = this.config.jwtBuilder(this.config, user);
1037
- const appUrl = new URL(this.config.appUrl);
1038
- this.response.cookie('TILMELDAUTH', token, {
1039
- domain: this.config.cookieDomain,
1040
- path: this.config.cookiePath,
1041
- maxAge: this.config.jwtExpire * 1000,
1042
- secure: appUrl.protocol === 'https',
1043
- httpOnly: false, // Allow JS access (for CSRF protection).
1044
- sameSite: appUrl.protocol === 'https' ? 'lax' : 'strict',
1045
- });
1046
- if (sendAuthHeader) {
1047
- this.response.set('X-TILMELDAUTH', token);
1108
+ if (this.response) {
1109
+ if (!this.response.headersSent) {
1110
+ const token = this.config.jwtBuilder(this.config, user);
1111
+
1112
+ if (sendCookie) {
1113
+ const appUrl = new URL(this.config.appUrl);
1114
+ this.response.cookie('TILMELDAUTH', token, {
1115
+ domain: this.config.cookieDomain,
1116
+ path: this.config.cookiePath,
1117
+ maxAge: this.config.jwtExpire * 1000,
1118
+ secure: appUrl.protocol === 'https',
1119
+ httpOnly: false, // Allow JS access (for CSRF protection).
1120
+ sameSite: appUrl.protocol === 'https' ? 'lax' : 'strict',
1121
+ });
1122
+ }
1123
+
1124
+ if (sendAuthHeader) {
1125
+ this.response.set('X-TILMELDAUTH', token);
1126
+ }
1127
+ }
1128
+
1129
+ this.response.locals.user = user;
1130
+ }
1131
+
1132
+ this.fillSession(user);
1133
+ return true;
1134
+ }
1135
+ return false;
1136
+ }
1137
+
1138
+ /**
1139
+ * Adds a switch auth token for the given user.
1140
+ *
1141
+ * This effectively logs the current user in to the system as the given user.
1142
+ *
1143
+ * @param user The user.
1144
+ * @param sendAuthHeader Send the auth token as a custom header.
1145
+ * @param sendCookie Send the auth token as a cookie.
1146
+ * @returns True on success, false on failure.
1147
+ */
1148
+ public loginSwitch(
1149
+ user: User & UserData,
1150
+ sendAuthHeader: boolean,
1151
+ sendCookie = true
1152
+ ) {
1153
+ if (user.guid != null) {
1154
+ if (this.response) {
1155
+ if (!this.response.headersSent) {
1156
+ const token = this.config.jwtBuilder(this.config, user, true);
1157
+
1158
+ if (sendCookie) {
1159
+ const appUrl = new URL(this.config.appUrl);
1160
+ this.response.cookie('TILMELDSWITCH', token, {
1161
+ domain: this.config.cookieDomain,
1162
+ path: this.config.cookiePath,
1163
+ maxAge: this.config.jwtSwitchExpire * 1000,
1164
+ secure: appUrl.protocol === 'https',
1165
+ httpOnly: false, // Allow JS access (for PubSub auth token).
1166
+ sameSite: appUrl.protocol === 'https' ? 'lax' : 'strict',
1167
+ });
1168
+ }
1169
+
1170
+ if (sendAuthHeader) {
1171
+ this.response.set('X-TILMELDSWITCH', token);
1172
+ }
1048
1173
  }
1174
+
1175
+ this.response.locals.user = user;
1049
1176
  }
1177
+
1050
1178
  this.fillSession(user);
1051
1179
  return true;
1052
1180
  }
@@ -1055,15 +1183,50 @@ export default class Tilmeld implements TilmeldInterface {
1055
1183
 
1056
1184
  /**
1057
1185
  * Logs the current user out of the system.
1186
+ *
1187
+ * @param clearCookie Clear the auth cookie. (Also send a header.)
1058
1188
  */
1059
- public logout() {
1189
+ public logout(clearCookie = true) {
1190
+ if (this.request && !this.alreadyLoggedOutSwitch) {
1191
+ const cookies = this.request.cookies ?? {};
1192
+
1193
+ if ('TILMELDSWITCH' in cookies) {
1194
+ this.alreadyLoggedOutSwitch = true;
1195
+ return this.logoutSwitch(clearCookie);
1196
+ }
1197
+ }
1198
+
1060
1199
  this.clearSession();
1061
- if (this.response && !this.response.headersSent) {
1062
- this.response.clearCookie('TILMELDAUTH', {
1063
- domain: this.config.cookieDomain,
1064
- path: this.config.cookiePath,
1065
- });
1066
- this.response.set('X-TILMELDAUTH', '');
1200
+ if (this.response) {
1201
+ if (clearCookie && !this.response.headersSent) {
1202
+ this.response.clearCookie('TILMELDAUTH', {
1203
+ domain: this.config.cookieDomain,
1204
+ path: this.config.cookiePath,
1205
+ });
1206
+ this.response.set('X-TILMELDAUTH', '');
1207
+ }
1208
+ this.response.locals.user = null;
1209
+ }
1210
+ }
1211
+
1212
+ /**
1213
+ * Clears the switch user out of the system.
1214
+ *
1215
+ * @param clearCookie Clear the auth cookie. (Also send a header.)
1216
+ */
1217
+ public logoutSwitch(clearCookie = true) {
1218
+ this.clearSession();
1219
+ if (this.response) {
1220
+ if (clearCookie && !this.response.headersSent) {
1221
+ this.response.clearCookie('TILMELDSWITCH', {
1222
+ domain: this.config.cookieDomain,
1223
+ path: this.config.cookiePath,
1224
+ });
1225
+ this.response.set('X-TILMELDSWITCH', '');
1226
+ }
1227
+ this.response.locals.user = null;
1228
+
1229
+ this.authenticate();
1067
1230
  }
1068
1231
  }
1069
1232