security: Enable authorize plugins to grant modify-only access

pull/4348/head
Richard Hansen 2020-09-11 19:46:47 -04:00 committed by John McLear
parent 6ed11b7605
commit 02757079c0
4 changed files with 66 additions and 4 deletions

View File

@ -253,8 +253,8 @@ following are true:
For pre-authentication invocations of your authorize function, you can pass the
following values to the provided callback:
* `[true]` or `['create']` will immediately grant access without requiring the
user to authenticate.
* `[true]`, `['create']`, or `['modify']` will immediately grant access without
requiring the user to authenticate.
* `[false]` will trigger authentication unless authentication is not required.
* `[]` or `undefined` will defer the decision to the next authorization plugin
(if any, otherwise it is the same as calling with `[false]`).
@ -267,7 +267,11 @@ public.
For post-authentication invocations of your authorize function, you can pass the
following values to the provided callback:
* `[true]` or `['create']` will grant access.
* `[true]` or `['create']` will grant access to modify or create the pad if the
request is for a pad, otherwise access is simply granted. (Access will be
downgraded to modify-only if `settings.editOnly` is true.)
* `['modify']` will grant access to modify but not create the pad if the
request is for a pad, otherwise access is simply granted.
* `[false]` will deny access.
* `[]` or `undefined` will defer the authorization decision to the next
authorization plugin (if any, otherwise deny).

View File

@ -58,6 +58,8 @@ exports.checkAccess = async function(padID, sessionCookie, token, password, user
return DENY;
}
let canCreate = !settings.editOnly;
if (settings.requireAuthentication) {
// Make sure the user has authenticated if authentication is required. The caller should have
// already performed this check, but it is repeated here just in case.
@ -73,6 +75,7 @@ exports.checkAccess = async function(padID, sessionCookie, token, password, user
authLogger.debug('access denied: unauthorized');
return DENY;
}
if (level !== 'create') canCreate = false;
}
// allow plugins to deny access
@ -88,7 +91,7 @@ exports.checkAccess = async function(padID, sessionCookie, token, password, user
const p_padExists = padManager.doesPadExist(padID);
const padExists = await p_padExists;
if (!padExists && settings.editOnly) {
if (!padExists && !canCreate) {
authLogger.debug('access denied: user attempted to create a pad, which is prohibited');
return DENY;
}

View File

@ -13,6 +13,7 @@ exports.normalizeAuthzLevel = (level) => {
switch (level) {
case true:
return 'create';
case 'modify':
case 'create':
return level;
default:

View File

@ -125,6 +125,7 @@ describe('socket.io access checks', function() {
beforeEach(async function() {
Object.assign(settingsBackup, settings);
assert(socket == null);
settings.editOnly = false;
settings.requireAuthentication = false;
settings.requireAuthorization = false;
settings.users = {
@ -224,4 +225,57 @@ describe('socket.io access checks', function() {
const message = await handshake(socket, 'other-pad');
assert.equal(message.accessStatus, 'deny');
});
// Authorization levels via authorize hook
it("level='create' -> can create", async () => {
authorize = () => 'create';
settings.requireAuthentication = true;
settings.requireAuthorization = true;
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await connect(res);
const clientVars = await handshake(socket, 'pad');
assert.equal(clientVars.type, 'CLIENT_VARS');
assert.equal(clientVars.data.readonly, false);
});
it('level=true -> can create', async () => {
authorize = () => true;
settings.requireAuthentication = true;
settings.requireAuthorization = true;
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await connect(res);
const clientVars = await handshake(socket, 'pad');
assert.equal(clientVars.type, 'CLIENT_VARS');
assert.equal(clientVars.data.readonly, false);
});
it("level='modify' -> can modify", async () => {
const pad = await padManager.getPad('pad'); // Create the pad.
authorize = () => 'modify';
settings.requireAuthentication = true;
settings.requireAuthorization = true;
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await connect(res);
const clientVars = await handshake(socket, 'pad');
assert.equal(clientVars.type, 'CLIENT_VARS');
assert.equal(clientVars.data.readonly, false);
});
it("level='create' settings.editOnly=true -> unable to create", async () => {
authorize = () => 'create';
settings.requireAuthentication = true;
settings.requireAuthorization = true;
settings.editOnly = true;
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await connect(res);
const message = await handshake(socket, 'pad');
assert.equal(message.accessStatus, 'deny');
});
it("level='modify' settings.editOnly=false -> unable to create", async () => {
authorize = () => 'modify';
settings.requireAuthentication = true;
settings.requireAuthorization = true;
settings.editOnly = false;
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await connect(res);
const message = await handshake(socket, 'pad');
assert.equal(message.accessStatus, 'deny');
});
});