ep_openid_connect 2.5.16 → 3.0.1

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 ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ ## v3.0.0
4
+
5
+ Released 2022-03-04.
6
+
7
+ #### Compatibility changes
8
+
9
+ * Removed the deprecated `displayname_claim` setting. Set
10
+ `user_properties.displayname.claim` instead.
11
+ * Displayname rendering was moved to the
12
+ [ep\_user\_displayname](https://github.com/ether/ep_user_displayname#readme)
13
+ plugin and the `permit_displayname_change` setting was removed.
14
+ * The plugin is now much more strict about what it accepts as valid settings.
package/README.md CHANGED
@@ -52,18 +52,12 @@ Etherpad's `requireAuthentication` setting must be `true`.
52
52
  * `base_url` (required): The public base Etherpad URL. When registering Etherpad
53
53
  with your identity provider, the redirect URL (a.k.a. callback URL) is this
54
54
  base URL plus `/ep_openid_connect/callback`.
55
- * `displayname_claim` (optional; defaults to `"name"`): The default value of
56
- `user_properties.displayname.claim`. **Deprecated:** Set
57
- `user_properties.displayname.claim` instead.
58
55
  * `scope` (optional; defaults to `["openid"]`): List of OAuth2 scope strings.
59
- * `permit_displayname_change` (optional; defaults to `false`): Whether users may
60
- change their displayed name.
61
- * `prohibited_usernames` (optional; defaults to `['admin', 'guest']`): List of
56
+ * `prohibited_usernames` (optional; defaults to `["admin", "guest"]`): List of
62
57
  strings that will trigger an authentication error if any match the `sub`
63
58
  (subject) claim from the identity provider. Use this to avoid conflicts with
64
59
  the users in the `users` setting and to avoid conflicts with other plugins
65
- (such as
66
- [ep\_readonly\_guest](https://github.com/ether/ep_readonly_guest#readme)).
60
+ (such as [ep\_guest](https://github.com/ether/ep_guest#readme)).
67
61
  * `user_properties` (optional): Object that controls the automatic creation of
68
62
  additional properties on each authenticated user's account object. See below
69
63
  for details.
@@ -79,10 +73,11 @@ will probably look something like "5374".
79
73
  Each authenticated user gets their own account object. Default properties for a
80
74
  user's account object come from the `users` setting in `settings.json`. Etherpad
81
75
  uses the `is_admin`, `readOnly`, and `canCreate` properties to control access,
82
- and this plugin uses the `displayname` property for the name displayed in the
83
- user list. For example, the following sets the default display name to
84
- "Firstname Lastname" and the default access to read-only for the user identified
85
- by "5374":
76
+ and the
77
+ [ep\_user\_displayname](https://github.com/ether/ep_user_displayname#readme)
78
+ plugin uses the `displayname` property for the name displayed in the user list.
79
+ For example, the following sets the default display name to "Firstname Lastname"
80
+ and the default access to read-only for the user identified by "5374":
86
81
 
87
82
  ```json
88
83
  "users": {
@@ -98,10 +93,57 @@ you can use the `prohibited_usernames` settings to force an authentication error
98
93
  if the `sub` claim happens to match. This is useful for preventing a malicious
99
94
  identity provider from gaining admin access to your Etherpad instance.
100
95
 
101
- The `user_properties` setting can be used to automatically add or change
102
- properties on a user's account object when the user authenticates. The
103
- `user_properties` setting maps a property name to an object that describes how
104
- the property's value is obtained. For example:
96
+ ### Controlling user account object properties with `user_properties`
97
+
98
+ The `user_properties` setting can be used to automatically add, remove, or
99
+ change properties on a user's account object when the user authenticates. The
100
+ `user_properties` setting maps a property name to a descriptor object that
101
+ describes how the property's value is obtained:
102
+
103
+ * If the descriptor object has a `claim` property that names an existing
104
+ OpenID Connect claim, the value is set to the value of the claim. (If there
105
+ is no such claim, `claim` has no effect.)
106
+ * If the descriptor object has a `default` property and the account object
107
+ property would otherwise be unset, the property is set to the given value.
108
+ (Note that a property set to `undefined` is not the same as unset.)
109
+ * If the descriptor object is `null`, the property is removed if present.
110
+
111
+ Furthermore:
112
+
113
+ * If `user_properties` does not specifiy a descriptor for `displayname`, one
114
+ is added as follows:
115
+
116
+ ```json
117
+ "ep_openid_connect": {
118
+ "user_properties": {
119
+ "displayname": {"claim": "name"}
120
+ }
121
+ },
122
+ ```
123
+
124
+ You can cancel out this default behavior by explicitly specifying an empty
125
+ object:
126
+
127
+ ```json
128
+ "ep_openid_connect": {
129
+ "user_properties": {
130
+ "displayname": {}
131
+ }
132
+ },
133
+ ```
134
+
135
+ * The `username` property is described as follows and cannot be overridden or
136
+ canceled:
137
+
138
+ ```json
139
+ "ep_openid_connect": {
140
+ "user_properties": {
141
+ "username": {"claim": "sub"}
142
+ }
143
+ },
144
+ ```
145
+
146
+ Example:
105
147
 
106
148
  ```json
107
149
  "ep_openid_connect": {
@@ -115,7 +157,8 @@ the property's value is obtained. For example:
115
157
  },
116
158
  "fixedValue": {
117
159
  "default": "fixed value"
118
- }
160
+ },
161
+ "forcedUnset": null
119
162
  }
120
163
  },
121
164
  ```
@@ -128,6 +171,10 @@ The above example sets properties as follows:
128
171
  `claimName` claim if present, otherwise the property is left unset/unchanged.
129
172
  * Each user's `fixedValue` property is set to the string `"fixed value"`
130
173
  unless already set.
174
+ * Each user's `forcedUnset` property is always deleted if present.
175
+ * Each user's `displayname` property is set to to the value of the user's `name`
176
+ claim if present, otherwise the property is left unset/unchanged.
177
+ * Each user's `username` property is set to the value of the `sub` claim.
131
178
 
132
179
  You can use this feature to control access in the OpenID Connect provider if it
133
180
  provides suitable claims:
@@ -146,15 +193,43 @@ provides suitable claims:
146
193
  To avoid breaking assumptions made by Etherpad, the `username` property cannot
147
194
  be altered via the `user_properties` setting.
148
195
 
149
- ## Interaction with the ep\_readonly\_guest Plugin
196
+ ## Interaction with the ep\_guest Plugin
150
197
 
151
- The [ep\_readonly\_guest
152
- plugin](https://github.com/ether/ep_readonly_guest#readme) creates a user that
153
- is used for all guest accesses. It is recommended you add the username you chose
154
- for the guest user to the `prohibited_usernames` setting. If the identity
198
+ The [ep\_guest](https://github.com/ether/ep_guest#readme) plugin creates a user
199
+ that is used for all guest accesses. It is recommended you add the username you
200
+ chose for the guest user to the `prohibited_usernames` setting. If the identity
155
201
  provider ever uses that username in the `sub` claim, you will get an obvious
156
202
  error instead of a mysterious inability to edit pads.
157
203
 
204
+ ## Interaction with the ep\_user\_displayname Plugin
205
+
206
+ By default, this plugin sets the user's `displayname` property to the value of
207
+ the `name` claim. The
208
+ [ep\_user\_displayname](https://github.com/ether/ep_user_displayname#readme)
209
+ plugin uses this property (and the `displaynameChangeable` property) to control
210
+ the name displayed in the pad's list of users.
211
+
212
+ You can change the claim used to get the displayname:
213
+
214
+ ```json
215
+ "ep_openid_connect": {
216
+ "user_properties": {
217
+ "displayname": {"claim": "nickname"}
218
+ }
219
+ },
220
+ ```
221
+
222
+ Or you can cancel the default behavior:
223
+
224
+
225
+ ```json
226
+ "ep_openid_connect": {
227
+ "user_properties": {
228
+ "displayname": {}
229
+ }
230
+ },
231
+ ```
232
+
158
233
  ## Interaction with Etherpad's Built-in HTTP Basic Authentication
159
234
 
160
235
  If the user has not yet successfully authenticated, this plugin defers the
package/ep.json CHANGED
@@ -1,18 +1,14 @@
1
1
  {
2
2
  "parts": [
3
3
  {
4
- "name": "ep_openid_connect",
5
- "client_hooks": {
6
- "postToolbarInit": "ep_openid_connect/static/js/client"
7
- },
4
+ "name": "main",
8
5
  "hooks": {
9
- "authenticate": "ep_openid_connect",
10
- "authnFailure": "ep_openid_connect",
11
- "clientVars": "ep_openid_connect",
12
- "expressCreateServer": "ep_openid_connect",
13
- "handleMessage": "ep_openid_connect",
14
- "loadSettings": "ep_openid_connect",
15
- "preAuthorize": "ep_openid_connect"
6
+ "authenticate": "",
7
+ "authnFailure": "",
8
+ "expressCreateServer": "",
9
+ "init_ep_openid_connect": "",
10
+ "loadSettings": "",
11
+ "preAuthorize": ""
16
12
  }
17
13
  }
18
14
  ]
package/index.js CHANGED
@@ -1,21 +1,42 @@
1
1
  'use strict';
2
2
 
3
- const log4js = require('ep_etherpad-lite/node_modules/log4js');
3
+ const Ajv = require('ajv/dist/jtd');
4
4
  const {URL} = require('url');
5
5
  const {Issuer, generators} = require('openid-client');
6
- const authorManager = require('ep_etherpad-lite/node/db/AuthorManager');
7
6
 
8
- const logger = log4js.getLogger('ep_openid_connect');
7
+ let logger = {};
8
+ for (const level of ['debug', 'info', 'warn', 'error']) {
9
+ logger[level] = console[level].bind(console, 'ep_openid_connect:');
10
+ }
9
11
  const defaultSettings = {
10
- displayname_claim: 'name',
11
- permit_displayname_change: false,
12
12
  prohibited_usernames: ['admin', 'guest'],
13
13
  scope: ['openid'],
14
14
  user_properties: {},
15
15
  };
16
- const settings = {...defaultSettings};
16
+ let settings;
17
17
  let oidcClient = null;
18
18
 
19
+ const validSettings = new Ajv().compile({
20
+ properties: {
21
+ base_url: {type: 'string'},
22
+ client_id: {type: 'string'},
23
+ client_secret: {type: 'string'},
24
+ },
25
+ optionalProperties: {
26
+ issuer: {type: 'string'},
27
+ issuer_metadata: {},
28
+ prohibited_usernames: {elements: {type: 'string'}},
29
+ scope: {elements: {type: 'string'}},
30
+ user_properties: {values: {
31
+ optionalProperties: {
32
+ claim: {type: 'string'},
33
+ default: {type: 'string'},
34
+ },
35
+ nullable: true,
36
+ }},
37
+ },
38
+ });
39
+
19
40
  const ep = (endpoint) => `/ep_openid_connect/${endpoint}`;
20
41
  const endpointUrl = (endpoint) => new URL(ep(endpoint).substr(1), settings.base_url).toString();
21
42
 
@@ -64,35 +85,37 @@ const getIssuer = async (settings) => {
64
85
  return new Issuer(settings.issuer_metadata);
65
86
  };
66
87
 
67
- exports.loadSettings = async (hookName, {settings: {ep_openid_connect: config = {}}}) => {
68
- Object.assign(settings, config);
69
- for (const setting of ['base_url', 'client_id', 'client_secret']) {
70
- if (!settings[setting]) {
71
- logger.error(`Required setting missing from settings.json: ep_openid_connect.${setting}`);
72
- return;
73
- }
74
- }
75
- if (!settings.issuer && !settings.issuer_metadata) {
76
- logger.error(
77
- 'Either ep_openid_connect.issuer or ep_openid_connect.issuer_metadata must be set');
88
+ exports.init_ep_openid_connect = async (hookName, {logger: l}) => {
89
+ if (l != null) logger = l;
90
+ };
91
+
92
+ exports.loadSettings = async (hookName, {settings: {ep_openid_connect: s = {}}}) => {
93
+ oidcClient = null;
94
+ settings = null;
95
+ if (!validSettings(s)) {
96
+ logger.error('Invalid settings. Detailed validation errors:', validSettings.errors);
78
97
  return;
79
98
  }
80
- if (settings.issuer && settings.issuer_metadata) {
81
- logger.warn('Ignoring ep_openid_connect.issuer_metadata setting ' +
82
- 'because ep_openid_connect.issuer is set');
99
+ if ((s.issuer == null) === (s.issuer_metadata == null)) {
100
+ logger.error('Either ep_openid_connect.issuer or .issuer_metadata must be set (but not both)');
101
+ return;
83
102
  }
84
- if (settings.response_types) {
85
- logger.warn('Ignoring ep_openid_connect.response_types setting (it is no longer used)');
86
- delete settings.response_types;
103
+ if ('username' in (s.user_properties || {})) {
104
+ logger.error('ep_openid_connect.user_properties.username must not be set');
105
+ return;
87
106
  }
107
+ settings = {
108
+ ...defaultSettings,
109
+ ...s,
110
+ user_properties: {
111
+ displayname: {claim: 'name'},
112
+ ...s.user_properties,
113
+ // The username property must always match the key used in settings.users.
114
+ username: {claim: 'sub'},
115
+ },
116
+ };
88
117
  // Make sure base_url ends with '/' so that relative URLs are appended:
89
118
  if (!settings.base_url.endsWith('/')) settings.base_url += '/';
90
- settings.user_properties = {
91
- displayname: {claim: settings.displayname_claim},
92
- ...settings.user_properties,
93
- // The username property must always match the key used in settings.users.
94
- username: {claim: 'sub'},
95
- };
96
119
  logger.debug('Settings:', {...settings, client_secret: '********'});
97
120
  oidcClient = new (await getIssuer(settings)).Client({
98
121
  client_id: settings.client_id,
@@ -103,12 +126,6 @@ exports.loadSettings = async (hookName, {settings: {ep_openid_connect: config =
103
126
  logger.info('Configured.');
104
127
  };
105
128
 
106
- exports.clientVars = (hookName, context) => {
107
- if (oidcClient == null) return;
108
- const {permit_displayname_change} = settings;
109
- return {ep_openid_connect: {permit_displayname_change}};
110
- };
111
-
112
129
  exports.expressCreateServer = (hookName, {app}) => {
113
130
  logger.debug('Configuring auth routes');
114
131
  app.get(ep('callback'), async (req, res, next) => {
@@ -194,7 +211,9 @@ exports.authenticate = (hookName, {req, res, users}) => {
194
211
  req.session.user = users[userinfo.sub];
195
212
  if (req.session.user == null) req.session.user = users[userinfo.sub] = {};
196
213
  for (const [propName, descriptor] of Object.entries(settings.user_properties)) {
197
- if (descriptor.claim != null && descriptor.claim in userinfo) {
214
+ if (descriptor == null) {
215
+ delete req.session.user[propName];
216
+ } else if (descriptor.claim != null && descriptor.claim in userinfo) {
198
217
  req.session.user[propName] = userinfo[descriptor.claim];
199
218
  } else if ('default' in descriptor && !(propName in req.session.user)) {
200
219
  req.session.user[propName] = descriptor.default;
@@ -244,24 +263,6 @@ exports.authnFailure = (hookName, {req, res}) => {
244
263
  return true;
245
264
  };
246
265
 
247
- exports.handleMessage = async (hookName, {message, socket}) => {
248
- if (oidcClient == null) return;
249
- logger.debug('handleMessage hook', message);
250
- const {user: {displayname} = {}} = socket.client.request.session;
251
- if (!displayname) return;
252
- if (message.type === 'CLIENT_READY') {
253
- logger.debug(
254
- `CLIENT_READY ${socket.id}: Setting username for token ${message.token} to ${displayname}`);
255
- // TODO: author ID might come from session ID, not token.
256
- const authorId = await authorManager.getAuthor4Token(message.token);
257
- await authorManager.setAuthorName(authorId, displayname);
258
- } else if (message.type === 'COLLABROOM' && message.data.type === 'USERINFO_UPDATE') {
259
- if (message.data.userInfo.name !== displayname && !settings.permit_displayname_change) {
260
- message.data.userInfo.name = displayname;
261
- }
262
- }
263
- };
264
-
265
266
  exports.preAuthorize = (hookName, {req}) => {
266
267
  if (oidcClient == null) return;
267
268
  if (req.path.startsWith(ep(''))) return true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ep_openid_connect",
3
- "version": "2.5.16",
3
+ "version": "3.0.1",
4
4
  "description": "Etherpad plugin to authenticate users against an OpenID Connect provider.",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -23,7 +23,8 @@
23
23
  },
24
24
  "homepage": "https://github.com/ether/ep_openid_connect#readme",
25
25
  "dependencies": {
26
- "openid-client": "^5.1.3"
26
+ "ajv": "^8.10.0",
27
+ "openid-client": "^5.1.4"
27
28
  },
28
29
  "devDependencies": {
29
30
  "eslint": "^8.10.0",
@@ -26,6 +26,8 @@ class OidcProvider {
26
26
  'etherpad_is_admin',
27
27
  'etherpad_readOnly',
28
28
  'etherpad_canCreate',
29
+ 'name',
30
+ 'prop',
29
31
  ],
30
32
  },
31
33
  cookies: {
@@ -38,10 +40,12 @@ class OidcProvider {
38
40
  accountId: sub,
39
41
  claims: async (use, scope, claims, rejected) => ({
40
42
  sub,
43
+ name: 'Firstname Lastname',
41
44
  etherpad_is_admin: sub.includes('admin'),
42
45
  etherpad_readOnly: !sub.includes('admin') && sub.includes('readOnly'),
43
46
  etherpad_canCreate:
44
47
  sub.includes('admin') || (!sub.includes('readOnly') && !sub.includes('noCreate')),
48
+ ...(sub === 'claimNull' ? {prop: null} : sub === 'claimVal' ? {prop: 'claimValue'} : {}),
45
49
  }),
46
50
  }),
47
51
  jwks: {
@@ -6,6 +6,7 @@ const common = require('ep_etherpad-lite/tests/backend/common');
6
6
  const epOpenidConnect = require('../../../../index');
7
7
  const login = require('../login');
8
8
  const padManager = require('ep_etherpad-lite/node/db/PadManager');
9
+ const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugin_defs');
9
10
  const settings = require('ep_etherpad-lite/node/utils/Settings');
10
11
  const supertest = require('supertest');
11
12
 
@@ -33,33 +34,37 @@ describe(__filename, function () {
33
34
 
34
35
  before(async function () {
35
36
  await common.init();
37
+ if (!plugins.hooks.clientVars) plugins.hooks.clientVars = [];
38
+ backup.hooks = {clientVars: plugins.hooks.clientVars};
36
39
  backup.settings = {...settings};
37
- settings.requireAuthentication = true;
38
- settings.requireAuthorization = false;
39
40
  provider = new OidcProvider();
40
41
  const clients =
41
42
  [{...client, redirect_uris: [new URL('/ep_openid_connect/callback', common.baseUrl)]}];
42
43
  await provider.start({clients});
44
+ });
45
+
46
+ beforeEach(async function () {
47
+ agent = supertest.agent('');
43
48
  issuer = provider.issuer;
44
49
  pluginSettings.base_url = common.baseUrl;
45
50
  pluginSettings.issuer = issuer;
51
+ settings.requireAuthentication = true;
52
+ settings.requireAuthorization = false;
53
+ settings.users = {};
46
54
  await epOpenidConnect.loadSettings(
47
55
  'loadSettings', {settings: {ep_openid_connect: pluginSettings}});
48
56
  });
49
57
 
50
- beforeEach(async function () {
51
- agent = supertest.agent('');
52
- });
53
-
54
58
  afterEach(async function () {
55
59
  if (socket != null) socket.close();
56
60
  socket = null;
61
+ Object.assign(plugins.hooks, backup.hooks);
62
+ Object.assign(settings, backup.settings);
57
63
  });
58
64
 
59
65
  after(async function () {
60
66
  if (provider != null) await provider.stop();
61
67
  provider = null;
62
- Object.assign(settings, backup.settings);
63
68
  });
64
69
 
65
70
  it('not logged in redirects to login endpoint', async function () {
@@ -225,4 +230,166 @@ describe(__filename, function () {
225
230
  .expect(401)
226
231
  .expect('www-authenticate', 'Etherpad');
227
232
  });
233
+
234
+ describe('user_properties', function () {
235
+ it('uses existing settings.users[username] object', async function () {
236
+ const wantUser = {foo: 'bar'};
237
+ settings.users.normalUser = wantUser;
238
+ const padId = common.randomString();
239
+ const url = new URL(`/p/${padId}`, common.baseUrl).toString();
240
+ const res = await login(agent, issuer, url, 'normalUser');
241
+ assert.equal(res.request.url, url);
242
+ assert.equal(res.status, 200);
243
+ socket = await common.connect(res);
244
+ const gotUserP = new Promise((resolve) => {
245
+ plugins.hooks.clientVars =
246
+ [{hook_fn: (hn, ctx) => resolve(ctx.socket.client.request.session.user)}];
247
+ });
248
+ const msg = await common.handshake(socket, padId);
249
+ assert.equal(msg.type, 'CLIENT_VARS', `not a CLIENT_VARS message: ${JSON.stringify(msg)}`);
250
+ assert.equal(settings.users.normalUser, wantUser);
251
+ assert.equal(settings.users.normalUser.foo, 'bar');
252
+ // They won't be reference equal because each request loads the user object from the session
253
+ // store.
254
+ assert.deepEqual(await gotUserP, settings.users.normalUser);
255
+ });
256
+
257
+ it('sets settings.users[username] to new object if missing', async function () {
258
+ assert(settings.users.normalUser == null);
259
+ const padId = common.randomString();
260
+ const url = new URL(`/p/${padId}`, common.baseUrl).toString();
261
+ const res = await login(agent, issuer, url, 'normalUser');
262
+ assert.equal(res.request.url, url);
263
+ assert.equal(res.status, 200);
264
+ socket = await common.connect(res);
265
+ const gotUserP = new Promise((resolve) => {
266
+ plugins.hooks.clientVars =
267
+ [{hook_fn: (hn, ctx) => resolve(ctx.socket.client.request.session.user)}];
268
+ });
269
+ const msg = await common.handshake(socket, padId);
270
+ assert.equal(msg.type, 'CLIENT_VARS', `not a CLIENT_VARS message: ${JSON.stringify(msg)}`);
271
+ assert(settings.users.normalUser != null);
272
+ // They won't be reference equal because each request loads the user object from the session
273
+ // store.
274
+ assert.deepEqual(await gotUserP, settings.users.normalUser);
275
+ });
276
+
277
+ it('displayname defaults to name claim', async function () {
278
+ assert(settings.users.normalUser == null);
279
+ const padId = common.randomString();
280
+ const url = new URL(`/p/${padId}`, common.baseUrl).toString();
281
+ const res = await login(agent, issuer, url, 'normalUser');
282
+ assert.equal(res.request.url, url);
283
+ assert.equal(res.status, 200);
284
+ assert.equal(settings.users.normalUser.displayname, 'Firstname Lastname');
285
+ });
286
+
287
+ it('setting empty displayname descriptor cancels default behavior', async function () {
288
+ assert(settings.users.normalUser == null);
289
+ await epOpenidConnect.loadSettings('loadSettings', {settings: {ep_openid_connect: {
290
+ ...pluginSettings,
291
+ user_properties: {displayname: {}},
292
+ }}});
293
+ const padId = common.randomString();
294
+ const url = new URL(`/p/${padId}`, common.baseUrl).toString();
295
+ const res = await login(agent, issuer, url, 'normalUser');
296
+ assert.equal(res.request.url, url);
297
+ assert.equal(res.status, 200);
298
+ assert(!('displayname' in settings.users.normalUser));
299
+ });
300
+
301
+ it('username is set to sub claim', async function () {
302
+ assert(settings.users.normalUser == null);
303
+ const padId = common.randomString();
304
+ const url = new URL(`/p/${padId}`, common.baseUrl).toString();
305
+ const res = await login(agent, issuer, url, 'normalUser');
306
+ assert.equal(res.request.url, url);
307
+ assert.equal(res.status, 200);
308
+ assert.equal(settings.users.normalUser.username, 'normalUser');
309
+ });
310
+
311
+ describe('username can\'t be changed', function () {
312
+ for (const dsc of [
313
+ null,
314
+ {},
315
+ {default: 'something else'},
316
+ {claim: 'preferred_username'},
317
+ {claim: 'preferred_username', default: 'something else'},
318
+ ]) {
319
+ it(`descriptor: ${JSON.stringify(dsc)}`, async function () {
320
+ await epOpenidConnect.loadSettings('loadSettings', {settings: {ep_openid_connect: {
321
+ ...pluginSettings,
322
+ user_properties: {username: dsc},
323
+ }}});
324
+ // Settings validation should fail, resulting in an unconfigured state and causing
325
+ // Etherpad to fall back to built-in authn.
326
+ const res = await agent.get(new URL('/ep_openid_connect/login', common.baseUrl));
327
+ assert.equal(res.status, 401);
328
+ });
329
+ }
330
+ });
331
+
332
+ describe('{settings.json, claim value, user_properties} combinations', function () {
333
+ const testCases = [];
334
+ for (const user of [{}, {prop: undefined}, {prop: null}, {prop: 'userValue'}]) {
335
+ for (const claimValue of [undefined, null, 'claimValue']) {
336
+ for (const cfg of [
337
+ {},
338
+ {prop: null},
339
+ {prop: {}},
340
+ {prop: {default: 'defaultValue'}},
341
+ {prop: {claim: 'prop'}},
342
+ {prop: {claim: 'prop', default: 'defaultValue'}},
343
+ ]) {
344
+ const p = cfg.prop || {};
345
+ const wantIn = !!(
346
+ p.default ||
347
+ (p.claim && claimValue !== undefined) ||
348
+ ('prop' in user && cfg.prop !== null)); // eslint-disable-line eqeqeq
349
+ const wantValue =
350
+ p.claim && claimValue !== undefined ? claimValue
351
+ : 'prop' in user ? user.prop
352
+ : p.default ? p.default
353
+ : undefined;
354
+ const desc =
355
+ `user.prop=${'prop' in user ? user.prop : '<unset>'}, ` +
356
+ `claimValue=${claimValue === undefined ? '<unset>' : claimValue}, ` +
357
+ `user_properties.prop=${'prop' in cfg ? JSON.stringify(cfg.prop) : '<unset>'} -> ` +
358
+ `${wantIn ? wantValue : '<unset>'}`;
359
+ testCases.push({desc, user, claimValue, cfg, wantIn, wantValue});
360
+ }
361
+ }
362
+ }
363
+
364
+ for (const {desc, user, claimValue, cfg, wantIn, wantValue} of testCases) {
365
+ it(desc, async function () {
366
+ const username =
367
+ claimValue === undefined ? 'claimUnset'
368
+ : claimValue != null ? 'claimVal'
369
+ : 'claimNull';
370
+ settings.users[username] = {...user};
371
+ await epOpenidConnect.loadSettings('loadSettings', {settings: {ep_openid_connect: {
372
+ ...pluginSettings,
373
+ user_properties: cfg,
374
+ }}});
375
+ const padId = common.randomString();
376
+ const url = new URL(`/p/${padId}`, common.baseUrl).toString();
377
+ const res = await login(agent, issuer, url, username);
378
+ assert.equal(res.request.url, url);
379
+ assert.equal(res.status, 200);
380
+ socket = await common.connect(res);
381
+ const userP = new Promise((resolve) => {
382
+ plugins.hooks.clientVars =
383
+ [{hook_fn: (hn, ctx) => resolve(ctx.socket.client.request.session.user)}];
384
+ });
385
+ const msg = await common.handshake(socket, padId);
386
+ assert.equal(
387
+ msg.type, 'CLIENT_VARS', `not a CLIENT_VARS message: ${JSON.stringify(msg)}`);
388
+ const gotUser = await userP;
389
+ assert.equal('prop' in gotUser, wantIn);
390
+ if (wantIn) assert.equal(gotUser.prop, wantValue);
391
+ });
392
+ }
393
+ });
394
+ });
228
395
  });
@@ -1,6 +0,0 @@
1
- 'use strict';
2
-
3
- exports.postToolbarInit = () => {
4
- const {ep_openid_connect: {permit_displayname_change = true} = {}} = clientVars;
5
- if (!permit_displayname_change) $('#myusernameedit').attr('disabled', true);
6
- };