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); }