import: Use the correct author ID when using sessions

There are two different ways an author ID becomes associated with a
user: either bound to a token or bound to a session ID. (The token and
session ID come from the `token` and `sessionID` cookies, or, in the
case of socket.io messages, from the `token` and `sessionID` message
properties.) When `settings.requireSession` is true or the user is
accessing a group pad, the session ID should be used. Otherwise the
token should be used.

Before this change, the `/p/:pad/import` handler was always using the
token, even when `settings.requireSession` was true. This caused the
following error because a different author ID was bound to the token
versus the session ID:

> Unable to import file into ${pad}. Author ${authorID} exists but he
> never contributed to this pad

This bug was reported in issue #4006. PR #4012 worked around the
problem by binding the same author ID to the token as well as the
session ID.

This change does the following:
  * Modifies the import handler to use the session ID to obtain the
    author ID (when appropriate).
  * Expands the documentation for the SecurityManager checkAccess
    function.
  * Removes the workaround from PR #4012.
  * Cleans up the `bin/createUserSession.js` test script.
This commit is contained in:
Richard Hansen 2020-09-02 17:16:02 -04:00 committed by John McLear
parent db0bcb524e
commit 6c2a361935
5 changed files with 92 additions and 160 deletions

View file

@ -1,3 +1,4 @@
const assert = require('assert').strict;
var hasPadAccess = require("../../padaccess");
var settings = require('../../utils/Settings');
var exportHandler = require('../../handler/ExportHandler');
@ -5,6 +6,7 @@ var importHandler = require('../../handler/ImportHandler');
var padManager = require("../../db/PadManager");
var authorManager = require("../../db/AuthorManager");
const rateLimit = require("express-rate-limit");
const securityManager = require("../../db/SecurityManager");
settings.importExportRateLimiting.onLimitReached = function(req, res, options) {
// when the rate limiter triggers, write a warning in the logs
@ -51,57 +53,41 @@ exports.expressCreateServer = function (hook_name, args, cb) {
// handle import requests
args.app.use('/p/:pad/import', limiter);
args.app.post('/p/:pad/import', async function(req, res, next) {
if (await hasPadAccess(req, res)) {
let exists = await padManager.doesPadExists(req.params.pad);
if (!exists) {
console.warn(`Someone tried to import into a pad that doesn't exist (${req.params.pad})`);
return next();
}
/*
* Starting from Etherpad 1.8.3 onwards, importing into a pad is allowed
* only if a user has his browser opened and connected to the pad (i.e. a
* Socket.IO session is estabilished for him) and he has already
* contributed to that specific pad.
*
* Note that this does not have anything to do with the "session", used
* for logging into "group pads". That kind of session is not needed here.
*
* This behaviour does not apply to API requests, only to /p/$PAD$/import
*
* See: https://github.com/ether/etherpad-lite/pull/3833#discussion_r407490205
*/
if (!req.cookies && !settings.allowAnyoneToImport) {
console.warn(`Unable to import file into "${req.params.pad}". No cookies included in request`);
return next();
}
if (!req.cookies.token && !settings.allowAnyoneToImport) {
console.warn(`Unable to import file into "${req.params.pad}". No token in the cookies`);
return next();
}
let author = await authorManager.getAuthor4Token(req.cookies.token);
// author is of the form: "a.g2droBYw1prY7HW9"
if (!author && !settings.allowAnyoneToImport) {
console.warn(`Unable to import file into "${req.params.pad}". No Author found for token ${req.cookies.token}`);
return next();
}
let authorsPads = await authorManager.listPadsOfAuthor(author);
if (!authorsPads && !settings.allowAnyoneToImport) {
console.warn(`Unable to import file into "${req.params.pad}". Author "${author}" exists but he never contributed to any pad`);
return next();
}
let authorsPadIDs = authorsPads.padIDs;
if ( (authorsPadIDs.indexOf(req.params.pad) === -1) && !settings.allowAnyoneToImport) {
console.warn(`Unable to import file into "${req.params.pad}". Author "${author}" exists but he never contributed to this pad`);
return next();
}
importHandler.doImport(req, res, req.params.pad);
if (!(await padManager.doesPadExists(req.params.pad))) {
console.warn(`Someone tried to import into a pad that doesn't exist (${req.params.pad})`);
return next();
}
const {accessStatus, authorID} = await securityManager.checkAccess(
req.params.pad, req.cookies.sessionID, req.cookies.token, req.cookies.password);
if (accessStatus !== 'grant') return res.status(403).send('Forbidden');
assert(authorID);
/*
* Starting from Etherpad 1.8.3 onwards, importing into a pad is allowed
* only if a user has his browser opened and connected to the pad (i.e. a
* Socket.IO session is estabilished for him) and he has already
* contributed to that specific pad.
*
* Note that this does not have anything to do with the "session", used
* for logging into "group pads". That kind of session is not needed here.
*
* This behaviour does not apply to API requests, only to /p/$PAD$/import
*
* See: https://github.com/ether/etherpad-lite/pull/3833#discussion_r407490205
*/
if (!settings.allowAnyoneToImport) {
const authorsPads = await authorManager.listPadsOfAuthor(authorID);
if (!authorsPads) {
console.warn(`Unable to import file into "${req.params.pad}". Author "${authorID}" exists but he never contributed to any pad`);
return next();
}
if (authorsPads.padIDs.indexOf(req.params.pad) === -1) {
console.warn(`Unable to import file into "${req.params.pad}". Author "${authorID}" exists but he never contributed to this pad`);
return next();
}
}
importHandler.doImport(req, res, req.params.pad);
});
}