Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/api/hooks_server-side.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ authnFailure function unless falling back to HTTP basic authentication is
appropriate upon authentication failure.

This hook is only called if either the `requireAuthentication` setting is true
or the request is for an `/admin` page.
or the request is for `/admin-auth` (admin login / session verification).

Calling the provided callback with `[true]` or `[false]` will cause
authentication to succeed or fail, respectively. Calling the callback with `[]`
Expand Down
2 changes: 1 addition & 1 deletion doc/api/hooks_server-side.md
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ authnFailure function unless falling back to HTTP basic authentication is
appropriate upon authentication failure.

This hook is only called if either the `requireAuthentication` setting is true
or the request is for an `/admin` page.
or the request is for `/admin-auth` (admin login / session verification).

Calling the provided callback with `[true]` or `[false]` will cause
authentication to succeed or fail, respectively. Calling the callback with `[]`
Expand Down
17 changes: 17 additions & 0 deletions src/node/hooks/express/webaccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,14 @@ const checkAccess = async (req:any, res:any, next: Function) => {
// user, or a privilege/identity change such as non-admin -> admin), which is
// the point at which the session id must be rotated (see below).
const prevUser = req.session != null ? req.session.user : null;
// Snapshot is_admin flags before authenticate plugins run. Plugins such as
// ep_hash_auth's hash_dir path replace settings.users[username] on success
// and can drop a pre-declared is_admin: true (issue #8110), which then fails
// the /admin-auth/ authorize check with 403. Restore those flags below.
const preAuthAdminUsers = new Set(
Object.entries(settings.users as Record<string, {is_admin?: boolean} | null | undefined>)
.filter(([, user]) => user != null && !!user.is_admin)
.map(([username]) => username));
// If the HTTP basic auth header is present, extract the username and password so it can be given
// to authn plugins.
const httpBasicAuth = req.headers.authorization && req.headers.authorization.startsWith('Basic ');
Expand Down Expand Up @@ -227,6 +235,15 @@ const checkAccess = async (req:any, res:any, next: Function) => {
httpLogger.error('authenticate hook failed to add user settings to session');
return res.status(500).send('Internal Server Error');
}
// Restore is_admin when a settings-declared admin was authenticated by a
// plugin that rebuilt the user object without carrying the flag forward.
const authedUsername = req.session.user.username;
if (authedUsername && preAuthAdminUsers.has(authedUsername) && !req.session.user.is_admin) {
req.session.user.is_admin = true;
if (settings.users[authedUsername] != null) {
settings.users[authedUsername].is_admin = true;
}
}
// Session fixation defense (GHSA-73h9-c5xp-gfg4): rotate the session id
// whenever authentication changed the principal — an anonymous session
// becoming authenticated, OR an authenticated session changing identity or
Expand Down
29 changes: 29 additions & 0 deletions src/tests/backend/specs/webaccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,35 @@ describe(__filename, function () {
await agent.get('/').expect(500);
assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']);
});

// Regression for https://github.com/ether/etherpad/issues/8110 —
// ep_hash_auth (hash_dir path) authenticates by replacing
// settings.users[username] and can drop a pre-declared is_admin flag.
// /admin-auth/ must still admit the user when settings said they were admin.
it('POST /admin-auth/ invokes authenticate and preserves settings is_admin (#8110)',
async function () {
settings.requireAuthentication = false;
settings.users = {
// Classic ep_hash_auth setup: admin declared in settings, credentials
// supplied by the authenticate plugin (no plaintext password here).
hashadmin: {is_admin: true},
};
let authenticateCalled = false;
plugins.hooks.authenticate = [makeHook('authenticate',
(hookName: string, context: any, cb: Function) => {
authenticateCalled = true;
assert.equal(context.username, 'hashadmin');
assert.equal(context.password, 'secret');
// Mimic ep_hash_auth hash_dir success: rebuild the user object
// without carrying is_admin forward.
settings.users.hashadmin = {username: 'hashadmin', is_admin: false};
context.req.session.user = settings.users.hashadmin;
return cb([true]);
})];
await agent.post('/admin-auth/').auth('hashadmin', 'secret').expect(200);
assert.equal(authenticateCalled, true);
assert.equal(settings.users.hashadmin.is_admin, true);
});
});

describe('authorize', function () {
Expand Down