mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
3210eee24f
Summary: By default, only respect GRIST_FORWARD_AUTH_HEADER on login endpoints; sessions are used elsewhere. With GRIST_IGNORE_SESSION, do not use sessions, and respect GRIST_FORWARD_AUTH_HEADER on all endpoints. GRIST_PROXY_AUTH_HEADER is now a synonym to GRIST_FORWARD_AUTH_HEADER. Test Plan: Fixed tests. Tested first approach (no GRIST_IGNORE_SESSION) with grist-omnibus manually. Tested the second approach (with GRIST_IGNORE_SESSION) with a Apache-based setup enforcing http basic auth on all endpoints. Reviewers: paulfitz, georgegevoian Reviewed By: paulfitz, georgegevoian Differential Revision: https://phab.getgrist.com/D4104
320 lines
13 KiB
TypeScript
320 lines
13 KiB
TypeScript
import {parseUrlId} from 'app/common/gristUrls';
|
|
import {HomeDBManager} from 'app/gen-server/lib/HomeDBManager';
|
|
import {DocManager} from 'app/server/lib/DocManager';
|
|
import {FlexServer} from 'app/server/lib/FlexServer';
|
|
import axios from 'axios';
|
|
import {assert} from 'chai';
|
|
import {toPairs} from 'lodash';
|
|
import {createInitialDb, removeConnection, setUpDB} from 'test/gen-server/seed';
|
|
import {configForUser, getGristConfig} from 'test/gen-server/testUtils';
|
|
import {createDocTools} from 'test/server/docTools';
|
|
import {openClient} from 'test/server/gristClient';
|
|
import * as testUtils from 'test/server/testUtils';
|
|
import uuidv4 from 'uuid/v4';
|
|
|
|
let serverUrl: string;
|
|
let server: FlexServer;
|
|
let dbManager: HomeDBManager;
|
|
|
|
async function activateServer(home: FlexServer, docManager: DocManager) {
|
|
await home.loadConfig();
|
|
await home.initHomeDBManager();
|
|
home.addHosts();
|
|
home.addDocWorkerMap();
|
|
home.addAccessMiddleware();
|
|
dbManager = home.getHomeDBManager();
|
|
await home.loadConfig();
|
|
home.addSessions();
|
|
home.addHealthCheck();
|
|
docManager.testSetHomeDbManager(dbManager);
|
|
home.testSetDocManager(docManager);
|
|
await home.start();
|
|
home.addAccessMiddleware();
|
|
home.addApiMiddleware();
|
|
home.addJsonSupport();
|
|
await home.addLandingPages();
|
|
home.addHomeApi();
|
|
await home.addTelemetry();
|
|
await home.addDoc();
|
|
home.addApiErrorHandlers();
|
|
home.finalizeEndpoints();
|
|
await home.finalizePlugins(null);
|
|
home.ready();
|
|
serverUrl = home.getOwnUrl();
|
|
}
|
|
|
|
const chimpy = configForUser('Chimpy');
|
|
const charon = configForUser('Charon');
|
|
|
|
const fixtures: {[docName: string]: string|null} = {
|
|
Bananas: 'Hello.grist',
|
|
Pluto: 'Hello.grist',
|
|
};
|
|
|
|
describe('Authorizer', function() {
|
|
|
|
testUtils.setTmpLogLevel('fatal');
|
|
|
|
const docTools = createDocTools({persistAcrossCases: true, useFixturePlugins: false,
|
|
server: () => (server = new FlexServer(0, 'test docWorker'))});
|
|
const docs: {[name: string]: {id: string}} = {};
|
|
|
|
// Loads the fixtures documents so that they are available to the doc worker under the correct
|
|
// names.
|
|
async function loadFixtureDocs() {
|
|
for (const [docName, fixtureDoc] of toPairs(fixtures)) {
|
|
const docId = String(await dbManager.testGetId(docName));
|
|
if (fixtureDoc) {
|
|
await docTools.loadFixtureDocAs(fixtureDoc, docId);
|
|
} else {
|
|
await docTools.createDoc(docId);
|
|
}
|
|
docs[docName] = {id: docId};
|
|
}
|
|
}
|
|
|
|
let oldEnv: testUtils.EnvironmentSnapshot;
|
|
before(async function() {
|
|
this.timeout(5000);
|
|
setUpDB(this);
|
|
oldEnv = new testUtils.EnvironmentSnapshot();
|
|
// GRIST_PROXY_AUTH_HEADER now only affects requests directly when GRIST_IGNORE_SESSION is
|
|
// also set.
|
|
process.env.GRIST_PROXY_AUTH_HEADER = 'X-email';
|
|
process.env.GRIST_IGNORE_SESSION = 'true';
|
|
await createInitialDb();
|
|
await activateServer(server, docTools.getDocManager());
|
|
await loadFixtureDocs();
|
|
});
|
|
|
|
after(async function() {
|
|
const messages = await testUtils.captureLog('warn', async () => {
|
|
await server.close();
|
|
await removeConnection();
|
|
});
|
|
assert.lengthOf(messages, 0);
|
|
oldEnv.restore();
|
|
});
|
|
|
|
// TODO XXX Is it safe to remove this support now?
|
|
// (It used to be implemented in getDocAccessInfo() in Authorizer.ts).
|
|
it.skip("viewer gets redirect by title", async function() {
|
|
const resp = await axios.get(`${serverUrl}/o/pr/doc/Bananas`, chimpy);
|
|
assert.equal(resp.status, 200);
|
|
assert.equal(getGristConfig(resp.data).assignmentId, 'sample_6');
|
|
assert.match(resp.request.res.responseUrl, /\/doc\/sample_6$/);
|
|
const resp2 = await axios.get(`${serverUrl}/o/nasa/doc/Pluto`, chimpy);
|
|
assert.equal(resp2.status, 200);
|
|
assert.equal(getGristConfig(resp2.data).assignmentId, 'sample_2');
|
|
assert.match(resp2.request.res.responseUrl, /\/doc\/sample_2$/);
|
|
});
|
|
|
|
it("stranger gets consistent refusal regardless of title", async function() {
|
|
const resp = await axios.get(`${serverUrl}/o/pr/doc/Bananas`, charon);
|
|
assert.equal(resp.status, 404);
|
|
assert.notMatch(resp.data, /sample_6/);
|
|
const resp2 = await axios.get(`${serverUrl}/o/pr/doc/Bananas2`, charon);
|
|
assert.equal(resp2.status, 404);
|
|
assert.notMatch(resp.data, /sample_6/);
|
|
assert.deepEqual(withoutTimestamp(resp.data),
|
|
withoutTimestamp(resp2.data));
|
|
});
|
|
|
|
it("viewer can access title", async function() {
|
|
const resp = await axios.get(`${serverUrl}/o/pr/doc/sample_6`, chimpy);
|
|
assert.equal(resp.status, 200);
|
|
const config = getGristConfig(resp.data);
|
|
assert.equal(config.getDoc![config.assignmentId!].name, 'Bananas');
|
|
});
|
|
|
|
it("stranger cannot access title", async function() {
|
|
const resp = await axios.get(`${serverUrl}/o/pr/doc/sample_6`, charon);
|
|
assert.equal(resp.status, 403);
|
|
assert.notMatch(resp.data, /Bananas/);
|
|
});
|
|
|
|
it("viewer cannot access document from wrong org", async function() {
|
|
const resp = await axios.get(`${serverUrl}/o/nasa/doc/sample_6`, chimpy);
|
|
assert.equal(resp.status, 404);
|
|
});
|
|
|
|
it("websocket allows openDoc for viewer", async function() {
|
|
const cli = await openClient(server, 'chimpy@getgrist.com', 'pr');
|
|
cli.ignoreTrivialActions();
|
|
assert.equal((await cli.readMessage()).type, 'clientConnect');
|
|
const openDoc = await cli.send("openDoc", "sample_6");
|
|
assert.equal(openDoc.error, undefined);
|
|
assert.match(JSON.stringify(openDoc.data), /Table1/);
|
|
await cli.close();
|
|
});
|
|
|
|
it("websocket forbids openDoc for stranger", async function() {
|
|
const cli = await openClient(server, 'charon@getgrist.com', 'pr');
|
|
cli.ignoreTrivialActions();
|
|
assert.equal((await cli.readMessage()).type, 'clientConnect');
|
|
const openDoc = await cli.send("openDoc", "sample_6");
|
|
assert.match(openDoc.error!, /No view access/);
|
|
assert.equal(openDoc.data, undefined);
|
|
assert.match(openDoc.errorCode!, /AUTH_NO_VIEW/);
|
|
await cli.close();
|
|
});
|
|
|
|
it("websocket forbids applyUserActions for viewer", async function() {
|
|
const cli = await openClient(server, 'charon@getgrist.com', 'nasa');
|
|
cli.ignoreTrivialActions();
|
|
assert.equal((await cli.readMessage()).type, 'clientConnect');
|
|
const openDoc = await cli.openDocOnConnect("sample_2");
|
|
assert.equal(openDoc.error, undefined);
|
|
const nonce = uuidv4();
|
|
const applyUserActions = await cli.send("applyUserActions",
|
|
0,
|
|
[["UpdateRecord", "Table1", 1, {A: nonce}], {}]);
|
|
assert.lengthOf(cli.messages, 0); // no user actions pushed to client
|
|
assert.match(applyUserActions.error!, /No write access/);
|
|
assert.match(applyUserActions.errorCode!, /AUTH_NO_EDIT/);
|
|
const fetchTable = await cli.send("fetchTable", 0, "Table1");
|
|
assert.equal(fetchTable.error, undefined);
|
|
assert.notInclude(JSON.stringify(fetchTable.data), nonce);
|
|
await cli.close();
|
|
});
|
|
|
|
it("websocket allows applyUserActions for editor", async function() {
|
|
const cli = await openClient(server, 'chimpy@getgrist.com', 'nasa');
|
|
cli.ignoreTrivialActions();
|
|
assert.equal((await cli.readMessage()).type, 'clientConnect');
|
|
const openDoc = await cli.openDocOnConnect("sample_2");
|
|
assert.equal(openDoc.error, undefined);
|
|
const nonce = uuidv4();
|
|
const applyUserActions = await cli.send("applyUserActions",
|
|
0,
|
|
[["UpdateRecord", "Table1", 1, {A: nonce}]]);
|
|
// Skip messages with no actions (since docUsage may or may not appear by now)
|
|
const messagesWithActions = cli.messages.filter(m => m.data.docActions);
|
|
assert.lengthOf(messagesWithActions, 1); // user actions pushed to client
|
|
assert.equal(applyUserActions.error, undefined);
|
|
const fetchTable = await cli.send("fetchTable", 0, "Table1");
|
|
assert.equal(fetchTable.error, undefined);
|
|
assert.include(JSON.stringify(fetchTable.data), nonce);
|
|
await cli.close();
|
|
});
|
|
|
|
it("can keep different simultaneous clients of a doc straight", async function() {
|
|
const editor = await openClient(server, 'chimpy@getgrist.com', 'nasa');
|
|
assert.equal((await editor.readMessage()).type, 'clientConnect');
|
|
const viewer = await openClient(server, 'charon@getgrist.com', 'nasa');
|
|
assert.equal((await viewer.readMessage()).type, 'clientConnect');
|
|
const stranger = await openClient(server, 'kiwi@getgrist.com', 'nasa');
|
|
assert.equal((await stranger.readMessage()).type, 'clientConnect');
|
|
|
|
editor.ignoreTrivialActions();
|
|
viewer.ignoreTrivialActions();
|
|
stranger.ignoreTrivialActions();
|
|
assert.equal((await editor.send("openDoc", "sample_2")).error, undefined);
|
|
assert.equal((await viewer.send("openDoc", "sample_2")).error, undefined);
|
|
assert.match((await stranger.send("openDoc", "sample_2")).error!, /No view access/);
|
|
|
|
const action = [0, [["UpdateRecord", "Table1", 1, {A: "foo"}]]];
|
|
assert.equal((await editor.send("applyUserActions", ...action)).error, undefined);
|
|
assert.match((await viewer.send("applyUserActions", ...action)).error!, /No write access/);
|
|
// Different message here because sending actions without a doc being open.
|
|
assert.match((await stranger.send("applyUserActions", ...action)).error!, /Invalid/);
|
|
});
|
|
|
|
it("previewer has view access to docs", async function() {
|
|
const cli = await openClient(server, 'thumbnail@getgrist.com', 'nasa');
|
|
cli.ignoreTrivialActions();
|
|
assert.equal((await cli.readMessage()).type, 'clientConnect');
|
|
const openDoc = await cli.send("openDoc", "sample_2");
|
|
assert.equal(openDoc.error, undefined);
|
|
const nonce = uuidv4();
|
|
const applyUserActions = await cli.send("applyUserActions",
|
|
0,
|
|
[["UpdateRecord", "Table1", 1, {A: nonce}], {}]);
|
|
assert.lengthOf(cli.messages, 0); // no user actions pushed to client
|
|
assert.match(applyUserActions.error!, /No write access/);
|
|
assert.match(applyUserActions.errorCode!, /AUTH_NO_EDIT/);
|
|
const fetchTable = await cli.send("fetchTable", 0, "Table1");
|
|
assert.equal(fetchTable.error, undefined);
|
|
assert.notInclude(JSON.stringify(fetchTable.data), nonce);
|
|
await cli.close();
|
|
});
|
|
|
|
it("viewer can fork doc", async function() {
|
|
const cli = await openClient(server, 'charon@getgrist.com', 'nasa');
|
|
cli.ignoreTrivialActions();
|
|
assert.equal((await cli.readMessage()).type, 'clientConnect');
|
|
const openDoc = await cli.send("openDoc", "sample_2");
|
|
assert.equal(openDoc.error, undefined);
|
|
const result = await cli.send("fork", 0);
|
|
assert.equal(result.data.docId, result.data.urlId);
|
|
const parts = parseUrlId(result.data.docId);
|
|
assert.equal(parts.trunkId, "sample_2");
|
|
assert.isAbove(parts.forkId!.length, 4);
|
|
assert.equal(parts.forkUserId, await dbManager.testGetId('Charon') as number);
|
|
});
|
|
|
|
it("anon can fork doc", async function() {
|
|
// anon does not have access to doc initially
|
|
const cli = await openClient(server, 'anon@getgrist.com', 'nasa');
|
|
cli.ignoreTrivialActions();
|
|
assert.equal((await cli.readMessage()).type, 'clientConnect');
|
|
let openDoc = await cli.send("openDoc", "sample_2");
|
|
assert.match(openDoc.error!, /No view access/);
|
|
|
|
// grant anon access to doc and retry
|
|
await dbManager.updateDocPermissions({
|
|
userId: await dbManager.testGetId('Chimpy') as number,
|
|
urlId: 'sample_2',
|
|
org: 'nasa'
|
|
}, {users: {"anon@getgrist.com": "viewers"}});
|
|
dbManager.flushDocAuthCache();
|
|
openDoc = await cli.send("openDoc", "sample_2");
|
|
assert.equal(openDoc.error, undefined);
|
|
|
|
// make a fork
|
|
const result = await cli.send("fork", 0);
|
|
assert.equal(result.data.docId, result.data.urlId);
|
|
const parts = parseUrlId(result.data.docId);
|
|
assert.equal(parts.trunkId, "sample_2");
|
|
assert.isAbove(parts.forkId!.length, 4);
|
|
assert.equal(parts.forkUserId, undefined);
|
|
});
|
|
|
|
it("can set user via GRIST_PROXY_AUTH_HEADER", async function() {
|
|
// User can access a doc by setting header.
|
|
const docUrl = `${serverUrl}/o/pr/api/docs/sample_6`;
|
|
const resp = await axios.get(docUrl, {
|
|
headers: {'X-email': 'chimpy@getgrist.com'}
|
|
});
|
|
assert.equal(resp.data.name, 'Bananas');
|
|
|
|
// Unknown user is denied.
|
|
await assert.isRejected(axios.get(docUrl, {
|
|
headers: {'X-email': 'notchimpy@getgrist.com'}
|
|
}));
|
|
|
|
// User can access a doc via websocket by setting header.
|
|
let cli = await openClient(server, 'chimpy@getgrist.com', 'pr', 'X-email');
|
|
cli.ignoreTrivialActions();
|
|
assert.equal((await cli.readMessage()).type, 'clientConnect');
|
|
let openDoc = await cli.send("openDoc", "sample_6");
|
|
assert.equal(openDoc.error, undefined);
|
|
assert.match(JSON.stringify(openDoc.data), /Table1/);
|
|
await cli.close();
|
|
|
|
// Unknown user is denied.
|
|
cli = await openClient(server, 'notchimpy@getgrist.com', 'pr', 'X-email');
|
|
cli.ignoreTrivialActions();
|
|
assert.equal((await cli.readMessage()).type, 'clientConnect');
|
|
openDoc = await cli.send("openDoc", "sample_6");
|
|
assert.match(openDoc.error!, /No view access/);
|
|
assert.equal(openDoc.data, undefined);
|
|
assert.match(openDoc.errorCode!, /AUTH_NO_VIEW/);
|
|
await cli.close();
|
|
});
|
|
});
|
|
|
|
function withoutTimestamp(txt: string): string {
|
|
return txt.replace(/"timestampMs":[ 0-9]+/, '"timestampMs": NNNN');
|
|
}
|