@nymphjs/tilmeld 1.0.0-beta.2 → 1.0.0-beta.21

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() {
@@ -105,6 +112,7 @@ export default class Tilmeld implements TilmeldInterface {
105
112
  */
106
113
  public init(nymph: Nymph) {
107
114
  this.nymph = nymph;
115
+ const tilmeld = this;
108
116
 
109
117
  // Set up access control hooks when Nymph is called.
110
118
  if (this.nymph.driver == null) {
@@ -112,18 +120,18 @@ export default class Tilmeld implements TilmeldInterface {
112
120
  }
113
121
 
114
122
  // Configure the classes.
115
- class NymphUser extends User {}
116
- NymphUser.nymph = this.nymph;
117
- NymphUser.tilmeld = this;
123
+ class NymphUser extends User {
124
+ static nymph: Nymph = nymph;
125
+ static tilmeld: Tilmeld = tilmeld;
126
+ }
118
127
  (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;
128
+ this.User = this.nymph.addEntityClass(NymphUser);
129
+ class NymphGroup extends Group {
130
+ static nymph: Nymph = nymph;
131
+ static tilmeld: Tilmeld = tilmeld;
132
+ }
124
133
  (NymphUser as any).skipOnClone = true;
125
- this.Group = NymphGroup;
126
- this.nymph.addEntityClass(NymphGroup);
134
+ this.Group = this.nymph.addEntityClass(NymphGroup);
127
135
 
128
136
  this.initAccessControl();
129
137
  if (this.request != null && !this.skipAuthenticate) {
@@ -150,12 +158,12 @@ export default class Tilmeld implements TilmeldInterface {
150
158
 
151
159
  private initAccessControl() {
152
160
  // Check for the skip access control option and add AC selectors.
153
- const handleQuery = async function (
161
+ const handleQuery = function (
154
162
  nymph: Nymph,
155
163
  options: Options,
156
164
  selectors: FormattedSelector[]
157
165
  ) {
158
- const tilmeld = nymph.tilmeld as Tilmeld;
166
+ const tilmeld = enforceTilmeld(nymph);
159
167
  if (
160
168
  !options ||
161
169
  !('skipAc' in options) ||
@@ -195,7 +203,7 @@ export default class Tilmeld implements TilmeldInterface {
195
203
  nymph: Nymph,
196
204
  entity: EntityInterface
197
205
  ) {
198
- const tilmeld = nymph.tilmeld as Tilmeld;
206
+ const tilmeld = enforceTilmeld(nymph);
199
207
  if (
200
208
  typeof entity.$tilmeldDeleteSkipAC === 'function' &&
201
209
  entity.$tilmeldDeleteSkipAC()
@@ -231,7 +239,7 @@ export default class Tilmeld implements TilmeldInterface {
231
239
  nymph: Nymph,
232
240
  entity: EntityInterface & AccessControlData
233
241
  ) {
234
- const tilmeld = nymph.tilmeld as Tilmeld;
242
+ const tilmeld = enforceTilmeld(nymph);
235
243
  if (!entity) {
236
244
  return;
237
245
  }
@@ -342,11 +350,10 @@ export default class Tilmeld implements TilmeldInterface {
342
350
  }
343
351
 
344
352
  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')
353
+ (originalAc.user && !originalAc.user.$is(newAc.user)) ||
354
+ (!originalAc.user &&
355
+ newAc.user &&
356
+ !newAc.user.$is(tilmeld.currentUser))
350
357
  ) {
351
358
  throw new AccessControlError(
352
359
  'No permission to assign to another user.'
@@ -359,8 +366,7 @@ export default class Tilmeld implements TilmeldInterface {
359
366
  !(
360
367
  newAc.group.$is(tilmeld.currentUser?.group) ||
361
368
  newAc.group.$inArray(tilmeld.currentUser?.groups ?? [])
362
- ) &&
363
- !tilmeld.gatekeeper('tilmeld/admin')
369
+ )
364
370
  ) {
365
371
  throw new AccessControlError(
366
372
  'No permission to assign to another group.'
@@ -398,7 +404,7 @@ export default class Tilmeld implements TilmeldInterface {
398
404
  nymph: Nymph,
399
405
  entity: EntityInterface & AccessControlData
400
406
  ) {
401
- const tilmeld = nymph.tilmeld as Tilmeld;
407
+ const tilmeld = enforceTilmeld(nymph);
402
408
  const user = tilmeld.currentUser;
403
409
  if (
404
410
  user != null &&
@@ -530,14 +536,6 @@ export default class Tilmeld implements TilmeldInterface {
530
536
  ) {
531
537
  const user = this.currentUser;
532
538
 
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
539
  if (options == null) {
542
540
  throw new Error('No options in argument.');
543
541
  } else if (
@@ -669,7 +667,7 @@ export default class Tilmeld implements TilmeldInterface {
669
667
  * - FULL_ACCESS, the user has read, write, and delete access to the entity,
670
668
  * as well as being able to manage its access controls and ownership.
671
669
  *
672
- * These properties defaults to:
670
+ * These properties default to:
673
671
  *
674
672
  * - acUser = TilmeldAccessLevels.FULL_ACCESS
675
673
  * - acGroup = TilmeldAccessLevels.READ_ACCESS
@@ -684,9 +682,7 @@ export default class Tilmeld implements TilmeldInterface {
684
682
  * The following conditions will result in different checks, which determine
685
683
  * whether the check passes:
686
684
  *
687
- * - The user has the "system/admin" ability. (Always true.)
688
685
  * - 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
686
  * - No user is logged in. (Check other AC.)
691
687
  * - The entity is the user. (Always true.)
692
688
  * - It is the user's primary group. (True for READ_ACCESS.)
@@ -727,30 +723,22 @@ export default class Tilmeld implements TilmeldInterface {
727
723
  userOrEmpty = user;
728
724
  }
729
725
 
730
- if (userOrEmpty.$gatekeeper('system/admin')) {
731
- return true;
732
- }
733
-
734
726
  // Users and groups are always readable. Editable by Tilmeld admins.
735
727
  if (
736
728
  (entity instanceof User || entity instanceof Group) &&
737
729
  (type === TilmeldAccessLevels.READ_ACCESS ||
738
- userOrEmpty.$gatekeeper('tilmeld/admin'))
730
+ userOrEmpty.abilities?.includes('tilmeld/admin') ||
731
+ userOrEmpty.abilities?.includes('system/admin'))
739
732
  ) {
740
733
  return true;
741
734
  }
742
735
 
743
- // Entities with no owners are always editable.
744
- if (entity.user == null && entity.group == null) {
745
- return true;
746
- }
747
-
748
736
  // Load access control, since we need it now...
749
737
  const acUser = entity.acUser ?? TilmeldAccessLevels.FULL_ACCESS;
750
738
  const acGroup = entity.acGroup ?? TilmeldAccessLevels.READ_ACCESS;
751
739
  const acOther = entity.acOther ?? TilmeldAccessLevels.NO_ACCESS;
752
740
 
753
- if (userOrNull === null) {
741
+ if (userOrNull == null) {
754
742
  return acOther >= type;
755
743
  }
756
744
 
@@ -860,7 +848,7 @@ export default class Tilmeld implements TilmeldInterface {
860
848
  userOrEmpty = user;
861
849
  }
862
850
 
863
- if (userOrEmpty.$gatekeeper('system/admin')) {
851
+ if (userOrEmpty.abilities?.includes('system/admin')) {
864
852
  return true;
865
853
  }
866
854
 
@@ -948,7 +936,7 @@ export default class Tilmeld implements TilmeldInterface {
948
936
  guid: guid,
949
937
  }
950
938
  );
951
- if (!user || !user.guid || !user.enabled) {
939
+ if (!user || !user.guid) {
952
940
  return null;
953
941
  }
954
942
 
@@ -956,7 +944,17 @@ export default class Tilmeld implements TilmeldInterface {
956
944
  }
957
945
 
958
946
  /**
959
- * Check for a TILMELDAUTH token, and, if set, authenticate from it.
947
+ * Check for TILMELDAUTH and TILMELDSWITCH tokens, and, if set, authenticate
948
+ * from it/them.
949
+ *
950
+ * You can also call this function after setting `response.locals.user` to the
951
+ * user you want to authenticate. You *should* check for `user.enabled` before
952
+ * setting this variable, unless you explicitly want to log in as a disabled
953
+ * user. (The user must be an instance of the User class for this Tilmeld
954
+ * instance.)
955
+ *
956
+ * This function will set `response.locals.user` to the logged in user on
957
+ * successful authentication.
960
958
  *
961
959
  * @param skipXsrfToken Skip the XSRF token check.
962
960
  * @returns True if a user was authenticated, false on any failure.
@@ -966,30 +964,52 @@ export default class Tilmeld implements TilmeldInterface {
966
964
  return false;
967
965
  }
968
966
 
967
+ if (
968
+ this.response &&
969
+ this.response.locals.user &&
970
+ this.response.locals.user instanceof this.User
971
+ ) {
972
+ // The user has already been authenticated through some other means.
973
+ const user = this.response.locals.user;
974
+ this.fillSession(user);
975
+ return true;
976
+ }
977
+
969
978
  const cookies = this.request.cookies ?? {};
970
979
 
971
980
  // If a client does't support cookies, they can use the X-TILMELDAUTH header
972
981
  // to provide the auth token.
973
982
  let fromAuthHeader = false;
974
983
  let authToken: string;
984
+ let switchToken: string | null;
975
985
  if (this.request.header('x-tilmeldauth') != null) {
976
986
  fromAuthHeader = true;
977
987
  authToken = this.request.header('x-tilmeldauth') as string;
988
+ switchToken =
989
+ (this.request.header('x-tilmeldswitch') as string) ||
990
+ 'TILMELDSWITCH' in cookies
991
+ ? cookies.TILMELDSWITCH
992
+ : null;
978
993
  } else if ('TILMELDAUTH' in cookies) {
979
994
  fromAuthHeader = false;
980
995
  authToken = cookies.TILMELDAUTH;
996
+ switchToken = 'TILMELDSWITCH' in cookies ? cookies.TILMELDSWITCH : null;
981
997
  } else {
982
998
  return false;
983
999
  }
984
1000
 
985
- let extract: { guid: string; expire: Date } | null;
1001
+ let authExtract: { guid: string; issued: Date; expire: Date } | null;
1002
+ let switchExtract: { guid: string; expire: Date } | null;
986
1003
  if (
987
1004
  skipXsrfToken ||
988
1005
  this.request.originalUrl.startsWith(this.config.setupPath)
989
1006
  ) {
990
1007
  // The request is for the setup app, or we were told to skip the XSRF
991
1008
  // check, so don't check for the XSRF token.
992
- extract = this.config.jwtExtract(this.config, authToken);
1009
+ authExtract = this.config.jwtExtract(this.config, authToken);
1010
+ switchExtract = switchToken
1011
+ ? this.config.jwtExtract(this.config, switchToken)
1012
+ : null;
993
1013
  } else {
994
1014
  // The request is for something else, so check for a valid XSRF token,
995
1015
  // unless the auth token is provided by a header (instead of a cookie).
@@ -998,28 +1018,62 @@ export default class Tilmeld implements TilmeldInterface {
998
1018
  return false;
999
1019
  }
1000
1020
 
1001
- extract = this.config.jwtExtract(this.config, authToken, xsrfToken);
1021
+ authExtract = this.config.jwtExtract(this.config, authToken, xsrfToken);
1022
+ switchExtract = switchToken
1023
+ ? this.config.jwtExtract(this.config, switchToken)
1024
+ : null;
1002
1025
  }
1003
1026
 
1004
- if (extract == null) {
1027
+ if (authExtract == null) {
1005
1028
  this.logout();
1006
1029
  return false;
1007
1030
  }
1008
- const { guid, expire } = extract;
1031
+ const { guid, issued, expire } = authExtract;
1032
+ const { guid: switchGuid, expire: switchExpire } = switchExtract || {
1033
+ guid: null,
1034
+ expire: null,
1035
+ };
1009
1036
 
1010
1037
  const user = this.User.factorySync(guid);
1011
- if (user.guid == null || !user.enabled) {
1038
+ if (
1039
+ user.guid == null ||
1040
+ !user.enabled ||
1041
+ expire.getTime() <= Date.now() ||
1042
+ issued.getTime() > Date.now() ||
1043
+ (user.revokeTokenDate != null && issued.getTime() <= user.revokeTokenDate)
1044
+ ) {
1012
1045
  this.logout();
1013
1046
  return false;
1014
1047
  }
1015
1048
 
1016
- if (expire.valueOf() < Date.now() + this.config.jwtRenew * 1000) {
1049
+ if (switchGuid && switchExpire && switchExpire.getTime() > Date.now()) {
1050
+ // Load the switch user.
1051
+ const switchUser = this.User.factorySync(switchGuid);
1052
+ if (switchUser.guid == null) {
1053
+ this.logoutSwitch();
1054
+ } else {
1055
+ this.fillSession(switchUser);
1056
+
1057
+ if (this.response) {
1058
+ this.response.locals.user = switchUser;
1059
+ }
1060
+
1061
+ return true;
1062
+ }
1063
+ }
1064
+
1065
+ if (expire.getTime() < Date.now() + this.config.jwtRenew * 1000) {
1017
1066
  // If the user is less than renew time from needing a new token, give them
1018
1067
  // a new one.
1019
1068
  this.login(user, fromAuthHeader);
1020
1069
  } else {
1021
1070
  this.fillSession(user);
1022
1071
  }
1072
+
1073
+ if (this.response) {
1074
+ this.response.locals.user = user;
1075
+ }
1076
+
1023
1077
  return true;
1024
1078
  }
1025
1079
 
@@ -1027,26 +1081,86 @@ export default class Tilmeld implements TilmeldInterface {
1027
1081
  * Logs the given user into the system.
1028
1082
  *
1029
1083
  * @param user The user.
1030
- * @param sendAuthHeader When true, a custom header with the auth token will be sent.
1084
+ * @param sendAuthHeader Send the auth token as a custom header.
1085
+ * @param sendCookie Send the auth token as a cookie.
1031
1086
  * @returns True on success, false on failure.
1032
1087
  */
1033
- public login(user: User & UserData, sendAuthHeader: boolean) {
1088
+ public login(
1089
+ user: User & UserData,
1090
+ sendAuthHeader: boolean,
1091
+ sendCookie = true
1092
+ ) {
1034
1093
  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);
1094
+ if (this.response) {
1095
+ if (!this.response.headersSent) {
1096
+ const token = this.config.jwtBuilder(this.config, user);
1097
+
1098
+ if (sendCookie) {
1099
+ const appUrl = new URL(this.config.appUrl);
1100
+ this.response.cookie('TILMELDAUTH', token, {
1101
+ domain: this.config.cookieDomain,
1102
+ path: this.config.cookiePath,
1103
+ maxAge: this.config.jwtExpire * 1000,
1104
+ secure: appUrl.protocol === 'https',
1105
+ httpOnly: false, // Allow JS access (for CSRF protection).
1106
+ sameSite: appUrl.protocol === 'https' ? 'lax' : 'strict',
1107
+ });
1108
+ }
1109
+
1110
+ if (sendAuthHeader) {
1111
+ this.response.set('X-TILMELDAUTH', token);
1112
+ }
1048
1113
  }
1114
+
1115
+ this.response.locals.user = user;
1049
1116
  }
1117
+
1118
+ this.fillSession(user);
1119
+ return true;
1120
+ }
1121
+ return false;
1122
+ }
1123
+
1124
+ /**
1125
+ * Adds a switch auth token for the given user.
1126
+ *
1127
+ * This effectively logs the current user in to the system as the given user.
1128
+ *
1129
+ * @param user The user.
1130
+ * @param sendAuthHeader Send the auth token as a custom header.
1131
+ * @param sendCookie Send the auth token as a cookie.
1132
+ * @returns True on success, false on failure.
1133
+ */
1134
+ public loginSwitch(
1135
+ user: User & UserData,
1136
+ sendAuthHeader: boolean,
1137
+ sendCookie = true
1138
+ ) {
1139
+ if (user.guid != null) {
1140
+ if (this.response) {
1141
+ if (!this.response.headersSent) {
1142
+ const token = this.config.jwtBuilder(this.config, user, true);
1143
+
1144
+ if (sendCookie) {
1145
+ const appUrl = new URL(this.config.appUrl);
1146
+ this.response.cookie('TILMELDSWITCH', token, {
1147
+ domain: this.config.cookieDomain,
1148
+ path: this.config.cookiePath,
1149
+ maxAge: this.config.jwtSwitchExpire * 1000,
1150
+ secure: appUrl.protocol === 'https',
1151
+ httpOnly: false, // Allow JS access (for PubSub auth token).
1152
+ sameSite: appUrl.protocol === 'https' ? 'lax' : 'strict',
1153
+ });
1154
+ }
1155
+
1156
+ if (sendAuthHeader) {
1157
+ this.response.set('X-TILMELDSWITCH', token);
1158
+ }
1159
+ }
1160
+
1161
+ this.response.locals.user = user;
1162
+ }
1163
+
1050
1164
  this.fillSession(user);
1051
1165
  return true;
1052
1166
  }
@@ -1055,15 +1169,50 @@ export default class Tilmeld implements TilmeldInterface {
1055
1169
 
1056
1170
  /**
1057
1171
  * Logs the current user out of the system.
1172
+ *
1173
+ * @param clearCookie Clear the auth cookie. (Also send a header.)
1058
1174
  */
1059
- public logout() {
1175
+ public logout(clearCookie = true) {
1176
+ if (this.request && !this.alreadyLoggedOutSwitch) {
1177
+ const cookies = this.request.cookies ?? {};
1178
+
1179
+ if ('TILMELDSWITCH' in cookies) {
1180
+ this.alreadyLoggedOutSwitch = true;
1181
+ return this.logoutSwitch(clearCookie);
1182
+ }
1183
+ }
1184
+
1060
1185
  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', '');
1186
+ if (this.response) {
1187
+ if (clearCookie && !this.response.headersSent) {
1188
+ this.response.clearCookie('TILMELDAUTH', {
1189
+ domain: this.config.cookieDomain,
1190
+ path: this.config.cookiePath,
1191
+ });
1192
+ this.response.set('X-TILMELDAUTH', '');
1193
+ }
1194
+ this.response.locals.user = null;
1195
+ }
1196
+ }
1197
+
1198
+ /**
1199
+ * Clears the switch user out of the system.
1200
+ *
1201
+ * @param clearCookie Clear the auth cookie. (Also send a header.)
1202
+ */
1203
+ public logoutSwitch(clearCookie = true) {
1204
+ this.clearSession();
1205
+ if (this.response) {
1206
+ if (clearCookie && !this.response.headersSent) {
1207
+ this.response.clearCookie('TILMELDSWITCH', {
1208
+ domain: this.config.cookieDomain,
1209
+ path: this.config.cookiePath,
1210
+ });
1211
+ this.response.set('X-TILMELDSWITCH', '');
1212
+ }
1213
+ this.response.locals.user = null;
1214
+
1215
+ this.authenticate();
1067
1216
  }
1068
1217
  }
1069
1218