1
0
mirror of https://github.com/gristlabs/grist-core.git synced 2024-10-27 20:44:07 +00:00
gristlabs_grist-core/test/gen-server/ApiServerAccess.ts
Florent 866ec66096
Optimize sql query for workspace acl ()
Without this optimization, we fetched loads of entries from the database, which led to database and nodejs overloads.

We could go further, this is a modest patch towards better performance.

We use two queries: one fetches the workspaces, the second the organization that the workspace belongs to.

---------

Co-authored-by: Florent FAYOLLE <florent.fayolle@beta.gouv.fr>
2024-01-31 14:04:22 -05:00

1715 lines
64 KiB
TypeScript

import {Role} from 'app/common/roles';
import {PermissionData, PermissionDelta} from 'app/common/UserAPI';
import {Deps} from 'app/gen-server/ApiServer';
import {Organization} from 'app/gen-server/entity/Organization';
import {Product} from 'app/gen-server/entity/Product';
import {User} from 'app/gen-server/entity/User';
import {HomeDBManager, UserChange} from 'app/gen-server/lib/HomeDBManager';
import {SendGridConfig, SendGridMail} from 'app/gen-server/lib/NotifierTypes';
import axios, {AxiosResponse} from 'axios';
import {delay} from 'bluebird';
import * as chai from 'chai';
import fromPairs = require('lodash/fromPairs');
import pick = require('lodash/pick');
import * as sinon from 'sinon';
import {TestServer} from 'test/gen-server/apiUtils';
import {configForUser} from 'test/gen-server/testUtils';
import * as testUtils from 'test/server/testUtils';
const assert = chai.assert;
let server: TestServer;
let dbManager: HomeDBManager;
let homeUrl: string;
let userCountUpdates: {[orgId: number]: number[]} = {};
let lastMail: SendGridMail|null = null;
let lastMailDesc: string|null = null;
const sandbox = sinon.createSandbox();
const chimpy = configForUser('Chimpy');
const kiwi = configForUser('Kiwi');
const charon = configForUser('Charon');
const nobody = configForUser('Anonymous');
const chimpyEmail = 'chimpy@getgrist.com';
const kiwiEmail = 'kiwi@getgrist.com';
const charonEmail = 'charon@getgrist.com';
const supportEmail = 'support@getgrist.com';
const everyoneEmail = 'everyone@getgrist.com';
let chimpyRef = '';
let kiwiRef = '';
let charonRef = '';
// Test concerns only access-related functions of the ApiServer. Created to help break up the
// large amount of tests on the ApiServer.
describe('ApiServerAccess', function() {
testUtils.setTmpLogLevel('error');
let notificationsConfig: SendGridConfig|undefined;
before(async function() {
server = new TestServer(this);
homeUrl = await server.start(['home', 'docs']);
notificationsConfig = server.server.getNotifier().testSetSendMessageCallback(
async (payload, desc) => {
// Filter for invite emails only - ignore any other categories of email
if (desc.includes('invite')) {
lastMail = payload;
lastMailDesc = desc;
}
}
);
dbManager = server.dbManager;
chimpyRef = await dbManager.getUserByLogin(chimpyEmail).then((user) => user!.ref);
kiwiRef = await dbManager.getUserByLogin(kiwiEmail).then((user) => user!.ref);
charonRef = await dbManager.getUserByLogin(charonEmail).then((user) => user!.ref);
// Listen to user count updates and add them to an array.
dbManager.on('userChange', ({org, countBefore, countAfter}: UserChange) => {
if (countBefore === countAfter) { return; }
userCountUpdates[org.id] = userCountUpdates[org.id] || [];
userCountUpdates[org.id].push(countAfter);
});
});
afterEach(async function() {
userCountUpdates = {};
await server.sanityCheck();
});
after(async function() {
await server.stop();
sandbox.restore();
});
async function getLastMail(maxWait: number = 1000) {
const start = Date.now();
while (Date.now() - start < maxWait) {
if (!server.server.getNotifier().testPending) {
const result = {payload: lastMail, description: lastMailDesc};
lastMailDesc = null;
lastMail = null;
return result;
}
await delay(1);
}
throw new Error('getMessages timed out');
}
async function assertLastMail(maxWait: number = 1000) {
const {payload, description} = await getLastMail(maxWait);
if (payload === null || description === null) {
throw new Error('no mail available');
}
return {payload, description};
}
it('PATCH /api/orgs/{oid}/access is operational', async function() {
const oid = await dbManager.testGetId('Chimpyland');
const nasaOrgId = await dbManager.testGetId('NASA');
// Assert that Charon is NOT allowed to rename a workspace in Chimpyland
const wid = await dbManager.testGetId('Private');
const charonResp1 = await axios.patch(`${homeUrl}/api/workspaces/${wid}`, {
name: 'Charon-Illegal-Rename'
}, charon);
assert.equal(charonResp1.status, 403);
// Move Charon from 'viewers' to 'editors'.
const delta1 = {
users: {
[charonEmail]: 'editors'
}
};
const resp1 = await axios.patch(`${homeUrl}/api/orgs/${oid}/access`, {delta: delta1}, chimpy);
assert.equal(resp1.status, 200);
if (notificationsConfig) {
// Assert that no mail would be sent (Charon already had access).
assert.equal((await getLastMail()).payload, null);
}
// Assert that the number of users in the org has not been updated (Charon role modified only).
assert.deepEqual(userCountUpdates[oid as number], undefined);
// Assert that Charon is now allowed to rename workspaces in Chimpyland
const charonResp2 = await axios.patch(`${homeUrl}/api/workspaces/${wid}`, {
name: 'Charon-Rename'
}, charon);
assert.equal(charonResp2.status, 200);
// Move Charon back to 'viewers' and add Kiwi to 'editors' (from no permission).
const delta2 = {
users: {
[charonEmail]: 'viewers',
[kiwiEmail]: 'editors'
}
};
const resp2 = await axios.patch(`${homeUrl}/api/orgs/${oid}/access`, {delta: delta2}, chimpy);
assert.equal(resp2.status, 200);
// We should send mail about this one, since Kiwi had no access previously.
if (notificationsConfig) {
const mail = await assertLastMail();
assert.match(mail.description, /^invite kiwi@getgrist.com to http.*\/o\/docs\/\?utm_id=invite-org$/);
const env = mail.payload.personalizations[0].dynamic_template_data;
assert.deepEqual(pick(env, ['resource.name', 'resource.kind', 'resource.kindUpperFirst',
'resource.isTeamSite', 'resource.isWorkspace', 'resource.isDocument',
'host.name', 'host.email',
'user.name', 'user.email',
'access.role', 'access.canEdit', 'access.canView']), {
resource: {
name: 'Chimpyland', kind: 'team site', kindUpperFirst: 'Team site',
isTeamSite: true, isWorkspace: false, isDocument: false
},
host: {name: 'Chimpy', email: 'chimpy@getgrist.com'},
user: {name: 'Kiwi', email: 'kiwi@getgrist.com'},
access: {role: 'editors', canEdit: true, canView: true}
} as any);
assert.match(env.resource.url, /^http.*\/o\/docs\/\?utm_id=invite-org$/);
assert.deepEqual(mail.payload.personalizations[0].to[0], {email: 'kiwi@getgrist.com', name: 'Kiwi'});
assert.deepEqual(mail.payload.from, {email: 'support@getgrist.com', name: 'Chimpy (via Grist)'});
assert.deepEqual(mail.payload.reply_to, {email: 'chimpy@getgrist.com', name: 'Chimpy'});
assert.deepEqual(mail.payload.template_id, notificationsConfig.template.invite);
}
// Assert that the number of users in the org has updated (Kiwi was added).
assert.deepEqual(userCountUpdates[oid as number], [3]);
// Assert that Charon is once again NOT allowed to rename workspaces in Chimpyland
const charonResp3 = await axios.patch(`${homeUrl}/api/workspaces/${wid}`, {
name: 'Charon-Illegal-Rename-2'
}, charon);
assert.equal(charonResp3.status, 403);
// Assert that Kiwi is now allowed to rename workspaces in Chimpyland
const kiwiResp1 = await axios.patch(`${homeUrl}/api/workspaces/${wid}`, {
name: 'Private'
}, kiwi);
assert.equal(kiwiResp1.status, 200);
// Revert the changes and check that behavior is expected once more for good measure.
const delta3 = {
users: {
[kiwiEmail]: null
}
};
const resp3 = await axios.patch(`${homeUrl}/api/orgs/${oid}/access`, {delta: delta3}, chimpy);
assert.equal(resp3.status, 200);
if (notificationsConfig) {
assert.equal((await getLastMail()).description, null);
}
// Assert that the number of users in the org has updated (Kiwi was removed).
assert.deepEqual(userCountUpdates[oid as number], [3, 2]);
// Assert that Kiwi is NOT allowed to rename workspaces in Chimpyland
const kiwiResp2 = await axios.patch(`${homeUrl}/api/workspaces/${wid}`, {
name: 'Kiwi-Illegal-Rename-2'
}, kiwi);
assert.equal(kiwiResp2.status, 403);
// Give Kiwi access to NASA as an editor.
// NOTE: This tests a bug with adding users to orgs that contain guests. The bug caused existing
// guests of the org to be removed on any access update.
const delta4 = {
users: {
[kiwiEmail]: 'editors'
}
};
const resp4 = await axios.patch(`${homeUrl}/api/orgs/${nasaOrgId}/access`, {delta: delta4}, chimpy);
assert.equal(resp4.status, 200);
if (notificationsConfig) {
assert.match(
(await assertLastMail()).description,
/^invite kiwi@getgrist.com to http.*\/o\/nasa\/\?utm_id=invite-org$/
);
}
// Assert that the number of users in the org has updated (Kiwi was added).
assert.deepEqual(userCountUpdates[nasaOrgId as number], [2]);
// Check that access to NASA is as expected.
const resp5 = await axios.get(`${homeUrl}/api/orgs/${nasaOrgId}/access`, chimpy);
assert.equal(resp5.status, 200);
assert.deepEqual(resp5.data, {
users: [{
id: 1,
name: 'Chimpy',
email: chimpyEmail,
ref: chimpyRef,
picture: null,
access: "owners",
isMember: true,
}, {
id: 2,
name: 'Kiwi',
email: kiwiEmail,
ref: kiwiRef,
picture: null,
access: "editors",
isMember: true,
}, {
id: 3,
name: 'Charon',
email: charonEmail,
ref: charonRef,
picture: null,
access: "guests",
isMember: false,
}]
});
// Revoke Kiwi's access to NASA.
const delta6 = {
users: {
[kiwiEmail]: null
}
};
const resp6 = await axios.patch(`${homeUrl}/api/orgs/${nasaOrgId}/access`, {delta: delta6}, chimpy);
assert.equal(resp6.status, 200);
if (notificationsConfig) {
assert.equal((await getLastMail()).description, null);
}
// Assert that the number of users in the org has updated (Kiwi was removed).
assert.deepEqual(userCountUpdates[nasaOrgId as number], [2, 1]);
// Check that access to NASA is again as expected, this time without Kiwi present.
const resp7 = await axios.get(`${homeUrl}/api/orgs/${nasaOrgId}/access`, chimpy);
assert.equal(resp7.status, 200);
assert.deepEqual(resp7.data, {
users: [{
id: 1,
name: 'Chimpy',
email: chimpyEmail,
ref: chimpyRef,
picture: null,
access: "owners",
isMember: true,
}, {
id: 3,
name: 'Charon',
email: charonEmail,
ref: charonRef,
picture: null,
access: "guests",
isMember: false,
}]
});
});
it('PATCH /api/orgs/{oid}/access allows non-owners to remove themselves', async function() {
const oid = await dbManager.testGetId('NASA');
const url = `${homeUrl}/api/orgs/${oid}/access`;
await testAllowNonOwnersToRemoveThemselves(url);
});
it('PATCH /api/orgs/{oid}/access returns 404 appropriately', async function() {
const delta = {
users: {
[charonEmail]: null
}
};
const resp = await axios.patch(`${homeUrl}/api/orgs/9999/access`, {delta}, chimpy);
assert.equal(resp.status, 404);
});
it('PATCH /api/orgs/{oid}/access returns 403 appropriately', async function() {
// Attempt to set access with a user that does not have ACL_EDIT permissions.
const oid = await dbManager.testGetId('Chimpyland');
const delta = {
users: {
[kiwiEmail]: 'viewers'
}
};
const resp = await axios.patch(`${homeUrl}/api/orgs/${oid}/access`, {delta}, charon);
assert.equal(resp.status, 403);
});
it('PATCH /api/orgs/{oid}/access returns 400 appropriately', async function() {
// Omit the delta and check that the operation fails with 400.
const oid = await dbManager.testGetId('Chimpyland');
const resp1 = await axios.patch(`${homeUrl}/api/orgs/${oid}/access`, {}, chimpy);
assert.equal(resp1.status, 400);
// Omit the users object and check that the operation fails with 400.
const resp2 = await axios.patch(`${homeUrl}/api/orgs/${oid}/access`, {delta: {}}, chimpy);
assert.equal(resp2.status, 400);
// Include a maxInheritedRole value and check that the operation fails with 400.
const delta1 = {maxInheritedRole: null};
const resp3 = await axios.patch(`${homeUrl}/api/orgs/${oid}/access`, {delta: delta1}, chimpy);
assert.equal(resp3.status, 400);
// Attempt to update own permissions check that the operation fails with 400.
const delta2 = {
users: {
[chimpyEmail]: 'viewers'
}
};
const resp4 = await axios.patch(`${homeUrl}/api/orgs/${oid}/access`, {delta: delta2}, chimpy);
assert.equal(resp4.status, 400);
});
it('PATCH /api/workspaces/{wid}/access is operational', async function() {
const oid = await dbManager.testGetId('Chimpyland');
const wid = await dbManager.testGetId('Private');
// Assert that Kiwi is unable to GET the org, since Kiwi has no permissions on the org.
const kiwiResp1 = await axios.get(`${homeUrl}/api/orgs/${oid}`, kiwi);
assert.equal(kiwiResp1.status, 403);
const delta0 = {
users: {[kiwiEmail]: 'members'}
};
const resp0 = await axios.patch(`${homeUrl}/api/orgs/${oid}/access`, {delta: delta0}, chimpy);
assert.equal(resp0.status, 200);
// Make Kiwi an editor of the workspace
const delta1 = {
users: {[kiwiEmail]: 'editors'}
};
const resp2 = await axios.patch(`${homeUrl}/api/workspaces/${wid}/access`, {delta: delta1}, chimpy);
assert.equal(resp2.status, 200);
// Check we would sent an email to Kiwi about this
if (notificationsConfig) {
const mail = await assertLastMail();
assert.match(mail.description, /^invite kiwi@getgrist.com to http.*\/o\/docs\/ws\/[0-9]+\/\?utm_id=invite-ws$/);
const env = mail.payload.personalizations[0].dynamic_template_data;
assert.match(env.resource.url, /^http.*\/o\/docs\/ws\/[0-9]+\/\?utm_id=invite-ws$/);
assert.equal(env.resource.kind, 'workspace');
assert.equal(env.resource.kindUpperFirst, 'Workspace');
assert.equal(env.resource.isTeamSite, false);
assert.equal(env.resource.isWorkspace, true);
assert.equal(env.resource.isDocument, false);
assert.equal(env.resource.name, 'Private');
}
// Assert that the number of users in Chimpyland has updated (Kiwi was added).
assert.deepEqual(userCountUpdates[oid as number], [3]);
// Assert that Kiwi is now allowed to rename workspace 'Private' in Chimpyland
const kiwiResp2 = await axios.patch(`${homeUrl}/api/workspaces/${wid}`, {
name: 'Kiwi-Rename'
}, kiwi);
assert.equal(kiwiResp2.status, 200);
// Assert that Kiwi is also now able to GET the org, since Kiwi is now a guest of the org.
const kiwiResp3 = await axios.get(`${homeUrl}/api/orgs/${oid}`, kiwi);
assert.equal(kiwiResp3.status, 200);
// Set the maxInheritedRole to 'viewers'
const delta2 = {
maxInheritedRole: 'viewers'
};
const resp3 = await axios.patch(`${homeUrl}/api/workspaces/${wid}/access`, {delta: delta2}, chimpy);
assert.equal(resp3.status, 200);
if (notificationsConfig) {
assert.equal((await getLastMail()).description, null);
}
// Assert that Kiwi is still allowed to rename the workspace.
const kiwiResp4 = await axios.patch(`${homeUrl}/api/workspaces/${wid}`, {
name: 'Kiwi-Rename2'
}, kiwi);
assert.equal(kiwiResp4.status, 200);
// Assert that Charon is still allowed to GET the workspace.
const charonResp1 = await axios.get(`${homeUrl}/api/workspaces/${wid}`, charon);
assert.equal(charonResp1.status, 200);
// Assert that as the owner, Chimpy can still rename the workspace.
const resp4 = await axios.patch(`${homeUrl}/api/workspaces/${wid}`, {
name: 'Chimpy-Rename'
}, chimpy);
assert.equal(resp4.status, 200);
// Remove inheritance and also update Kiwi's role to viewer.
const delta3 = {
maxInheritedRole: null,
users: {
[kiwiEmail]: 'viewers'
}
};
const resp5 = await axios.patch(`${homeUrl}/api/workspaces/${wid}/access`, {delta: delta3}, chimpy);
assert.equal(resp5.status, 200);
if (notificationsConfig) {
assert.equal((await getLastMail()).description, null);
}
// Assert that Kiwi can still GET the workspace.
const kiwiResp5 = await axios.get(`${homeUrl}/api/workspaces/${wid}`, kiwi);
assert.equal(kiwiResp5.status, 200);
// Assert that Charon can NOT GET the workspace.
const charonResp2 = await axios.get(`${homeUrl}/api/workspaces/${wid}`, charon);
assert.equal(charonResp2.status, 403);
// Assert that as the owner, Chimpy can still rename the workspace.
const resp6 = await axios.patch(`${homeUrl}/api/workspaces/${wid}`, {
name: 'Chimpy-Rename2'
}, chimpy);
assert.equal(resp6.status, 200);
// Add Charon as an editor to 'Public', and make sure it does NOT affect org
// guest access for Kiwi.
const wid2 = await dbManager.testGetId('Public');
const delta4 = {
users: {
[charonEmail]: 'editors'
}
};
const resp7 = await axios.patch(`${homeUrl}/api/workspaces/${wid2}/access`, {delta: delta4}, chimpy);
assert.equal(resp7.status, 200);
if (notificationsConfig) {
assert.match((await assertLastMail()).description, /^invite charon@getgrist.com /);
}
// Assert that Kiwi is still able to GET the org, since Kiwi is still a guest
// of the org.
const kiwiResp6 = await axios.get(`${homeUrl}/api/orgs/${oid}`, kiwi);
assert.equal(kiwiResp6.status, 200);
// Remove Charon's custom permissions to 'Public'
const delta5 = {
users: {
[charonEmail]: null
}
};
const resp8 = await axios.patch(`${homeUrl}/api/workspaces/${wid2}/access`, {delta: delta5}, chimpy);
assert.equal(resp8.status, 200);
if (notificationsConfig) {
assert.equal((await getLastMail()).description, null);
}
// Reset inheritance and remove Kiwi's custom permissions
const delta6 = {
maxInheritedRole: 'owners'
};
const resp9 = await axios.patch(`${homeUrl}/api/workspaces/${wid}/access`, {delta: delta6}, chimpy);
assert.equal(resp9.status, 200);
if (notificationsConfig) {
assert.equal((await getLastMail()).description, null);
}
const removeKiwiDelta = {
users: {[kiwiEmail]: null}
};
const removeKiwiResp = await axios.patch(`${homeUrl}/api/orgs/${oid}/access`,
{delta: removeKiwiDelta}, chimpy);
assert.equal(removeKiwiResp.status, 200);
// TODO: Unnecessary once removing from org removes from all.
const removeKiwiResp2 = await axios.patch(`${homeUrl}/api/workspaces/${wid}/access`,
{delta: removeKiwiDelta}, chimpy);
assert.equal(removeKiwiResp2.status, 200);
// Assert that the number of users in the org has updated (Kiwi was removed).
assert.deepEqual(userCountUpdates[oid as number], [3, 2]);
// Assert that Kiwi is NOT allowed to GET the workspace
const kiwiResp7 = await axios.get(`${homeUrl}/api/workspaces/${wid}`, kiwi);
assert.equal(kiwiResp7.status, 403);
// Assert that Charon can once again GET the workspace
const charonResp3 = await axios.get(`${homeUrl}/api/workspaces/${wid}`, charon);
assert.equal(charonResp3.status, 200);
// Assert that as the owner, Chimpy can still rename the workspace.
const resp10 = await axios.patch(`${homeUrl}/api/workspaces/${wid}`, {
name: 'Private'
}, chimpy);
assert.equal(resp10.status, 200);
// Assert that Kiwi is no longer able to GET the org, since Kiwi is no longer a guest
// of the org.
const kiwiResp8 = await axios.get(`${homeUrl}/api/orgs/${oid}`, kiwi);
assert.equal(kiwiResp8.status, 403);
});
it('PATCH /api/workspaces/{wid}/access allows non-owners to remove themselves', async function() {
const wid = await dbManager.testGetId('Private');
const url = `${homeUrl}/api/workspaces/${wid}/access`;
await testAllowNonOwnersToRemoveThemselves(url);
});
it('PATCH /api/workspaces/{wid}/access returns 404 appropriately', async function() {
const delta = {
users: {
[charonEmail]: null
}
};
const resp = await axios.patch(`${homeUrl}/api/workspaces/9999/access`, {delta}, chimpy);
assert.equal(resp.status, 404);
});
it('PATCH /api/workspaces/{wid}/access returns 403 appropriately', async function() {
// Attempt to set access with a user that does not have ACL_EDIT permissions.
const wid = await dbManager.testGetId('Private');
const delta = {
users: {
[kiwiEmail]: 'viewers'
}
};
const resp = await axios.patch(`${homeUrl}/api/workspaces/${wid}/access`, {delta}, charon);
assert.equal(resp.status, 403);
});
it('PATCH /api/workspaces/{wid}/access returns 400 appropriately', async function() {
// Omit the delta and check that the operation fails with 400.
const wid = await dbManager.testGetId('Private');
const resp1 = await axios.patch(`${homeUrl}/api/workspaces/${wid}/access`, {}, chimpy);
assert.equal(resp1.status, 400);
// Omit the content and check that the operation fails with 400.
const resp2 = await axios.patch(`${homeUrl}/api/workspaces/${wid}/access`, {delta: {}}, chimpy);
assert.equal(resp2.status, 400);
// Attempt to update own permissions check that the operation fails with 400.
const delta = {
users: {
[chimpyEmail]: 'viewers'
}
};
const resp3 = await axios.patch(`${homeUrl}/api/workspaces/${wid}/access`, {delta}, chimpy);
assert.equal(resp3.status, 400);
});
it('PATCH /api/docs/{did}/access is operational', async function() {
const oid = await dbManager.testGetId('Chimpyland');
const wid = await dbManager.testGetId('Private');
const did = await dbManager.testGetId('Timesheets');
// Assert that Kiwi is unable to GET the workspace, since Kiwi has no permissions on
// the org/workspace.
const kiwiResp1 = await axios.get(`${homeUrl}/api/workspaces/${wid}`, kiwi);
assert.equal(kiwiResp1.status, 403);
// Make Kiwi a member of the org.
const delta0 = {
users: {[kiwiEmail]: 'members'}
};
const resp1 = await axios.patch(`${homeUrl}/api/orgs/${oid}/access`, {delta: delta0}, chimpy);
assert.equal(resp1.status, 200);
// Make Kiwi a doc editor for Timesheets
const delta1 = {
users: {[kiwiEmail]: 'editors'}
};
const resp2 = await axios.patch(`${homeUrl}/api/docs/${did}/access`, {delta: delta1}, chimpy);
assert.equal(resp2.status, 200);
// Check we would sent an email to Kiwi about this
if (notificationsConfig) {
const mail = await assertLastMail();
assert.match(mail.description, /^invite kiwi@getgrist.com to http.*\/o\/docs\/doc\/.*$/);
const env = mail.payload.personalizations[0].dynamic_template_data;
assert.match(env.resource.url, /^http.*\/o\/docs\/doc\/.*$/);
assert.equal(env.resource.kind, 'document');
assert.equal(env.resource.kindUpperFirst, 'Document');
assert.equal(env.resource.isTeamSite, false);
assert.equal(env.resource.isWorkspace, false);
assert.equal(env.resource.isDocument, true);
assert.equal(env.resource.name, 'Timesheets');
}
// Assert that the number of users in Chimpyland has updated (Kiwi was added).
assert.deepEqual(userCountUpdates[oid as number], [3]);
// Assert that Kiwi is not allowed to rename doc 'Timesheets' in Chimpyland
const kiwiResp2 = await axios.patch(`${homeUrl}/api/docs/${did}`, {
name: 'Kiwi-Rename'
}, kiwi);
assert.equal(kiwiResp2.status, 403);
// Assert that Kiwi is also now able to GET the workspace, since Kiwi is now a guest of
// the workspace.
const kiwiResp3 = await axios.get(`${homeUrl}/api/workspaces/${wid}`, kiwi);
assert.equal(kiwiResp3.status, 200);
// Assert that Kiwi is also now able to GET the org, since Kiwi is now a guest/member of the org.
const kiwiResp4 = await axios.get(`${homeUrl}/api/orgs/${oid}`, kiwi);
assert.equal(kiwiResp4.status, 200);
// Set the maxInheritedRole to null
const delta2 = {maxInheritedRole: null};
const resp3 = await axios.patch(`${homeUrl}/api/docs/${did}/access`, {delta: delta2}, chimpy);
assert.equal(resp3.status, 200);
if (notificationsConfig) {
assert.equal((await getLastMail()).description, null);
}
// Assert that Kiwi is still not allowed to rename the doc.
const kiwiResp5 = await axios.patch(`${homeUrl}/api/docs/${did}`, {
name: 'Kiwi-Rename2'
}, kiwi);
assert.equal(kiwiResp5.status, 403);
// Assert that Charon cannot view 'Timesheets'.
const charonResp1 = await axios.get(`${homeUrl}/api/workspaces/${wid}`, charon);
assert.equal(charonResp1.status, 200);
assert.deepEqual(charonResp1.data.docs.map((doc: any) => doc.name), ['Appointments']);
// Assert that as the owner, Chimpy can still rename the doc.
const resp4 = await axios.patch(`${homeUrl}/api/docs/${did}`, {
name: 'Chimpy-Rename'
}, chimpy);
assert.equal(resp4.status, 200);
// Add inheritance for viewers and also update Kiwi's role to viewer.
const delta3 = {
maxInheritedRole: 'viewers',
users: {
[kiwiEmail]: 'viewers'
}
};
const resp5 = await axios.patch(`${homeUrl}/api/docs/${did}/access`, {delta: delta3}, chimpy);
assert.equal(resp5.status, 200);
if (notificationsConfig) {
assert.equal((await getLastMail()).description, null);
}
// Assert that Kiwi can still view the doc.
const kiwiResp6 = await axios.get(`${homeUrl}/api/workspaces/${wid}`, kiwi);
assert.equal(kiwiResp6.status, 200);
assert.deepEqual(kiwiResp6.data.docs.map((doc: any) => doc.name),
['Chimpy-Rename']);
// Assert that Charon can now view the doc.
const charonResp2 = await axios.get(`${homeUrl}/api/workspaces/${wid}`, charon);
assert.equal(charonResp2.status, 200);
assert.deepEqual(charonResp2.data.docs.map((doc: any) => doc.name),
['Chimpy-Rename', 'Appointments']);
// Assert that Charon can NOT rename the doc.
const charonResp3 = await axios.patch(`${homeUrl}/api/docs/${did}`, {
name: 'Charon-Invalid-Rename',
}, charon);
assert.equal(charonResp3.status, 403);
// Assert that as the owner, Chimpy can still rename the doc.
const resp6 = await axios.patch(`${homeUrl}/api/docs/${did}`, {
name: 'Timesheets'
}, chimpy);
assert.equal(resp6.status, 200);
// Add Charon as an editor to 'Appointments', and make sure it does NOT affect org
// or workspace guest access for Kiwi.
const did2 = await dbManager.testGetId('Appointments');
const delta4 = {
users: {
[charonEmail]: 'editors'
}
};
const resp7 = await axios.patch(`${homeUrl}/api/docs/${did2}/access`, {delta: delta4}, chimpy);
assert.equal(resp7.status, 200);
if (notificationsConfig) {
assert.match((await assertLastMail()).description, /^invite charon@getgrist.com /);
}
// Assert that Kiwi is still able to GET the workspace, since Kiwi is still a
// guest of the workspace.
const kiwiResp7 = await axios.get(`${homeUrl}/api/workspaces/${wid}`, kiwi);
assert.equal(kiwiResp7.status, 200);
// Assert that Kiwi is still able to GET the org, since Kiwi is still a guest
// of the org.
const kiwiResp8 = await axios.get(`${homeUrl}/api/orgs/${oid}`, kiwi);
assert.equal(kiwiResp8.status, 200);
// Remove Charon's custom permissions to 'Appointments'
const delta5 = {
users: {
[charonEmail]: null
}
};
const resp8 = await axios.patch(`${homeUrl}/api/docs/${did2}/access`, {delta: delta5}, chimpy);
assert.equal(resp8.status, 200);
// Reset doc inheritance setting
const delta6 = {
maxInheritedRole: 'owners',
};
const resp9 = await axios.patch(`${homeUrl}/api/docs/${did}/access`, {delta: delta6}, chimpy);
assert.equal(resp9.status, 200);
if (notificationsConfig) {
assert.equal((await getLastMail()).description, null);
}
// Remove Kiwi from the org.
const removeKiwiDelta = {
users: {[kiwiEmail]: null}
};
const resp10 = await axios.patch(`${homeUrl}/api/orgs/${oid}/access`,
{delta: removeKiwiDelta}, chimpy);
assert.equal(resp10.status, 200);
// TODO: Unnecessary once removing from org removes from all.
const resp11 = await axios.patch(`${homeUrl}/api/docs/${did}/access`,
{delta: removeKiwiDelta}, chimpy);
assert.equal(resp11.status, 200);
// Assert that the number of users in Chimpyland has updated (Kiwi was removed).
assert.deepEqual(userCountUpdates[oid as number], [3, 2]);
// Assert that Kiwi is no longer able to GET the workspace, since Kiwi is no longer a
// guest of the workspace.
const kiwiResp9 = await axios.get(`${homeUrl}/api/workspaces/${wid}`, kiwi);
assert.equal(kiwiResp9.status, 403);
// Assert that Kiwi is no longer able to GET the org, since Kiwi is no longer a guest/member
// of the org.
const kiwiResp10 = await axios.get(`${homeUrl}/api/orgs/${oid}`, kiwi);
assert.equal(kiwiResp10.status, 403);
});
it('PATCH /api/docs/{did}/access allows non-owners to remove themselves', async function() {
const did = await dbManager.testGetId('Timesheets');
const url = `${homeUrl}/api/docs/${did}/access`;
await testAllowNonOwnersToRemoveThemselves(url);
});
it('PATCH /api/docs/{did}/access can send multiple invites', async function() {
const did = await dbManager.testGetId('Timesheets');
let delta: PermissionDelta = {
users: {
'user1@getgrist.com': 'editors',
'user2@getgrist.com': 'viewers',
'user3@getgrist.com': 'viewers',
}
};
let resp = await axios.patch(`${homeUrl}/api/docs/${did}/access`, {delta}, chimpy);
assert.equal(resp.status, 200);
if (notificationsConfig) {
const mail = await assertLastMail();
assert.lengthOf(mail.payload.personalizations, 3);
assert.sameMembers(mail.payload.personalizations.map(p => p.to[0].email),
['user1@getgrist.com', 'user2@getgrist.com', 'user3@getgrist.com']);
assert.deepEqual(mail.payload.personalizations.map(p => p.dynamic_template_data.access),
[{role: 'editors', canEdit: true, canView: true, canEditAccess: false},
{role: 'viewers', canEdit: false, canView: true, canEditAccess: false},
{role: 'viewers', canEdit: false, canView: true, canEditAccess: false}]);
}
delta = {
users: {
'user2@getgrist.com': null,
'user3@getgrist.com': 'editors',
'user4@getgrist.com': 'viewers',
}
};
resp = await axios.patch(`${homeUrl}/api/docs/${did}/access`, {delta}, chimpy);
assert.equal(resp.status, 200);
if (notificationsConfig) {
const mail = await assertLastMail();
assert.lengthOf(mail.payload.personalizations, 1);
assert.deepEqual(mail.payload.personalizations[0].to, [{
email: 'user4@getgrist.com',
name: '', // name is blank since this user has never logged in.
}]);
}
delta = {
users: {
'user1@getgrist.com': null,
'user3@getgrist.com': null,
'user4@getgrist.com': null,
}
};
resp = await axios.patch(`${homeUrl}/api/docs/${did}/access`, {delta}, chimpy);
assert.equal(resp.status, 200);
if (notificationsConfig) {
assert.equal((await getLastMail()).payload, null);
}
});
it('PATCH /api/docs/{did}/access returns 404 appropriately', async function() {
const delta = {
users: {
[charonEmail]: null
}
};
const resp = await axios.patch(`${homeUrl}/api/docs/9999/access`, {delta}, chimpy);
assert.equal(resp.status, 404);
});
it('PATCH /api/docs/{did}/access returns 403 appropriately', async function() {
// Attempt to set access with a user that does not have ACL_EDIT permissions.
const did = await dbManager.testGetId('Timesheets');
const delta = {
users: {
[kiwiEmail]: 'viewers'
}
};
const resp = await axios.patch(`${homeUrl}/api/docs/${did}/access`, {delta}, charon);
assert.equal(resp.status, 403);
});
it('PATCH /api/docs/{did}/access returns 400 appropriately', async function() {
// Omit the delta and check that the operation fails with 400.
const did = await dbManager.testGetId('Timesheets');
const resp1 = await axios.patch(`${homeUrl}/api/docs/${did}/access`, {}, chimpy);
assert.equal(resp1.status, 400);
// Omit the content and check that the operation fails with 400.
const resp2 = await axios.patch(`${homeUrl}/api/docs/${did}/access`, {delta: {}}, chimpy);
assert.equal(resp2.status, 400);
// Attempt to update own permissions check that the operation fails with 400.
const delta = {
users: {
[chimpyEmail]: 'viewers'
}
};
const resp3 = await axios.patch(`${homeUrl}/api/docs/${did}/access`, {delta}, chimpy);
assert.equal(resp3.status, 400);
});
it('GET /api/orgs/{oid}/access is operational', async function() {
const oid = await dbManager.testGetId('Chimpyland');
const resp = await axios.get(`${homeUrl}/api/orgs/${oid}/access`, chimpy);
assert.equal(resp.status, 200);
assert.deepEqual(resp.data, {
users: [{
id: 1,
name: 'Chimpy',
email: chimpyEmail,
ref: chimpyRef,
picture: null,
access: "owners",
isMember: true,
}, {
id: 3,
name: 'Charon',
email: charonEmail,
ref: charonRef,
picture: null,
access: "viewers",
isMember: true,
}]
});
});
it('GET /api/orgs/{oid}/access returns 404 appropriately', async function() {
const resp = await axios.get(`${homeUrl}/api/orgs/9999/access`, chimpy);
assert.equal(resp.status, 404);
});
it('GET /api/orgs/{oid}/access returns 403 appropriately', async function() {
const oid = await dbManager.testGetId('Chimpyland');
const resp = await axios.get(`${homeUrl}/api/orgs/${oid}/access`, kiwi);
assert.equal(resp.status, 403);
});
it('GET /api/workspaces/{wid}/access is operational', async function() {
// Run a simple case on a Chimpyland workspace
const oid = await dbManager.testGetId('Chimpyland');
const wid = await dbManager.testGetId('Public');
const resp1 = await axios.get(`${homeUrl}/api/workspaces/${wid}/access`, chimpy);
assert.equal(resp1.status, 200);
assert.deepEqual(resp1.data, {
maxInheritedRole: "owners",
users: [{
id: 1,
name: 'Chimpy',
email: chimpyEmail,
ref: chimpyRef,
picture: null,
access: null,
parentAccess: "owners",
isMember: true,
}, {
id: 3,
name: 'Charon',
email: charonEmail,
ref: charonRef,
picture: null,
access: null,
parentAccess: "viewers",
isMember: true,
}]
});
// Run a complex case by modifying maxInheritedRole and individual roles on the workspace,
// then querying for access
// Set the maxInheritedRole to null
const kiwiMemberDelta = {
users: {[kiwiEmail]: "members"}
};
const orgPatchResp = await axios.patch(`${homeUrl}/api/orgs/${oid}/access`,
{delta: kiwiMemberDelta}, chimpy);
assert.equal(orgPatchResp.status, 200);
const delta = {
maxInheritedRole: null,
users: {
[kiwiEmail]: "editors"
}
};
const patchResp = await axios.patch(`${homeUrl}/api/workspaces/${wid}/access`, {delta}, chimpy);
assert.equal(patchResp.status, 200);
const resp2 = await axios.get(`${homeUrl}/api/workspaces/${wid}/access`, chimpy);
assert.equal(resp2.status, 200);
assert.deepEqual(resp2.data, {
maxInheritedRole: null,
users: [{
id: 1,
name: 'Chimpy',
email: chimpyEmail,
ref: chimpyRef,
picture: null,
// Note that chimpy's access has been elevated to "owners"
access: "owners",
parentAccess: "owners",
isMember: true,
}, {
id: 2,
name: 'Kiwi',
email: kiwiEmail,
ref: kiwiRef,
picture: null,
access: "editors",
parentAccess: null,
isMember: true,
}, {
id: 3,
name: 'Charon',
email: charonEmail,
ref: charonRef,
picture: null,
access: null,
parentAccess: "viewers",
isMember: true,
}]
});
const deltaOrg = {
users: {
[kiwiEmail]: "owners",
}
};
const respDeltaOrg = await axios.patch(`${homeUrl}/api/orgs/${oid}/access`, {delta: deltaOrg}, chimpy);
assert.equal(respDeltaOrg.status, 200);
const resp3 = await axios.get(`${homeUrl}/api/workspaces/${wid}/access`, chimpy);
assert.include(resp3.data.users.find((user: any) => user.email === kiwiEmail), {
access: "editors",
parentAccess: "owners"
});
// Reset the access settings
const resetDelta = {
maxInheritedRole: "owners",
users: {
[kiwiEmail]: null
}
};
const resetResp = await axios.patch(`${homeUrl}/api/workspaces/${wid}/access`, {delta: resetDelta}, chimpy);
assert.equal(resetResp.status, 200);
const resetOrgDelta = {
users: {
[kiwiEmail]: "members",
}
};
const resetOrgResp = await axios.patch(`${homeUrl}/api/orgs/${oid}/access`, {delta: resetOrgDelta}, chimpy);
assert.equal(resetOrgResp.status, 200);
// Assert that ws guests are properly displayed.
// Tests a minor bug that showed ws guests as having null access.
// Add a doc to 'Public', and add Kiwi to the doc.
// Add a doc to 'Public'
const addDocResp = await axios.post(`${homeUrl}/api/workspaces/${wid}/docs`, {
name: 'PublicDoc'
}, chimpy);
// Assert that the response is successful
assert.equal(addDocResp.status, 200);
const did = addDocResp.data;
// Add Kiwi to the doc
const docAccessResp = await axios.patch(`${homeUrl}/api/docs/${did}/access`, {delta}, chimpy);
assert.equal(docAccessResp.status, 200);
// Assert that Kiwi is now a guest of public.
const wsResp = await axios.get(`${homeUrl}/api/workspaces/${wid}/access`, chimpy);
assert.equal(wsResp.status, 200);
assert.deepEqual(wsResp.data, {
maxInheritedRole: "owners",
users: [{
id: 1,
name: 'Chimpy',
email: chimpyEmail,
ref: chimpyRef,
picture: null,
access: "owners",
parentAccess: "owners",
isMember: true,
}, {
id: 2,
name: 'Kiwi',
email: kiwiEmail,
ref: kiwiRef,
picture: null,
access: "guests",
parentAccess: null,
isMember: true,
}, {
id: 3,
name: 'Charon',
email: charonEmail,
ref: charonRef,
picture: null,
access: null,
parentAccess: "viewers",
isMember: true,
}]
});
// Remove the doc.
const deleteResp = await axios.delete(`${homeUrl}/api/docs/${did}`, chimpy);
assert.equal(deleteResp.status, 200);
// Assert that Kiwi is no longer a guest of public.
const wsResp2 = await axios.get(`${homeUrl}/api/workspaces/${wid}/access`, chimpy);
assert.equal(wsResp2.status, 200);
assert.deepEqual(wsResp2.data, {
maxInheritedRole: "owners",
users: [{
id: 1,
name: 'Chimpy',
email: chimpyEmail,
ref: chimpyRef,
picture: null,
access: "owners",
parentAccess: "owners",
isMember: true,
}, {
id: 2,
name: "Kiwi",
email: kiwiEmail,
ref: kiwiRef,
picture: null,
access: null,
parentAccess: null,
isMember: true,
}, {
id: 3,
name: 'Charon',
email: charonEmail,
ref: charonRef,
picture: null,
access: null,
parentAccess: "viewers",
isMember: true,
}]
});
// Remove Kiwi from the org to reset initial settings
const kiwiResetDelta = {
users: {[kiwiEmail]: null}
};
const orgPatchResp2 = await axios.patch(`${homeUrl}/api/orgs/${oid}/access`,
{delta: kiwiResetDelta}, chimpy);
assert.equal(orgPatchResp2.status, 200);
});
it('GET /api/workspaces/{wid}/access returns 404 appropriately', async function() {
const resp = await axios.get(`${homeUrl}/api/workspaces/9999/access`, chimpy);
assert.equal(resp.status, 404);
});
it('GET /api/workspaces/{wid}/access returns 403 appropriately', async function() {
const wid = await dbManager.testGetId('Private');
const resp = await axios.get(`${homeUrl}/api/workspaces/${wid}/access`, kiwi);
assert.equal(resp.status, 403);
});
it('GET /api/docs/{did}/access is operational', async function() {
// Run a simple case on a Chimpyland doc
const oid = await dbManager.testGetId('Chimpyland');
const did = await dbManager.testGetId('Timesheets');
const resp1 = await axios.get(`${homeUrl}/api/docs/${did}/access`, chimpy);
assert.equal(resp1.status, 200);
assert.deepEqual(resp1.data, {
maxInheritedRole: "owners",
users: [{
id: 1,
name: 'Chimpy',
email: chimpyEmail,
ref: chimpyRef,
picture: null,
// Note that Chimpy explicitly has owners access to the doc from a previous test.
access: "owners",
parentAccess: "owners",
isMember: true,
}, {
id: 3,
name: 'Charon',
email: charonEmail,
ref: charonRef,
picture: null,
access: null,
parentAccess: "viewers",
isMember: true,
}]
});
// Add kiwi as a member of Chimpyland
const kiwiMemberDelta = {
users: {[kiwiEmail]: "members"}
};
const kiwiMemberResp = await axios.patch(`${homeUrl}/api/orgs/${oid}/access`,
{delta: kiwiMemberDelta}, chimpy);
assert.equal(kiwiMemberResp.status, 200);
// Run a complex case by modifying maxInheritedRole and individual roles on a doc then querying
// for access
// Set the maxInheritedRole to null
const delta = {
maxInheritedRole: null,
users: {[kiwiEmail]: "editors"}
};
const patchResp = await axios.patch(`${homeUrl}/api/docs/${did}/access`, {delta}, chimpy);
assert.equal(patchResp.status, 200);
const resp2 = await axios.get(`${homeUrl}/api/docs/${did}/access`, chimpy);
assert.equal(resp2.status, 200);
assert.deepEqual(resp2.data, {
maxInheritedRole: null,
users: [{
id: 1,
name: 'Chimpy',
email: chimpyEmail,
ref: chimpyRef,
picture: null,
access: "owners",
parentAccess: "owners",
isMember: true,
}, {
id: 2,
name: 'Kiwi',
email: kiwiEmail,
ref: kiwiRef,
picture: null,
access: "editors",
parentAccess: null,
isMember: true,
}, {
id: 3,
name: 'Charon',
email: charonEmail,
ref: charonRef,
picture: null,
access: null,
parentAccess: "viewers",
isMember: true,
}]
});
// Reset the access settings
const kiwiResetDelta = {
users: {[kiwiEmail]: null}
};
const kiwiResetResp = await axios.patch(`${homeUrl}/api/orgs/${oid}/access`,
{delta: kiwiResetDelta}, chimpy);
assert.equal(kiwiResetResp.status, 200);
// TODO: Unnecessary once removing from org removes from all.
const resetDelta = {
maxInheritedRole: "owners",
users: {
[kiwiEmail]: null
}
};
const resetResp = await axios.patch(`${homeUrl}/api/docs/${did}/access`, {delta: resetDelta}, chimpy);
assert.equal(resetResp.status, 200);
// Run another complex case by modifying maxInheritedRole and individual roles of the workspace
// the doc is in then querying for access.
const shark = await dbManager.testGetId('Shark');
const sharkWs = await dbManager.testGetId('Big');
const wsDelta = {
maxInheritedRole: "viewers"
};
const patchResp2 = await axios.patch(`${homeUrl}/api/workspaces/${sharkWs}/access`, {delta: wsDelta}, chimpy);
assert.equal(patchResp2.status, 200);
const resp3 = await axios.get(`${homeUrl}/api/docs/${shark}/access`, chimpy);
assert.equal(resp3.status, 200);
// Assert that the maxInheritedRole of the workspace limits inherited access from the org.
assert.deepEqual(resp3.data, {
maxInheritedRole: 'owners',
users: [{
id: 1,
name: 'Chimpy',
email: chimpyEmail,
ref: chimpyRef,
picture: null,
// Note that Chimpy's access to shark is inherited from the workspace, of which he is
// explicitly an owner.
access: null,
parentAccess: "owners",
isMember: true,
}, {
id: 2,
name: 'Kiwi',
email: kiwiEmail,
ref: kiwiRef,
picture: null,
access: null,
parentAccess: "viewers",
isMember: true,
}, {
id: 3,
name: 'Charon',
email: charonEmail,
ref: charonRef,
picture: null,
access: null,
parentAccess: "viewers",
isMember: true,
}]
});
// Reset the access settings
const resetDelta2 = {
maxInheritedRole: "owners"
};
const resetResp2 = await axios.patch(`${homeUrl}/api/workspaces/${sharkWs}/access`, {delta: resetDelta2}, chimpy);
assert.equal(resetResp2.status, 200);
});
it('GET /api/docs/{did}/access returns 404 appropriately', async function() {
const resp = await axios.get(`${homeUrl}/api/docs/9999/access`, chimpy);
assert.equal(resp.status, 404);
});
it('GET /api/docs/{did}/access returns 403 appropriately', async function() {
const did = await dbManager.testGetId('Timesheets');
const resp = await axios.get(`${homeUrl}/api/docs/${did}/access`, kiwi);
assert.equal(resp.status, 403);
});
it('should show special users if they are added', async function() {
// TODO We may want to expose special flags in requests and responses rather than allow adding
// and retrieving special email addresses. For now, just make sure that if we succeed adding a
// a special user, that we can also retrieve it.
const wid = await dbManager.testGetId('Private');
const did = await dbManager.testGetId('Timesheets'); // This is inside workspace `wid`
// Turns users from PermissionData into a mapping from email address to [access, parentAccess],
// for more concise comparisons below.
function compactAccess(data: PermissionData): {[email: string]: [Role|null, Role|null]} {
return fromPairs(data.users.map((u) => [u.email, [u.access, u.parentAccess || null]]));
}
let resp = await axios.patch(`${homeUrl}/api/workspaces/${wid}/access`,
{delta: {users: {[everyoneEmail]: 'viewers'}}}, chimpy);
assert.equal(resp.status, 200);
// The special user should be visible when we get the access list.
resp = await axios.get(`${homeUrl}/api/workspaces/${wid}/access`, chimpy);
assert.deepEqual(compactAccess(resp.data), {
[chimpyEmail]: ['owners', 'owners'],
[charonEmail]: [null, 'viewers'],
[everyoneEmail]: ['viewers', null],
});
// The special user should be visible on the doc too, since it's inherited.
resp = await axios.get(`${homeUrl}/api/docs/${did}/access`, chimpy);
assert.deepEqual(compactAccess(resp.data), {
[chimpyEmail]: ['owners', 'owners'],
[charonEmail]: [null, 'viewers'],
[everyoneEmail]: [null, 'viewers'],
});
// Remove the special user; it should no longer be visible on either.
resp = await axios.patch(`${homeUrl}/api/workspaces/${wid}/access`,
{delta: {users: {[everyoneEmail]: null}}}, chimpy);
resp = await axios.get(`${homeUrl}/api/workspaces/${wid}/access`, chimpy);
assert.deepEqual(compactAccess(resp.data), {
[chimpyEmail]: ['owners', 'owners'],
[charonEmail]: [null, 'viewers'],
});
resp = await axios.get(`${homeUrl}/api/docs/${did}/access`, chimpy);
assert.deepEqual(compactAccess(resp.data), {
[chimpyEmail]: ['owners', 'owners'],
[charonEmail]: [null, 'viewers'],
});
// Add special user to the doc.
resp = await axios.patch(`${homeUrl}/api/docs/${did}/access`,
{delta: {users: {[everyoneEmail]: 'editors'}}}, chimpy);
assert.equal(resp.status, 200);
resp = await axios.get(`${homeUrl}/api/docs/${did}/access`, chimpy);
assert.deepEqual(compactAccess(resp.data), {
[chimpyEmail]: ['owners', 'owners'],
[charonEmail]: [null, 'viewers'],
[everyoneEmail]: ['editors', null],
});
// But it should not be visible on the workspace.
resp = await axios.get(`${homeUrl}/api/workspaces/${wid}/access`, chimpy);
assert.deepEqual(compactAccess(resp.data), {
[chimpyEmail]: ['owners', 'owners'],
[charonEmail]: [null, 'viewers'],
});
// Remove the special user.
resp = await axios.patch(`${homeUrl}/api/docs/${did}/access`,
{delta: {users: {[everyoneEmail]: null}}}, chimpy);
resp = await axios.get(`${homeUrl}/api/docs/${did}/access`, chimpy);
assert.deepEqual(compactAccess(resp.data), {
[chimpyEmail]: ['owners', 'owners'],
[charonEmail]: [null, 'viewers'],
});
});
it('should allow setting member role', async function() {
const oid = await dbManager.testGetId('Chimpyland');
const wid = await dbManager.testGetId('Private');
const did = await dbManager.testGetId('Timesheets');
const addDelta = {
users: { [kiwiEmail]: "members" }
};
const removeDelta = {
users: { [kiwiEmail]: null }
};
// Set Kiwi as a member of org 'Chimpyland'.
const addKiwiToOrg = await axios.patch(`${homeUrl}/api/orgs/${oid}/access`,
{delta: addDelta}, chimpy);
assert.equal(addKiwiToOrg.status, 200);
// Fetch workspace permissions and check that Kiwi has and inherits no access.
const kiwiWsAccess = await axios.get(`${homeUrl}/api/workspaces/${wid}/access`, chimpy);
assert.equal(kiwiWsAccess.status, 200);
assert.deepEqual(kiwiWsAccess.data, {
maxInheritedRole: 'owners',
users: [{
id: 1,
name: 'Chimpy',
email: chimpyEmail,
ref: chimpyRef,
picture: null,
// Note that Chimpy already has ownership access to the workspace.
access: "owners",
parentAccess: "owners",
isMember: true,
}, {
id: 2,
name: 'Kiwi',
email: kiwiEmail,
ref: kiwiRef,
picture: null,
access: null,
parentAccess: null,
isMember: true,
}, {
id: 3,
name: 'Charon',
email: charonEmail,
ref: charonRef,
picture: null,
access: null,
parentAccess: "viewers",
isMember: true,
}]
});
// Fetch org permissions and check that Kiwi is a member.
const kiwiOrgAccess = await axios.get(`${homeUrl}/api/orgs/${oid}/access`, chimpy);
assert.equal(kiwiOrgAccess.status, 200);
assert.deepEqual(kiwiOrgAccess.data, {
users: [{
id: 1,
name: 'Chimpy',
email: chimpyEmail,
ref: chimpyRef,
picture: null,
access: "owners",
isMember: true,
}, {
id: 2,
name: 'Kiwi',
email: kiwiEmail,
ref: kiwiRef,
picture: null,
access: "members",
isMember: true,
}, {
id: 3,
name: 'Charon',
email: charonEmail,
ref: charonRef,
picture: null,
access: "viewers",
isMember: true,
}]
});
// Unset Kiwi as a member of org 'Chimpyland'.
const removeKiwiFromOrg = await axios.patch(`${homeUrl}/api/orgs/${oid}/access`,
{delta: removeDelta}, chimpy);
assert.equal(removeKiwiFromOrg.status, 200);
// Assert that updating a workspace user to "members" throws with status 400.
const invalidResp1 = await axios.patch(`${homeUrl}/api/workspaces/${wid}/access`,
{delta: addDelta}, chimpy);
assert.equal(invalidResp1.status, 400);
// Assert that updating a doc user to "members" throws with status 400.
const invalidResp2 = await axios.patch(`${homeUrl}/api/docs/${did}/access`,
{delta: addDelta}, chimpy);
assert.equal(invalidResp2.status, 400);
// Assert that updating the maxInheritedRole to "members" throws with status 400.
const invalidDelta = { maxInheritedRole: "members" };
const invalidResp3 = await axios.patch(`${homeUrl}/api/workspaces/${wid}/access`,
{delta: invalidDelta}, chimpy);
assert.equal(invalidResp3.status, 400);
});
describe('team plan', function() {
let nasaOrg: Organization;
let oldProduct: Product;
before(async function() {
// Set NASA to be specifically on a team plan, with team plan restrictions.
const db = dbManager.connection.manager;
nasaOrg = (await db.findOne(Organization, {where: {domain: 'nasa'},
relations: ['billingAccount',
'billingAccount.product']}))!;
oldProduct = nasaOrg.billingAccount.product;
nasaOrg.billingAccount.product = (await db.findOne(Product, {where: {name: 'team'}}))!;
await nasaOrg.billingAccount.save();
});
after(async function() {
nasaOrg.billingAccount.product = oldProduct;
await nasaOrg.billingAccount.save();
});
it('should prevent adding non-org-members to workspaces', async function() {
// Add Kiwi to Horizon
const horizonWs = await dbManager.testGetId('Horizon');
const addDelta = {
users: {[kiwiEmail]: 'viewers'}
};
const errorResp = await axios.patch(`${homeUrl}/api/workspaces/${horizonWs}/access`,
{delta: addDelta}, chimpy);
assert.equal(errorResp.status, 403);
assert.equal(errorResp.data.error, 'No external workspace shares permitted');
});
it('should prevent adding more than n non-org-members to docs', async function() {
// Add Kiwi to Apathy
const apathyDoc = await dbManager.testGetId('Apathy');
let resp = await axios.patch(`${homeUrl}/api/docs/${apathyDoc}/access`,
{delta: {users: {[kiwiEmail]: 'viewers'}}}, chimpy);
assert.equal(resp.status, 200);
// Add Support to Apathy, should not count
resp = await axios.patch(`${homeUrl}/api/docs/${apathyDoc}/access`,
{delta: {users: {[supportEmail]: 'viewers'}}}, chimpy);
assert.equal(resp.status, 200);
// Add Ella to Apathy
resp = await axios.patch(`${homeUrl}/api/docs/${apathyDoc}/access`,
{delta: {users: {'ella@getgrist.com': 'editors'}}}, chimpy);
assert.equal(resp.status, 200);
// Add Charon to Apathy
resp = await axios.patch(`${homeUrl}/api/docs/${apathyDoc}/access`,
{delta: {users: {[charonEmail]: 'viewers'}}}, chimpy);
assert.equal(resp.status, 403);
assert.equal(resp.data.error, 'No more external document shares permitted');
// Remove added users
const removeDelta = {
users: {
[kiwiEmail]: null,
[supportEmail]: null,
}
};
resp = await axios.patch(`${homeUrl}/api/docs/${apathyDoc}/access`,
{delta: removeDelta}, chimpy);
assert.equal(resp.status, 200);
});
});
it('should emit userChange events when expected', async function() {
// Change org permissions ==>
const fishOrgId = await dbManager.testGetId('Fish');
// Remove charon and kiwi from org
const removeCharonKiwi = {
users: { [charonEmail]: null, [kiwiEmail]: null }
};
const fishResp1 = await axios.patch(`${homeUrl}/api/orgs/${fishOrgId}/access`,
{delta: removeCharonKiwi}, chimpy);
assert.equal(fishResp1.status, 200);
assert.deepEqual(userCountUpdates[fishOrgId as number], [1]);
// Re-add charon
const addCharon = {
users: { [charonEmail]: 'viewers' }
};
const fishResp2 = await axios.patch(`${homeUrl}/api/orgs/${fishOrgId}/access`,
{delta: addCharon}, chimpy);
assert.equal(fishResp2.status, 200);
assert.deepEqual(userCountUpdates[fishOrgId as number], [1, 2]);
// Re-add kiwi
const addKiwi = {
users: { [kiwiEmail]: 'editors' }
};
const fishResp3 = await axios.patch(`${homeUrl}/api/orgs/${fishOrgId}/access`,
{delta: addKiwi}, chimpy);
assert.equal(fishResp3.status, 200);
assert.deepEqual(userCountUpdates[fishOrgId as number], [1, 2, 3]);
// Change workspace permissions ==>
const clOrgId = await dbManager.testGetId('Chimpyland');
const publicWsId = await dbManager.testGetId('Public');
// Add charon to ws
const publicResp1 = await axios.patch(`${homeUrl}/api/workspaces/${publicWsId}/access`,
{delta: addCharon}, chimpy);
assert.equal(publicResp1.status, 200);
// Remove charon
const removeCharon = {
users: {[charonEmail]: null}
};
const publicResp2 = await axios.patch(`${homeUrl}/api/workspaces/${publicWsId}/access`,
{delta: removeCharon}, chimpy);
assert.equal(publicResp2.status, 200);
// Assert that workspace user changes have no effect on userCount.
assert.deepEqual(userCountUpdates[clOrgId as number], undefined);
});
it('GET /api/profile/apikey gives user\'s api key', async function() {
const resp = await axios.get(`${homeUrl}/api/profile/apikey`, chimpy);
assert.equal(resp.status, 200);
assert.equal(resp.data, 'api_key_for_chimpy');
});
it('POST /api/profile/apiKey fails for anonymous', async function() {
const resp = await axios.post(`${homeUrl}/api/profile/apikey`, null, nobody);
assert.equal(resp.status, 401);
assert.deepEqual(resp.data, {error: "user not authorized"});
});
it('DELETE /api/profile/apiKey fails for anonymous', async function() {
const resp = await axios.delete(`${homeUrl}/api/profile/apikey`, nobody);
assert.equal(resp.status, 401);
assert.deepEqual(resp.data, {error: "user not authorized"});
});
it('DELETE /api/profile/apikey delete api key', async function() {
let resp: AxiosResponse;
resp = await axios.delete(`${homeUrl}/api/profile/apikey`, chimpy);
assert.equal(resp.status, 200);
// check that chimpy's apikey does not work any more
resp = await axios.get(`${homeUrl}/api/orgs`, chimpy);
assert.equal(resp.status, 401);
assert.deepEqual(resp.data, "Bad request: invalid API key");
// check that the apikey '' does not work either
resp = await axios.get(`${homeUrl}/api/orgs`, {
responseType: 'json',
validateStatus: () => true,
headers: {Authorization: "Bearer "}
});
assert.equal(resp.status, 401);
assert.deepEqual(resp.data, "Bad request: invalid API key");
// check that db encoded null for the apikey
const chimpyUser = (await User.findOne({where: {name: 'Chimpy'}}))!;
assert.deepEqual(chimpyUser.apiKey, null);
// restore api key for chimpy
chimpyUser.apiKey = 'api_key_for_chimpy';
await chimpyUser.save();
});
describe('POST /api/profile/apikey', function() {
let resp: AxiosResponse;
it ('fails if apiKey already set', async function() {
resp = await axios.post(`${homeUrl}/api/profile/apikey`, null, kiwi);
assert.equal(resp.status, 400);
assert.match(resp.data.error, /apikey is already set/);
});
it('succeed if apiKey already set but force flag is used', async function() {
resp = await axios.post(`${homeUrl}/api/profile/apikey`, {force: true}, kiwi);
assert.equal(resp.status, 200);
const apiKey = resp.data;
// check that old apikey does not work any more
resp = await axios.get(`${homeUrl}/api/orgs`, kiwi);
assert.equal(resp.status, 401);
assert.deepEqual(resp.data, "Bad request: invalid API key");
// check that the new api key works
kiwi.headers = {Authorization: 'Bearer ' + apiKey};
resp = await axios.get(`${homeUrl}/api/orgs`, kiwi);
assert.equal(resp.status, 200);
assert.deepEqual(resp.data.map((org: any) => org.name),
['Kiwiland', 'Fish', 'Flightless', 'Primately']);
});
describe('force flag is not needed if apiKey is not set', function() {
before(function() {
// turn off api key access for chimpy
return dbManager.connection.query(`update users set api_key = null where name = 'Chimpy'`);
});
after(function() {
// bring back api key access for chimpy
return dbManager.connection.query(`update users set api_key = 'api_key_for_chimpy' where name = 'Chimpy'`);
});
it('force flag is not needed', async function() {
// make sure api key access is off
resp = await axios.get(`${homeUrl}/api/orgs`, chimpy);
assert.equal(resp.status, 401);
const cookie = await server.getCookieLogin('nasa', {email: 'chimpy@getgrist.com',
name: 'Chimpy'});
// let's create an apikey
resp = await axios.post(`${homeUrl}/o/nasa/api/profile/apikey`, {}, cookie);
// check call was successful
assert.equal(resp.status, 200);
// check that new api key works
chimpy.headers = {Authorization: 'Bearer ' + resp.data};
resp = await axios.get(`${homeUrl}/api/orgs`, chimpy);
assert.equal(resp.status, 200);
assert.deepEqual(resp.data.map((org: any) => org.name),
['Chimpyland', 'EmptyOrg', 'EmptyWsOrg', 'Fish', 'Flightless',
'FreeTeam', 'NASA', 'Primately', 'TestDailyApiLimit']);
});
});
describe('generates a unique key', function() {
let apiKeyGenerator: sinon.SinonStub;
let apiKeyGeneratorReturns: string[];
before(function() {
apiKeyGenerator = sinon.stub(Deps, 'apiKeyGenerator');
apiKeyGenerator.callsFake(() => apiKeyGeneratorReturns.shift()!);
});
after(function() {
apiKeyGenerator.restore();
});
it('retries until the generated key is unique', async function() {
apiKeyGeneratorReturns = ['api_key_for_charon', 'santa1'];
resp = await axios.post(`${homeUrl}/api/profile/apikey`, {force: true}, kiwi);
assert.equal(resp.status, 200);
assert.equal(resp.data, 'santa1');
assert.equal(apiKeyGenerator.callCount, 2);
apiKeyGenerator.resetHistory();
kiwi.headers = {Authorization: 'Bearer ' + resp.data};
apiKeyGeneratorReturns = ['api_key_for_charon', 'api_key_for_charon', 'santa2'];
resp = await axios.post(`${homeUrl}/api/profile/apikey`, {force: true}, kiwi);
assert.equal(resp.status, 200);
assert.equal(resp.data, 'santa2');
assert.equal(apiKeyGenerator.callCount, 3);
apiKeyGenerator.resetHistory();
kiwi.headers = {Authorization: 'Bearer ' + resp.data};
// after 5 attempts throws
apiKeyGeneratorReturns = ['api_key_for_charon', 'api_key_for_charon', 'api_key_for_charon',
'api_key_for_charon', 'api_key_for_charon', 'santa3'];
resp = await axios.post(`${homeUrl}/api/profile/apikey`, {force: true}, kiwi);
assert.equal(resp.status, 500);
assert.deepEqual(resp.data, {error: 'Could not generate a valid api key.'});
});
});
});
});
async function testAllowNonOwnersToRemoveThemselves(url: string) {
// Add a viewer and an editor.
let resp = await axios.patch(url, {
delta: {
users: {
[charonEmail]: 'editors',
[kiwiEmail]: 'viewers',
}
}
}, chimpy);
assert.equal(resp.status, 200);
// One cannot remove the other.
resp = await axios.patch(url, {
delta: {
users: {
[kiwiEmail]: null,
}
}
}, charon);
assert.equal(resp.status, 403);
// But they can remove themselves.
resp = await axios.patch(url, {
delta: {
users: {
[charonEmail]: null,
}
}
}, charon);
assert.equal(resp.status, 200);
resp = await axios.patch(url, {
delta: {
users: {
[kiwiEmail]: null,
}
}
}, kiwi);
assert.equal(resp.status, 200);
}