From ae092edf0cbeab2d4a998a18005e19ae0cb1b7c9 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 28 Feb 2022 04:19:30 -0500 Subject: [PATCH] AuthorManager: New `getAuthorId` hook --- CHANGELOG.md | 3 ++ doc/api/hooks_server-side.md | 56 ++++++++++++++++++++++++++++++++++ src/node/db/AuthorManager.js | 28 +++++++++++++---- src/node/db/SecurityManager.js | 2 +- 4 files changed, 82 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97882a0e6..214e419ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ * New `expressPreSession` server-side hook. * New `padDefaultContent` server-side hook. +* New `getAuthorId` server-side hook. * New APIs for processing attributes: `ep_etherpad-lite/static/js/attributes` (low-level API) and `ep_etherpad-lite/static/js/AttributeMap` (high-level API). @@ -79,6 +80,8 @@ * `appendATextToAssembler()`: Deprecated in favor of the new `opsFromAText()` generator function. * `newOp()`: Deprecated in favor of the new `Op` class. +* The `AuthorManager.getAuthor4Token()` function is deprecated; use the new + `AuthorManager.getAuthorId()` function instead. # 1.8.17 diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index 3e2e6313b..0bae816a7 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -207,6 +207,62 @@ Things in context: This hook gets called when the access to the concrete pad is being checked. Return `false` to deny access. +## `getAuthorId` + +Called from `src/node/db/AuthorManager.js` + +Called when looking up (or creating) the author ID for a user, except for author +IDs obtained via the HTTP API. Registered hook functions are called until one +returns a non-`undefined` value. If a truthy value is returned by a hook +function, it is used as the user's author ID. Otherwise, the value of the +`dbKey` context property is used to look up the author ID. If there is no such +author ID at that key, a new author ID is generated and associated with that +key. + +Context properties: + +* `dbKey`: Database key to use when looking up the user's author ID if no hook + function returns an author ID. This is initialized to the user-supplied token + value (see the `token` context property), but hook functions can modify this + to control how author IDs are allocated to users. If no author ID is + associated with this database key, a new author ID will be randomly generated + and associated with the key. For security reasons, if this is modified it + should be modified to not look like a valid token (see the `token` context + property) unless the plugin intentionally wants the user to be able to + impersonate another user. +* `token`: The user-supplied token, or nullish for an anonymous user. Tokens are + secret values that must not be disclosed to others. If non-null, the token is + guaranteed to be a string with the form `t.` where `` is + any valid non-empty base64url string (RFC 4648 section 5 with padding). + Example: `t.twim3X2_KGiRj8cJ-3602g==`. +* `user`: If the user has authenticated, this is an object from `settings.users` + (or similar from an authentication plugin). Etherpad core and all good + authentication plugins set the `username` property of this object to a string + that uniquely identifies the authenticated user. This object is nullish if the + user has not authenticated. + +Example: + +```javascript +exports.getAuthorId = async (hookName, context) => { + const {username} = context.user || {}; + // If the user has not authenticated, or has "authenticated" as the guest + // user, do the default behavior (try another plugin if any, falling through + // to using the token as the database key). + if (!username || username === 'guest') return; + // The user is authenticated and has a username. Give the user a stable author + // ID so that they appear to be the same author even after clearing cookies or + // accessing the pad from another device. Note that this string is guaranteed + // to never have the form of a valid token; without that guarantee an + // unauthenticated user might be able to impersonate an authenticated user. + context.dbKey = `username=${username}`; + // Return a falsy but non-undefined value to stop Etherpad from calling any + // more getAuthorId hook functions and look up the author ID using the + // username-derived database key. + return ''; +}; +``` + ## `padCreate` Called from: `src/node/db/Pad.js` diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.js index 2a354d425..7049be5db 100644 --- a/src/node/db/AuthorManager.js +++ b/src/node/db/AuthorManager.js @@ -21,7 +21,8 @@ const db = require('./DB'); const CustomError = require('../utils/customError'); -const randomString = require('../../static/js/pad_utils').randomString; +const hooks = require('../../static/js/pluginfw/hooks.js'); +const {randomString, padutils: {warnDeprecated}} = require('../../static/js/pad_utils'); exports.getColorPalette = () => [ '#ffc7c7', @@ -102,17 +103,32 @@ exports.doesAuthorExist = async (authorID) => { /* exported for backwards compatibility */ exports.doesAuthorExists = exports.doesAuthorExist; -/** - * Returns the AuthorID for a token. - * @param {String} token The token - */ -exports.getAuthor4Token = async (token) => { +const getAuthor4Token = async (token) => { const author = await mapAuthorWithDBKey('token2author', token); // return only the sub value authorID return author ? author.authorID : author; }; +exports.getAuthorId = async (token, user) => { + const context = {dbKey: token, token, user}; + let [authorId] = await hooks.aCallFirst('getAuthorId', context); + if (!authorId) authorId = await getAuthor4Token(context.dbKey); + return authorId; +}; + +/** + * Returns the AuthorID for a token. + * + * @deprecated Use `getAuthorId` instead. + * @param {String} token The token + */ +exports.getAuthor4Token = async (token) => { + warnDeprecated( + 'AuthorManager.getAuthor4Token() is deprecated; use AuthorManager.getAuthorId() instead'); + return await getAuthor4Token(token); +}; + /** * Returns the AuthorID for a mapper. * @param {String} token The mapper diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.js index 0f3de33c3..280c753bb 100644 --- a/src/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.js @@ -115,7 +115,7 @@ exports.checkAccess = async (padID, sessionCookie, token, userSettings) => { const grant = { accessStatus: 'grant', - authorID: sessionAuthorID || await authorManager.getAuthor4Token(token), + authorID: sessionAuthorID || await authorManager.getAuthorId(token, userSettings), }; if (!padID.includes('$')) {