Skip to content
Merged
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Develop -- TODO Change to 1.8.x.
### Compatibility-breaking changes
* **IMPORTANT:** It is no longer possible to protect a group pad with a
password. All API calls to `setPassword` or `isPasswordProtected` will fail.
Existing group pads that were previously password protected will no longer be
password protected. If you need fine-grained access control, you can restrict
API session creation in your frontend service, or you can use plugins.
* Authorization failures now return 403 by default instead of 401
* The `authorize` hook is now only called after successful
authentication. Use the new `preAuthorize` hook if you need to bypass
Expand Down
5 changes: 2 additions & 3 deletions doc/api/hooks_server-side.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,8 @@ Called from: src/node/db/SecurityManager.js
Things in context:

1. padID - the pad the user wants to access
2. password - the password the user has given to access the pad
3. token - the token of the author
4. sessionCookie - the session the use has
2. token - the token of the author
3. sessionCookie - the session the use has

This hook gets called when the access to the concrete pad is being checked. Return `false` to deny access.

Expand Down
18 changes: 0 additions & 18 deletions doc/api/http_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -581,24 +581,6 @@ return true of false
* `{code: 0, message:"ok", data: {publicStatus: true}}`
* `{code: 1, message:"padID does not exist", data: null}`

#### setPassword(padID, password)
* API >= 1

returns ok or an error message

*Example returns:*
* `{code: 0, message:"ok", data: null}`
* `{code: 1, message:"padID does not exist", data: null}`

#### isPasswordProtected(padID)
* API >= 1

returns true or false

*Example returns:*
* `{code: 0, message:"ok", data: {passwordProtection: true}}`
* `{code: 1, message:"padID does not exist", data: null}`

#### listAuthorsOfPad(padID)
* API >= 1

Expand Down
1 change: 0 additions & 1 deletion doc/docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,6 @@ For the editor container, you can also make it full width by adding `full-width-
| `SUPPRESS_ERRORS_IN_PAD_TEXT` | Should we suppress errors from being visible in the default Pad Text? | `false` |
| `REQUIRE_SESSION` | If this option is enabled, a user must have a session to access pads. This effectively allows only group pads to be accessed. | `false` |
| `EDIT_ONLY` | Users may edit pads but not create new ones. Pad creation is only via the API. This applies both to group pads and regular pads. | `false` |
| `SESSION_NO_PASSWORD` | If set to true, those users who have a valid session will automatically be granted access to password protected pads. | `false` |
| `MINIFY` | If true, all css & js will be minified before sending to the client. This will improve the loading performance massively, but makes it difficult to debug the javascript/css | `true` |
| `MAX_AGE` | How long may clients use served javascript code (in seconds)? Not setting this may cause problems during deployment. Set to 0 to disable caching. | `21600` (6 hours) |
| `ABIWORD` | Absolute path to the Abiword executable. Abiword is needed to get advanced import/export features of pads. Setting it to null disables Abiword and will only allow plain text and HTML import/exports. | `null` |
Expand Down
6 changes: 0 additions & 6 deletions settings.json.docker
Original file line number Diff line number Diff line change
Expand Up @@ -260,12 +260,6 @@
*/
"editOnly": "${EDIT_ONLY:false}",

/*
* If set to true, those users who have a valid session will automatically be
* granted access to password protected pads.
*/
"sessionNoPassword": "${SESSION_NO_PASSWORD:false}",

/*
* If true, all css & js will be minified before sending to the client.
*
Expand Down
6 changes: 0 additions & 6 deletions settings.json.template
Original file line number Diff line number Diff line change
Expand Up @@ -263,12 +263,6 @@
*/
"editOnly": false,

/*
* If set to true, those users who have a valid session will automatically be
* granted access to password protected pads.
*/
"sessionNoPassword": false,

/*
* If true, all css & js will be minified before sending to the client.
*
Expand Down
2 changes: 0 additions & 2 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,7 @@

"pad.loading": "Loading...",
"pad.noCookie": "Cookie could not be found. Please allow cookies in your browser! Your session and settings will not be saved between visits. This may be due to Etherpad being included in an iFrame in some Browsers. Please ensure Etherpad is on the same subdomain/domain as the parent iFrame",
"pad.passwordRequired": "You need a password to access this pad",
"pad.permissionDenied": "You do not have permission to access this pad",
"pad.wrongPassword": "Your password was wrong",

"pad.settings.padSettings": "Pad Settings",
"pad.settings.myView": "My View",
Expand Down
39 changes: 0 additions & 39 deletions src/node/db/API.js
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,6 @@ exports.setPublicStatus = async function(padID, publicStatus)
publicStatus = (publicStatus.toLowerCase() === "true");
}

// set the password
await pad.setPublicStatus(publicStatus);
}

Expand All @@ -706,44 +705,6 @@ exports.getPublicStatus = async function(padID)
return { publicStatus: pad.getPublicStatus() };
}

/**
setPassword(padID, password) returns ok or a error message

Example returns:

{code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null}
*/
exports.setPassword = async function(padID, password)
{
// ensure this is a group pad
checkGroupPad(padID, "password");

// get the pad
let pad = await getPadSafe(padID, true);

// set the password
await pad.setPassword(password === '' ? null : password);
}

/**
isPasswordProtected(padID) returns true or false

Example returns:

{code: 0, message:"ok", data: {passwordProtection: true}}
{code: 1, message:"padID does not exist", data: null}
*/
exports.isPasswordProtected = async function(padID)
{
// ensure this is a group pad
checkGroupPad(padID, "password");

// get the pad
let pad = await getPadSafe(padID, true);
return { isPasswordProtected: pad.isPasswordProtected() };
}

/**
listAuthorsOfPad(padID) returns an array of authors who contributed to this pad

Expand Down
30 changes: 0 additions & 30 deletions src/node/db/Pad.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ let Pad = function Pad(id) {
this.head = -1;
this.chatHead = -1;
this.publicStatus = false;
this.passwordHash = null;
this.id = id;
this.savedRevisions = [];
};
Expand Down Expand Up @@ -595,19 +594,6 @@ Pad.prototype.setPublicStatus = async function setPublicStatus(publicStatus) {
await this.saveToDatabase();
};

Pad.prototype.setPassword = async function setPassword(password) {
this.passwordHash = password == null ? null : hash(password, generateSalt());
await this.saveToDatabase();
};

Pad.prototype.isCorrectPassword = function isCorrectPassword(password) {
return compare(this.passwordHash, password);
};

Pad.prototype.isPasswordProtected = function isPasswordProtected() {
return this.passwordHash != null;
};

Pad.prototype.addSavedRevision = async function addSavedRevision(revNum, savedById, label) {
// if this revision is already saved, return silently
for (var i in this.savedRevisions) {
Expand All @@ -633,19 +619,3 @@ Pad.prototype.getSavedRevisions = function getSavedRevisions() {
return this.savedRevisions;
};

/* Crypto helper methods */

function hash(password, salt) {
var shasum = crypto.createHash('sha512');
shasum.update(password + salt);

return shasum.digest("hex") + "$" + salt;
}

function generateSalt() {
return randomString(86);
}

function compare(hashStr, password) {
return hash(password, hashStr.split("$")[1]) === hashStr;
}
30 changes: 6 additions & 24 deletions src/node/db/SecurityManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ var log4js = require('log4js');
var authLogger = log4js.getLogger("auth");

const DENY = Object.freeze({accessStatus: 'deny'});
const WRONG_PASSWORD = Object.freeze({accessStatus: 'wrongPassword'});
const NEED_PASSWORD = Object.freeze({accessStatus: 'needPassword'});

/**
* Determines whether the user can access a pad.
Expand All @@ -42,16 +40,14 @@ const NEED_PASSWORD = Object.freeze({accessStatus: 'needPassword'});
* 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 password is the password the user has given to access this pad. It can be null.
* @param userSettings is the settings.users[username] object (or equivalent from an authn plugin).
* @return {accessStatus: grant|deny|wrongPassword|needPassword, authorID: a.xxxxxx}. The caller
* must use the author ID returned in this object when making any changes associated with the
* author.
* @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 function(padID, sessionCookie, token, password, userSettings)
exports.checkAccess = async function(padID, sessionCookie, token, userSettings)
{
if (!padID) {
authLogger.debug('access denied: missing padID');
Expand Down Expand Up @@ -84,7 +80,7 @@ exports.checkAccess = async function(padID, sessionCookie, token, password, user

// allow plugins to deny access
const isFalse = (x) => x === false;
if (hooks.callAll('onAccessCheck', {padID, password, token, sessionCookie}).some(isFalse)) {
if (hooks.callAll('onAccessCheck', {padID, token, sessionCookie}).some(isFalse)) {
authLogger.debug('access denied: an onAccessCheck hook function returned false');
return DENY;
}
Expand Down Expand Up @@ -112,8 +108,7 @@ exports.checkAccess = async function(padID, sessionCookie, token, password, user
};

if (!padID.includes('$')) {
// Only group pads can be private or have passwords, so there is nothing more to check for this
// non-group pad.
// Only group pads can be private, so there is nothing more to check for this non-group pad.
return grant;
}

Expand All @@ -122,7 +117,7 @@ exports.checkAccess = async function(padID, sessionCookie, token, password, user
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 password or public status to check.
// Creating a group pad, so there is no public status to check.
return grant;
}

Expand All @@ -133,18 +128,5 @@ exports.checkAccess = async function(padID, sessionCookie, token, password, user
return DENY;
}

const passwordExempt = settings.sessionNoPassword && sessionAuthorID != null;
const requirePassword = pad.isPasswordProtected() && !passwordExempt;
if (requirePassword) {
if (password == null) {
authLogger.debug('access denied: password required');
return NEED_PASSWORD;
}
if (!password || !pad.isCorrectPassword(password)) {
authLogger.debug('access denied: wrong password');
return WRONG_PASSWORD;
}
}

return grant;
};
2 changes: 0 additions & 2 deletions src/node/handler/APIHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,6 @@ version["1"] = Object.assign({},
, "getReadOnlyID" : ["padID"]
, "setPublicStatus" : ["padID", "publicStatus"]
, "getPublicStatus" : ["padID"]
, "setPassword" : ["padID", "password"]
, "isPasswordProtected" : ["padID"]
, "listAuthorsOfPad" : ["padID"]
, "padUsersCount" : ["padID"]
}
Expand Down
5 changes: 2 additions & 3 deletions src/node/handler/PadMessageHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ exports.handleMessage = async function(client, message)

const {session: {user} = {}} = client.client.request;
const {accessStatus, authorID} =
await securityManager.checkAccess(padId, auth.sessionID, auth.token, auth.password, user);
await securityManager.checkAccess(padId, auth.sessionID, auth.token, user);
if (accessStatus !== 'grant') {
// Access denied. Send the reason to the user.
client.json.send({accessStatus});
Expand Down Expand Up @@ -835,7 +835,7 @@ async function handleSwitchToPad(client, message, _authorID)
const newPadIds = await readOnlyManager.getIds(message.padId);
const {session: {user} = {}} = client.client.request;
const {accessStatus, authorID} = await securityManager.checkAccess(
newPadIds.padId, message.sessionID, message.token, message.password, user);
newPadIds.padId, message.sessionID, message.token, user);
if (accessStatus !== 'grant') {
// Access denied. Send the reason to the user.
client.json.send({accessStatus});
Expand Down Expand Up @@ -877,7 +877,6 @@ function createSessionInfoAuth(sessionInfo, message)
sessionID: message.sessionID,
padID: message.padId,
token: message.token,
password: message.password,
};
}

Expand Down
21 changes: 4 additions & 17 deletions src/node/handler/SocketIORouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ exports.setSocketIO = function(_socket) {
// wrap the original send function to log the messages
client._send = client.send;
client.send = function(message) {
messageLogger.debug("to " + client.id + ": " + stringifyWithoutPassword(message));
messageLogger.debug(`to ${client.id}: ${JSON.stringify(message)}`);
client._send(message);
}

Expand All @@ -79,14 +79,14 @@ exports.setSocketIO = function(_socket) {

client.on('message', async function(message) {
if (message.protocolVersion && message.protocolVersion != 2) {
messageLogger.warn("Protocolversion header is not correct:" + stringifyWithoutPassword(message));
messageLogger.warn(`Protocolversion header is not correct: ${JSON.stringify(message)}`);
return;
}
if (!message.component || !components[message.component]) {
messageLogger.error("Can't route the message:" + stringifyWithoutPassword(message));
messageLogger.error(`Can't route the message: ${JSON.stringify(message)}`);
return;
}
messageLogger.debug("from " + client.id + ": " + stringifyWithoutPassword(message));
messageLogger.debug(`from ${client.id}: ${JSON.stringify(message)}`);
await components[message.component].handleMessage(client, message);
});

Expand All @@ -98,16 +98,3 @@ exports.setSocketIO = function(_socket) {
});
});
}

// returns a stringified representation of a message, removes the password
// this ensures there are no passwords in the log
function stringifyWithoutPassword(message)
{
let newMessage = Object.assign({}, message);

if (newMessage.password != null) {
newMessage.password = "xxx";
}

return JSON.stringify(newMessage);
}
2 changes: 1 addition & 1 deletion src/node/hooks/express/importexport.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ exports.expressCreateServer = function (hook_name, args, cb) {
args.app.post('/p/:pad/import', async function(req, res, next) {
const {session: {user} = {}} = req;
const {accessStatus} = await securityManager.checkAccess(
req.params.pad, req.cookies.sessionID, req.cookies.token, req.cookies.password, user);
req.params.pad, req.cookies.sessionID, req.cookies.token, user);
if (accessStatus !== 'grant' || !webaccess.userCanModify(req.params.pad, req)) {
return res.status(403).send('Forbidden');
}
Expand Down
9 changes: 0 additions & 9 deletions src/node/hooks/express/openapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,15 +201,6 @@ const resources = {
summary: 'return true of false',
responseSchema: { publicStatus: { type: 'boolean' } },
},
setPassword: {
operationId: 'setPassword',
summary: 'returns ok or a error message',
},
isPasswordProtected: {
operationId: 'isPasswordProtected',
summary: 'returns true or false',
responseSchema: { passwordProtection: { type: 'boolean' } },
},
authors: {
operationId: 'listAuthorsOfPad',
summary: 'returns an array of authors who contributed to this pad',
Expand Down
2 changes: 1 addition & 1 deletion src/node/padaccess.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module.exports = async function (req, res) {
try {
const {session: {user} = {}} = req;
const accessObj = await securityManager.checkAccess(
req.params.pad, req.cookies.sessionID, req.cookies.token, req.cookies.password, user);
req.params.pad, req.cookies.sessionID, req.cookies.token, user);

if (accessObj.accessStatus === "grant") {
// there is access, continue
Expand Down
5 changes: 0 additions & 5 deletions src/node/utils/Settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,11 +193,6 @@ exports.requireSession = false;
*/
exports.editOnly = false;

/**
* A flag that bypasses password prompts for users with valid sessions
*/
exports.sessionNoPassword = false;

/**
* Max age that responses will have (affects caching layer).
*/
Expand Down
Loading