etherpad-lite/src/node/db/SecurityManager.js
pcworld 3c71e8983b Fix read only pad access with authentication
Before this commit, webaccess.checkAccess saved the authorization in
user.padAuthorizations[padId] with padId being the read-only pad ID,
however later stages, e.g. in PadMessageHandler, use the real pad ID for
access checks. This led to authorization being denied.

This commit fixes it by only storing and comparing the real pad IDs and
not read-only pad IDs.

This fixes test case "authn user readonly pad -> 200, ok" in
src/tests/backend/specs/socketio.js.
2021-04-12 22:51:06 -04:00

142 lines
5.3 KiB
JavaScript

'use strict';
/**
* Controls the security of pad access
*/
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const authorManager = require('./AuthorManager');
const hooks = require('../../static/js/pluginfw/hooks.js');
const padManager = require('./PadManager');
const readOnlyManager = require('./ReadOnlyManager');
const sessionManager = require('./SessionManager');
const settings = require('../utils/Settings');
const webaccess = require('../hooks/express/webaccess');
const log4js = require('log4js');
const authLogger = log4js.getLogger('auth');
const DENY = Object.freeze({accessStatus: 'deny'});
/**
* Determines whether the user can access a pad.
*
* @param padID identifies the pad the user wants to access.
* @param sessionCookie identifies the sessions the user created via the HTTP API, if any.
* Note: The term "session" used here is unrelated to express-session.
* @param token is a random token of the form t.randomstring_of_length_20 generated by the client
* when using the web UI (not the HTTP API). This token is only used if settings.requireSession
* is false and the user is accessing a public pad. If there is not an author already associated
* with this token then a new author object is created (including generating an author ID) and
* associated with this token.
* @param userSettings is the settings.users[username] object (or equivalent from an authn plugin).
* @return {accessStatus: grant|deny, authorID: a.xxxxxx}. The caller must use the author ID
* returned in this object when making any changes associated with the author.
*
* WARNING: Tokens and session IDs MUST be kept secret, otherwise users will be able to impersonate
* each other (which might allow them to gain privileges).
*/
exports.checkAccess = async (padID, sessionCookie, token, userSettings) => {
if (!padID) {
authLogger.debug('access denied: missing padID');
return DENY;
}
let canCreate = !settings.editOnly;
if (readOnlyManager.isReadOnlyId(padID)) {
canCreate = false;
padID = await readOnlyManager.getPadId(padID);
if (padID == null) {
authLogger.debug('access denied: read-only pad ID for a pad that does not exist');
return DENY;
}
}
// Authentication and authorization checks.
if (settings.loadTest) {
console.warn(
'bypassing socket.io authentication and authorization checks due to settings.loadTest');
} else if (settings.requireAuthentication) {
if (userSettings == null) {
authLogger.debug('access denied: authentication is required');
return DENY;
}
if (userSettings.canCreate != null && !userSettings.canCreate) canCreate = false;
if (userSettings.readOnly) canCreate = false;
// Note: userSettings.padAuthorizations should still be populated even if
// settings.requireAuthorization is false.
const padAuthzs = userSettings.padAuthorizations || {};
const level = webaccess.normalizeAuthzLevel(padAuthzs[padID]);
if (!level) {
authLogger.debug('access denied: unauthorized');
return DENY;
}
if (level !== 'create') canCreate = false;
}
// allow plugins to deny access
const isFalse = (x) => x === false;
if (hooks.callAll('onAccessCheck', {padID, token, sessionCookie}).some(isFalse)) {
authLogger.debug('access denied: an onAccessCheck hook function returned false');
return DENY;
}
// start fetching the info we may need
const p_sessionAuthorID = sessionManager.findAuthorID(padID.split('$')[0], sessionCookie);
const p_tokenAuthorID = authorManager.getAuthor4Token(token);
const p_padExists = padManager.doesPadExist(padID);
const padExists = await p_padExists;
if (!padExists && !canCreate) {
authLogger.debug('access denied: user attempted to create a pad, which is prohibited');
return DENY;
}
const sessionAuthorID = await p_sessionAuthorID;
if (settings.requireSession && !sessionAuthorID) {
authLogger.debug('access denied: HTTP API session is required');
return DENY;
}
const grant = {
accessStatus: 'grant',
authorID: (sessionAuthorID != null) ? sessionAuthorID : await p_tokenAuthorID,
};
if (!padID.includes('$')) {
// Only group pads can be private, so there is nothing more to check for this non-group pad.
return grant;
}
if (!padExists) {
if (sessionAuthorID == null) {
authLogger.debug('access denied: must have an HTTP API session to create a group pad');
return DENY;
}
// Creating a group pad, so there is no public status to check.
return grant;
}
const pad = await padManager.getPad(padID);
if (!pad.getPublicStatus() && sessionAuthorID == null) {
authLogger.debug('access denied: must have an HTTP API session to access private group pads');
return DENY;
}
return grant;
};