mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
1690 lines
63 KiB
TypeScript
1690 lines
63 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\/$/);
|
||
|
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\/$/);
|
||
|
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\/$/);
|
||
|
}
|
||
|
// 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]+\/$/);
|
||
|
const env = mail.payload.personalizations[0].dynamic_template_data;
|
||
|
assert.match(env.resource.url, /^http.*\/o\/docs\/ws\/[0-9]+\/$/);
|
||
|
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,
|
||
|
}]
|
||
|
});
|
||
|
// 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);
|
||
|
|
||
|
// 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);
|
||
|
}
|