(core) move access rule UI tests to core

Summary:
Moving some tests in preparation for some new development
work.

Test Plan: moving tests

Reviewers: jordigh

Reviewed By: jordigh

Differential Revision: https://phab.getgrist.com/D4374
This commit is contained in:
Paul Fitzpatrick 2024-10-09 10:59:09 -04:00
parent 5d349e603b
commit d1803dddb7
5 changed files with 1978 additions and 0 deletions

View File

@ -0,0 +1,505 @@
/**
* Test of the UI for Granular Access Control, part 1.
*/
import { assert, driver, Key, stackWrapFunc } from 'mocha-webdriver';
import { enterRulePart, findDefaultRuleSet, findRuleSet,
findTable, triggerAutoComplete } from 'test/nbrowser/aclTestUtils';
import * as gu from 'test/nbrowser/gristUtils';
import { server } from 'test/nbrowser/testServer';
import { setupTestSuite } from 'test/nbrowser/testUtils';
describe('AccessRules1', function() {
this.timeout(20000);
const cleanup = setupTestSuite();
let docId: string;
before(async function() {
// Import a test document we've set up for this.
const mainSession = await gu.session().teamSite.user('user1').login();
docId = (await mainSession.tempDoc(cleanup, 'ACL-Test.grist', {load: false})).id;
// Share it with a few users.
const api = mainSession.createHomeApi();
await api.updateDocPermissions(docId, { users: {
[gu.translateUser("user2").email]: 'owners',
[gu.translateUser("user3").email]: 'editors',
} });
return docId;
});
afterEach(() => gu.checkForErrors());
const getTableNamesToAddWidget = stackWrapFunc(async function(): Promise<string[]> {
await gu.openAddWidgetToPage();
const options = await driver.findAll('.test-wselect-table', e => e.getText());
// Close add widget popup
await driver.sendKeys(Key.ESCAPE);
assert.equal(options[0], "New Table");
return options.slice(1);
});
const checkFullView = stackWrapFunc(async function(user: gu.TestUser) {
const session = await gu.session().teamSite.user(user).login();
await session.loadDoc(`/doc/${docId}`);
// Check that we can see and add widgets for FinancialsTable.
assert.deepEqual(await gu.getPageNames(), ['ClientsTable', 'FinancialsTable']);
assert.deepEqual(await getTableNamesToAddWidget(), ['ClientsTable', 'FinancialsTable']);
await gu.openPage('FinancialsTable');
await gu.waitForServer();
assert.deepEqual(await gu.getVisibleGridCells({cols: ['Year', 'Income', 'Expenses'], rowNums: [1, 2, 3]}), [
'2010', '$123.40', '$540,000.00',
'2011', '$1,234.50', '$640,000.00',
'2022', '$1,234,567.00', '$0.55',
]);
// Check that we can see RumorsColumn of ClientsTable.
await gu.openPage('ClientsTable');
await gu.waitForServer();
assert.equal(await driver.findContent('.column_name', /RumorsColumn/).isPresent(), true);
assert.deepEqual(await gu.getVisibleGridCells({cols: ['RumorsColumn'], rowNums: [1, 3, 9, 13]}), [
'Secrets', '', 'Dark rumors', 'Buzz'
]);
// Check that we can all rows of ClientsTable
assert.deepEqual(await gu.getVisibleGridCells({cols: ['First Name', 'Shared'], rowNums: [1, 3, 9, 13]}), [
'Deina', 'false',
'Terrence', 'true',
'Rachele', 'true',
'Erny', 'false',
]);
});
const checkLimitedView = stackWrapFunc(async function(user: gu.TestUser) {
const session = await gu.session().teamSite.user(user).login();
await session.loadDoc(`/doc/${docId}`);
assert.deepEqual(await gu.getPageNames(), ['ClientsTable']);
assert.deepEqual(await getTableNamesToAddWidget(), ['ClientsTable']);
await gu.openPage('ClientsTable');
await gu.waitForServer();
assert.equal(await driver.findContent('.column_name', /RumorsColumn/).isPresent(), false);
});
const checkLimitedUpdateMemo = stackWrapFunc(async function(user: gu.TestUser, memo: string = '') {
const session = await gu.session().teamSite.user(user).login();
await session.loadDoc(`/doc/${docId}`);
await gu.openPage('ClientsTable');
await gu.waitForServer();
await gu.getCell(0, 1).click();
await gu.sendKeys('Will it work?', Key.ENTER);
await gu.waitForServer();
if (memo === '') {
assert.isFalse(await driver.find('.test-notifier-toast-memo').isPresent());
} else {
await driver.findContent('.test-notifier-toast-memo', memo);
}
});
it('should support rules for tables and columns', async function() {
this.timeout(40000);
// Check that users 1 and 3 can see everything initially.
await checkFullView('user1');
await checkFullView('user3');
// Load Access Rules UI.
const mainSession = await gu.session().teamSite.user('user1').login();
await mainSession.loadDoc(`/doc/${docId}`);
await driver.find('.test-tools-access-rules').click();
await driver.findWait('.test-rule-set', 2000); // Wait for initialization fetch to complete.
// Make FinancialsTable private to user1.
await driver.findContentWait('button', /Add Table Rules/, 2000).click();
await driver.findContentWait('.grist-floating-menu li', /FinancialsTable/, 3000).click();
let ruleSet = findDefaultRuleSet(/FinancialsTable/);
await enterRulePart(ruleSet, 1, null, 'Deny All');
await ruleSet.find('.test-rule-part .test-rule-add').click();
await enterRulePart(ruleSet, 1, `user.Email == '${gu.translateUser('user1').email}'`, 'Allow All');
// Make RumorsColumn of ClientsTable private to user1.
await driver.findContentWait('button', /Add Table Rules/, 2000).click();
await driver.findContent('.grist-floating-menu li', /ClientsTable/).click();
await findTable(/ClientsTable/).find('.test-rule-table-menu-btn').click();
await driver.findContent('.grist-floating-menu li', /Add Column Rule/).click();
ruleSet = findRuleSet(/ClientsTable/, 1);
await ruleSet.find('.test-rule-resource .test-select-open').click();
assert.deepEqual(
await driver.findAll('.test-select-menu li', el => el.getText()),
[
'Agent_Email',
'Email',
'First_Name',
'Last_Name',
'Phone',
'RumorsColumn',
'Shared',
]
);
await driver.findContent('.test-select-menu li', 'RumorsColumn').click();
await enterRulePart(ruleSet, 1, null, 'Deny All');
await ruleSet.find('.test-rule-part .test-rule-add').click();
await enterRulePart(ruleSet, 1, `user.Email == '${gu.translateUser('user1').email}'`, 'Allow All');
// Make rows of ClientsTable only visible to the team if assigned to them, or marked as shared.
ruleSet = findDefaultRuleSet(/ClientsTable/);
await ruleSet.find('.test-rule-part .test-rule-add').click();
await ruleSet.find('.test-rule-part .test-rule-add').click();
await ruleSet.find('.test-rule-part .test-rule-add').click();
await enterRulePart(ruleSet, 1, `user.Email == '${gu.translateUser('user1').email}'`, 'Allow All');
await enterRulePart(ruleSet, 2, `rec.Shared`, {R: 'allow'});
await enterRulePart(ruleSet, 3, `user.Email == rec.Agent_Email`, {R: 'allow'});
await enterRulePart(ruleSet, 4, null, 'Deny All');
await driver.find('.test-rules-save').click();
await gu.waitForServer();
// Check that user1 can see FinancialsTable, RumorsColumn, and all rows of ClientsTable.
await checkFullView('user1');
// Check that user2 cannot see FinancialsTable or RumorsColumn.
await checkLimitedView('user2');
// Check that user2 only sees certain rows of ClientsTable.
assert.deepEqual(await gu.getVisibleGridCells(
{cols: ['First Name', 'Agent Email', 'Shared'], rowNums: [1, 3, 6, 7, 8, 9, 10]}
), [
// Everyone assigned to charon is visible.
'Deina', 'gristoid+charon@gmail.com', 'false',
'Terrence', 'gristoid+charon@gmail.com', 'true',
'Wyndham', 'gristoid+charon@gmail.com', 'false',
// And everyone with Shared = true is visible.
'Rachele', 'gristoid+chimpy@gmail.com', 'true',
'Elianore', 'gristoid+kiwi@gmail.com', 'true',
'Ellsworth', 'gristoid+kiwi@gmail.com', 'true',
'', '', ''
]);
// Check that user3 cannot see FinancialsTable or RumorsColumn.
await checkLimitedView('user3');
// Check that user3 only sees certain rows of ClientsTable.
assert.deepEqual(await gu.getVisibleGridCells(
{cols: ['First Name', 'Agent Email', 'Shared'], rowNums: [1, 2, 3, 4, 9, 11, 12]}
), [
// And everyone with Shared = true is visible.
'Siobhan', 'gristoid+charon@gmail.com', 'true',
'Terrence', 'gristoid+charon@gmail.com', 'true',
'Rachele', 'gristoid+chimpy@gmail.com', 'true',
// Everyone assigned to kiwi is visible.
'Kettie', 'gristoid+kiwi@gmail.com', 'false',
'Neely', 'gristoid+kiwi@gmail.com', 'false',
'Ellsworth', 'gristoid+kiwi@gmail.com', 'true',
'', '', '',
]);
});
it('should support rules with rec.id in formula', async function() {
// This tests behavior that broke after a bug was introduced.
const mainSession = await gu.session().teamSite.user('user1').login();
await mainSession.loadDoc(`/doc/${docId}`);
await driver.find('.test-tools-access-rules').click();
await driver.findWait('.test-rule-set', 2000);
const ruleSet = findDefaultRuleSet(/FinancialsTable/);
await enterRulePart(ruleSet, 1, `rec.id == 1`, 'Allow All');
await driver.find('.test-rules-save').click();
await gu.waitForServer();
await gu.openPage('FinancialsTable');
await gu.waitForServer();
assert.deepEqual(
await gu.getVisibleGridCells({cols: ['Expenses', 'Income', 'Year'], rowNums: [1, 2]}),
[
'$540,000.00', '$123.40', '2010',
'', '', ''
]
);
await gu.undo();
});
it('should support adding memos', async function() {
const mainSession = await gu.session().teamSite.user('user1').login();
await mainSession.loadDoc(`/doc/${docId}`);
await driver.find('.test-tools-access-rules').click();
await driver.findWait('.test-rule-set', 2000); // Wait for initialization fetch to complete.
// Change the catch-all rule on ClientsTable to read-only permission, and give it a memo.
const ruleSet = findDefaultRuleSet(/ClientsTable/);
await enterRulePart(ruleSet, 4, null, 'Read Only', 'Sorry, this table is read-only.');
await driver.find('.test-rules-save').click();
await gu.waitForServer();
// Check that user2 sees the memo when trying to update ClientsTable.
await checkLimitedUpdateMemo('user2', 'Sorry, this table is read-only');
});
it('should support removing memos', async function() {
const mainSession = await gu.session().teamSite.user('user1').login();
await mainSession.loadDoc(`/doc/${docId}`);
await driver.find('.test-tools-access-rules').click();
await driver.findWait('.test-rule-set', 2000); // Wait for initialization fetch to complete.
// Delete the memo for the ClientsTable catch-all rule.
await gu.session().teamSite.user('user1').login();
await driver.find('.test-tools-access-rules').click();
await driver.findWait('.test-rule-set', 2000);
const ruleSet = findDefaultRuleSet(/ClientsTable/);
await ruleSet.find('.test-rule-part-and-memo:nth-child(4) .test-rule-memo-remove').click();
await driver.find('.test-rules-save').click();
await gu.waitForServer();
// Check that user2 no longer sees the memo.
await checkLimitedUpdateMemo('user2');
});
it('should not produce unnecessary user actions when changing rules', async function() {
const mainSession = await gu.session().teamSite.user('user1').login();
await mainSession.loadDoc(`/doc/${docId}`);
await driver.find('.test-tools-access-rules').click();
await driver.findWait('.test-rule-set', 2000); // Wait for initialization fetch to complete.
// Revert a rule that was modified in an earlier test.
let ruleSet = findDefaultRuleSet(/ClientsTable/);
await enterRulePart(ruleSet, 4, null, 'Deny All');
await driver.find('.test-rules-save').click();
await gu.waitForServer();
assert.equal(await driver.find('.test-rules-save').isDisplayed(), false);
// Add/remove a rule; unchanged AccessRules should still show as Saved.
ruleSet = findDefaultRuleSet(/ClientsTable/);
await ruleSet.find('.test-rule-part:nth-child(1) .test-rule-add').click();
await ruleSet.find('.test-rule-part:nth-child(1) .test-rule-remove').click();
assert.equal(await driver.find('.test-rules-save').isDisplayed(), false);
// Add a rule part, remove another rule part, and save.
ruleSet = findDefaultRuleSet(/ClientsTable/);
await ruleSet.find('.test-rule-part:nth-child(1) .test-rule-add').click();
// Add a rule giving user2 (charon) access.
await enterRulePart(ruleSet, 1, `user.Email == '${gu.translateUser('user2').email}'`, {R: 'allow'});
// Remove the rule giving everyone access to Shared rows.
await ruleSet.find('.test-rule-part-and-memo:nth-child(3) .test-rule-remove').click();
// Check that the Save button is enabled, and save.
assert.equal(await driver.find('.test-rules-save').isDisplayed(), true);
await gu.userActionsCollect();
await driver.find('.test-rules-save').click();
await gu.waitForServer();
await gu.userActionsVerify([
["BulkRemoveRecord", "_grist_ACLRules", [6]],
["BulkAddRecord", "_grist_ACLRules", [-1], {
aclFormula: ["user.Email == 'gristoid+charon@gmail.com'"],
permissionsText: ["+R"],
resource: [5],
rulePos: [4.5],
memo: [""],
}],
]);
assert.equal(await driver.find('.test-rules-save').isDisplayed(), false);
// Check that the rule has an effect: user3 has limited access, and cannot see Shared rows except their own.
await checkLimitedView('user3');
assert.deepEqual(await gu.getVisibleGridCells(
{cols: ['First Name', 'Agent Email', 'Shared'], rowNums: [1, 6, 8, 9]}
), [
'Kettie', 'gristoid+kiwi@gmail.com', 'false',
'Neely', 'gristoid+kiwi@gmail.com', 'false',
'Ellsworth', 'gristoid+kiwi@gmail.com', 'true',
'', '', '',
]);
});
it('should report errors when typed-in rule is invalid', async function() {
const mainSession = await gu.session().teamSite.user('user1').login();
await mainSession.loadDoc(`/doc/${docId}`);
await driver.find('.test-tools-access-rules').click();
await driver.findWait('.test-rule-set', 2000);
const ruleSet = findDefaultRuleSet(/ClientsTable/);
await ruleSet.find('.test-rule-part:nth-child(1) .test-rule-add').click();
await enterRulePart(ruleSet, 1, `user.Access === 'owners'`, {R: 'allow'});
await gu.waitForServer();
// Check that an error is shown, and save button is disabled.
assert.match(await ruleSet.find('.test-rule-part:nth-child(1) .test-rule-error').getText(),
/^SyntaxError/);
assert.equal(await driver.find('.test-rules-save').isDisplayed(), false);
assert.equal(await driver.find('.test-rules-non-save').isDisplayed(), true);
assert.equal(await driver.find('.test-rules-non-save').getText(), 'Invalid');
// Check that invalid names are also detected.
await enterRulePart(ruleSet, 1, `fuser.Access == 'owners'`, {R: 'allow'});
await gu.waitForServer();
assert.match(await ruleSet.find('.test-rule-part:nth-child(1) .test-rule-error').getText(),
/Unknown variable 'fuser'/);
assert.equal(await driver.find('.test-rules-save').isDisplayed(), false);
assert.equal(await driver.find('.test-rules-non-save').isDisplayed(), true);
assert.equal(await driver.find('.test-rules-non-save').getText(), 'Invalid');
// Check that invalid colIds are detected.
await enterRulePart(ruleSet, 1, `rec.Shared == True or rec.Sharedd == True`, {R: 'allow'});
await gu.waitForServer();
assert.match(await ruleSet.find('.test-rule-part:nth-child(1) .test-rule-error').getText(),
/Invalid columns: Sharedd/);
assert.equal(await driver.find('.test-rules-save').isDisplayed(), false);
assert.equal(await driver.find('.test-rules-non-save').isDisplayed(), true);
assert.equal(await driver.find('.test-rules-non-save').getText(), 'Invalid');
// Check that saving is also disabled while checking rules.
await server.pauseUntil(async () => {
await enterRulePart(ruleSet, 1, `user.Access == 'owners'`, {R: 'allow'});
assert.equal(await ruleSet.find('.test-rule-part:nth-child(1) .test-rule-error').isPresent(), false);
assert.equal(await driver.find('.test-rules-save').isDisplayed(), false);
assert.equal(await driver.find('.test-rules-non-save').isDisplayed(), true);
assert.equal(await driver.find('.test-rules-non-save').getText(), 'Checking…');
});
// Once rules are checked to be valid, the Save button is shown.
await gu.waitForServer();
assert.equal(await ruleSet.find('.test-rule-part:nth-child(1) .test-rule-error').isPresent(), false);
assert.equal(await driver.find('.test-rules-save').isDisplayed(), true);
assert.equal(await driver.find('.test-rules-non-save').isDisplayed(), false);
// Revert the changes in this test case.
await driver.find('.test-rules-revert').click();
await gu.waitForServer();
});
it('should allow read control for columns with rec in formula', async function() {
const mainSession = await gu.session().teamSite.user('user1').login();
await mainSession.loadDoc(`/doc/${docId}`);
await driver.find('.test-tools-access-rules').click();
await driver.findWait('.test-rule-set', 2000);
// Add a column rule that uses rec but doesn't set read permission.
await findTable(/FinancialsTable/).find('.test-rule-table-menu-btn').click();
await driver.findContent('.grist-floating-menu li', /Add Column Rule/).click();
let ruleSet = findRuleSet(/FinancialsTable/, 1);
await ruleSet.find('.test-rule-resource .test-select-open').click();
assert.deepEqual(
await driver.findAll('.test-select-menu li', el => el.getText()),
['Expenses', 'Income', 'Year']
);
await driver.findContent('.test-select-menu li', 'Year').click();
await enterRulePart(ruleSet, 1, 'rec.Year == "yore"', {U: 'deny'});
await gu.waitForServer();
await driver.find('.test-rules-save').click();
await gu.waitForServer();
// Attempting to set read bit should no longer result in a notification.
await driver.findWait('.test-rule-set', 2000);
ruleSet = findRuleSet(/FinancialsTable/, 1);
await assert.isFulfilled(enterRulePart(ruleSet, 1, null, {R: 'deny'}));
await gu.checkForErrors();
// Remove rule.
await ruleSet.find('.test-rule-part:nth-child(1) .test-rule-remove').click();
await driver.find('.test-rules-save').click();
await gu.waitForServer();
});
it('should report possible order-dependencies', async function() {
// Make a rule for FinancialsTable.Year and FinancialsTable.Income that denies something.
const mainSession = await gu.session().teamSite.user('user1').login();
await mainSession.loadDoc(`/doc/${docId}/p/acl`, {wait: false});
await driver.findWait('.test-rule-set', 2000);
await findTable(/FinancialsTable/).find('.test-rule-table-menu-btn').click();
await driver.findContent('.grist-floating-menu li', /Add Column Rule/).click();
let ruleSet = findRuleSet(/FinancialsTable/, 1);
await ruleSet.find('.test-rule-resource .test-select-open').click();
assert.deepEqual(
await driver.findAll('.test-select-menu li', el => el.getText()),
['Expenses', 'Income', 'Year']
);
await driver.findContent('.test-select-menu li', 'Year').click();
await ruleSet.find('.test-rule-resource .test-select-open').click();
await driver.findContent('.test-select-menu li', 'Income').click();
await enterRulePart(ruleSet, 1, 'user.Email == "noone1"', {R: 'deny'});
// Make a rule for FinancialsTable.Year and FinancialsTable.Expenses that allows something.
await findTable(/FinancialsTable/).find('.test-rule-table-menu-btn').click();
await driver.findContent('.grist-floating-menu li', /Add Column Rule/).click();
ruleSet = findRuleSet(/FinancialsTable/, 2);
await ruleSet.find('.test-rule-resource .test-select-open').click();
assert.deepEqual(
await driver.findAll('.test-select-menu li', el => el.getText()),
['Expenses', 'Income', 'Year']
);
await driver.findContent('.test-select-menu li', 'Year').click();
await ruleSet.find('.test-rule-resource .test-select-open').click();
await driver.findContent('.test-select-menu li', 'Expenses').click();
await enterRulePart(ruleSet, 1, 'user.Email == "noone2"', {R: 'allow'});
// Check that trying to save throws an error.
await driver.find('.test-rules-save').click();
await gu.waitForServer();
await driver.findContent('.test-notifier-toast-wrapper',
/Column Year appears .* table FinancialsTable .* might be order-dependent/);
await gu.wipeToasts();
// Tweak one rule to be compatible (no order dependency) with the other, and recheck.
await enterRulePart(ruleSet, 1, 'user.Email == "noone2"', {R: 'deny'});
await gu.waitForServer();
assert.lengthOf(await gu.getToasts(), 0);
});
it("'Add Widget to Page' should be disabled", async() => {
const mainSession = await gu.session().teamSite.user('user1').login();
await mainSession.loadDoc(`/doc/${docId}/p/acl`, {wait: false});
await driver.findWait('.test-rule-set', 2000);
await driver.find('.test-dp-add-new').doClick();
assert.includeMembers(await driver.findAll('.test-dp-add-new-menu > li.disabled', (imp) => imp.getText()), [
"Add Widget to Page",
]);
await driver.sendKeys(Key.ESCAPE);
await gu.waitAppFocus();
});
it('should support dollar syntax in the editor', async function() {
const mainSession = await gu.session().teamSite.user('user1').login();
await mainSession.loadDoc(`/doc/${docId}/p/acl`, {wait: false});
// Wait for ACL page to load.
await driver.findWait('.test-rule-set', 2000);
// Add rules for FinancialsTable.
const ruleSet = findRuleSet(/FinancialsTable/, 1);
// Use new syntax to hide Year 2010.
// First make sure we have autocomplete working.
await triggerAutoComplete(ruleSet, 1, '$');
await gu.waitToPass(async () => {
const completions = await driver.findAll('.ace_autocomplete .ace_line', el => el.getText());
assert.deepEqual(completions, [
'$\nExpenses\n ',
'$\nid\n ',
'$\nIncome\n ',
'$\nYear\n ',
]);
});
await driver.sendKeys(Key.ESCAPE);
// Next test that column is understood.
await enterRulePart(ruleSet, 1, '1 == 1 and (rec.Year != "2" and $Year2 == "2010")', {R: 'deny'});
await gu.waitForServer();
assert.match(await ruleSet.find('.test-rule-part:nth-child(1) .test-rule-error').getText(),
/Invalid columns: Year2/);
assert.equal(await driver.find('.test-rules-save').isDisplayed(), false);
assert.equal(await driver.find('.test-rules-non-save').isDisplayed(), true);
assert.equal(await driver.find('.test-rules-non-save').getText(), 'Invalid');
// Now apply the rule.
await enterRulePart(ruleSet, 1, '($Year == "2010" or rec.Year == "2011")', {R: 'deny'});
await gu.waitForServer();
// Remove second rule that hides table for everyone.
await ruleSet.find('.test-rule-part-and-memo:nth-child(2) .test-rule-remove').click();
await driver.find('.test-rules-save').click();
await gu.waitForServer();
// Test that this rule works.
await gu.openPage("FinancialsTable");
assert.deepEqual(await gu.getVisibleGridCells(
{cols: ['Year'], rowNums: [1, 2]}
), [
'2022',
'',
]);
});
});

View File

@ -0,0 +1,862 @@
/**
* Test of the UI for Granular Access Control, part 2.
*/
import escapeRegExp from 'lodash/escapeRegExp';
import { assert, driver, Key, stackWrapFunc, WebElement } from 'mocha-webdriver';
import { enterRulePart, findDefaultRuleSet, findRuleSet, findTable,
getRuleText } from 'test/nbrowser/aclTestUtils';
import * as gu from 'test/nbrowser/gristUtils';
import { setupTestSuite } from 'test/nbrowser/testUtils';
const isChecked = stackWrapFunc(async function(el: WebElement): Promise<boolean> {
return await el.getAttribute('checked') !== null;
});
const isDisabled = stackWrapFunc(async function(el: WebElement): Promise<boolean> {
return await el.getAttribute('disabled') !== null;
});
describe("AccessRules2", function() {
this.timeout(40000);
const cleanup = setupTestSuite();
let docId: string;
before(async function() {
// Import a test document we've set up for this.
const mainSession = await gu.session().teamSite.user('user1').login();
docId = (await mainSession.tempDoc(cleanup, 'ACL-Test.grist', {load: false})).id;
// Share it with a few users.
const api = mainSession.createHomeApi();
await api.updateDocPermissions(docId, { users: {
[gu.translateUser("user2").email]: 'owners',
[gu.translateUser("user3").email]: 'editors',
} });
return docId;
});
afterEach(() => gu.checkForErrors());
let viewAsUrl: string;
it('should list users with access to the document, plus examples', async function() {
// Open AccessRules page, and click Users button.
const mainSession = await gu.session().teamSite.user('user1').login();
await mainSession.loadDoc(`/doc/${docId}`);
await driver.find('.test-tools-access-rules').click();
await driver.findContentWait('button', /View As/, 3000).click();
const [email1, email2, email3] = ['user1', 'user2', 'user3'].map(u => gu.translateUser(u as any).email);
// All users the doc is shared with should be listed, with correct Access.
assert.equal(await driver.findContent('.test-acl-user-item', email1).isPresent(), false);
assert.equal(await driver.findContent('.test-acl-user-item', email2)
.find('.test-acl-user-access').getText(), '(Owner)');
assert.equal(await driver.findContent('.test-acl-user-item', email3)
.find('.test-acl-user-access').getText(), '(Editor)');
// Check examples are present.
assert.deepEqual(
(await driver.findAll('.test-acl-user-item span', e => e.getText()))
.filter(txt => txt.includes('@example')),
['owner@example.com', 'editor1@example.com', 'editor2@example.com', 'viewer@example.com', 'unknown@example.com']);
// Add a user attribute table.
await mainSession.createHomeApi().applyUserActions(docId, [
['AddTable', 'Zones', [{id: 'Email'}, {id: 'City'}]],
['AddRecord', 'Zones', null, {Email: email1, City: 'Seattle'}],
['AddRecord', 'Zones', null, {Email: email2, City: 'Boston'}],
['AddRecord', 'Zones', null, {Email: 'fast@speed.com', City: 'Cambridge'}],
['AddRecord', 'Zones', null, {Email: 'slow@speed.com', City: 'Springfield'}],
]);
const records = await mainSession.createHomeApi().applyUserActions(docId, [
['AddRecord', '_grist_ACLResources', -1, {tableId: '*', colIds: '*'}],
['AddRecord', '_grist_ACLRules', null, {
resource: -1, userAttributes: JSON.stringify({
name: 'Zone',
tableId: 'Zones',
charId: 'Email',
lookupColId: 'Email',
}),
}],
]);
// Refresh list.
await mainSession.loadDoc(`/doc/${docId}`);
await driver.find('.test-tools-access-rules').click();
await driver.findContentWait('button', /View As/, 3000).click();
// Check users from attribute table are present.
assert.deepEqual(
(await driver.findAll('.test-acl-user-item span', e => e.getText()))
.filter(txt => txt.includes('@speed')),
['fast@speed.com', 'slow@speed.com']);
// 'View As' is present, except for current user.
assert.equal(await driver.findContent('.test-acl-user-item', email1)
.isPresent(), false);
assert.equal(await driver.findContent('.test-acl-user-item', email2)
.isPresent(), true);
// Click "View As", and wait for doc to reload.
await driver.findContent('.test-acl-user-item', email3).click();
await gu.waitForUrl(/aclAsUser/);
await gu.waitForDocToLoad();
// Make sure we are not on ACL page.
viewAsUrl = await driver.getCurrentUrl();
assert.notMatch(viewAsUrl, /\/p\/acl/);
// Check for a tag in the doc header.
assert.equal(await driver.findWait('.test-view-as-banner', 2000).isPresent(), true);
assert.match(await driver.find('.test-view-as-banner .test-select-open').getText(),
new RegExp(gu.translateUser('user3').name, 'i'));
// check the aclAsUser parameter on the url persists after navigating to another page
await gu.getPageItem('FinancialsTable').click();
await gu.waitForDocToLoad();
assert.equal(await gu.getActiveSectionTitle(), 'FINANCIALSTABLE');
assert.match(await driver.getCurrentUrl(), /aclAsUser/);
// Revert.
await driver.find('.test-view-as-banner .test-revert').click();
await gu.waitForDocToLoad();
// Delete user attribute table.
await mainSession.createHomeApi().applyUserActions(docId, [
['RemoveRecord', '_grist_ACLRules', records.retValues[1]],
['RemoveRecord', '_grist_ACLResources', records.retValues[0]],
['RemoveTable', 'Zones'],
]);
});
it('should allow returning from view-as mode via View As banner', async function() {
// Check that we can revert view-as and visit Access Rules page with the button next to the
// page item.
await driver.get(viewAsUrl);
await gu.waitForUrl(/aclAsUser/);
await gu.waitForDocToLoad();
await driver.find('.test-view-as-banner .test-revert').click();
await gu.waitForDocToLoad();
assert.equal(await driver.find('.test-view-as-banner').isPresent(), false);
});
it('should allow switching users in view-as mode', async function() {
// open doc in view-as mode
await driver.get(viewAsUrl);
await gu.waitForUrl(/aclAsUser/);
await gu.waitForDocToLoad();
// check name
assert.match(await driver.find('.test-view-as-banner .test-select-open').getText(),
new RegExp(gu.translateUser('user3').name, 'i'));
// select other user
await driver.find('.test-view-as-banner .test-select-open').click();
await driver.findContent('.test-acl-user-item', gu.translateUser('user1').name).click();
// check name changed
await gu.waitForDocToLoad();
// check name updated correctly
assert.match(await driver.find('.test-view-as-banner .test-select-open').getText(), /Chimpy/);
});
it('should make all tables/columns available to editor of ACL rules', async function() {
const mainSession = await gu.session().teamSite.user('user1').login();
await mainSession.loadDoc(`/doc/${docId}`);
await driver.find('.test-tools-access-rules').click();
await driver.findWait('.test-rule-set', 2000);
// Add table rules for ClientsTable.
await driver.findContentWait('button', /Add Table Rules/, 2000).click();
const options = await driver.findAll('.grist-floating-menu li', e => e.getText());
assert.deepEqual(options, ["ClientsTable", "ClientsTable [by Shared]", "FinancialsTable"]);
await driver.findContent('.grist-floating-menu li', /ClientsTable/).click();
// Add rules hiding First_Name, Last_Name columns.
await findTable(/ClientsTable/).find('.test-rule-table-menu-btn').click();
await driver.findContent('.grist-floating-menu li', /Add Column Rule/).click();
let ruleSet = findRuleSet(/ClientsTable/, 1);
await ruleSet.find('.test-rule-resource .test-select-open').click();
await driver.findContent('.test-select-menu li', 'First_Name').click();
await ruleSet.find('.test-rule-resource .test-select-open').click();
await driver.findContent('.test-select-menu li', 'Last_Name').click();
await enterRulePart(ruleSet, 1, null, {R: 'deny'});
// Add table rule entirely hiding FinancialsTable.
await driver.findContentWait('button', /Add Table Rules/, 2000).click();
await driver.findContent('.grist-floating-menu li', /FinancialsTable/).click();
ruleSet = findDefaultRuleSet(/FinancialsTable/);
await enterRulePart(ruleSet, 1, null, {R: 'deny'});
// Save and reload.
await driver.find('.test-rules-save').click();
await gu.waitForServer();
await driver.navigate().refresh();
await driver.findWait('.test-rule-set', 5000);
// Now this user isn't aware of the inaccessible columns and table, but ACL rules should
// still list them.
// Remove Last_Name column from being blocked. Check that it's now available in dropdown.
await driver.findWait('.test-rule-set', 2000);
ruleSet = findRuleSet(/ClientsTable/, 1);
await ruleSet.find('.test-rule-resource').click();
await ruleSet.findContent('.test-acl-column', 'Last_Name').find('.test-acl-col-remove').click();
await ruleSet.find('.test-rule-resource .test-select-open').click();
assert.equal(await driver.findContent('.test-select-menu li', 'Last_Name').isPresent(), true);
await driver.sendKeys(Key.ESCAPE); // Close menu.
await driver.sendKeys(Key.ESCAPE); // Close editing of columns.
// Remove FinancialsTable from being blocked. Check that it's now available in dropdown.
ruleSet = findRuleSet(/FinancialsTable/, 1);
await ruleSet.find('.test-rule-part-and-memo:nth-child(1) .test-rule-remove').click();
await driver.findContentWait('button', /Add Table Rules/, 2000).click();
assert.equal(await driver.findContent('.grist-floating-menu li', /FinancialsTable/).isPresent(), true);
assert.equal(await driver.findContent('.grist-floating-menu li', /FinancialsTable/).matches('.disabled'), false);
await driver.sendKeys(Key.ESCAPE);
// Save
await driver.find('.test-rules-save').click();
await gu.waitForServer();
// Remove First_Name column from being blocked, and add back Last_Name column.
ruleSet = findRuleSet(/ClientsTable/, 1);
await ruleSet.find('.test-rule-resource').click();
await ruleSet.findContent('.test-acl-column', 'First_Name').find('.test-acl-col-remove').click();
await ruleSet.find('.test-rule-resource .test-select-open').click();
await driver.findContent('.test-select-menu li', 'Last_Name').click();
// Add back FinancialsTable to be blocked.
assert.equal(await findRuleSet(/FinancialsTable/, 1).isPresent(), false);
await driver.findContentWait('button', /Add Table Rules/, 2000).click();
await driver.findContent('.grist-floating-menu li', /FinancialsTable/).click();
ruleSet = findDefaultRuleSet(/FinancialsTable/);
await enterRulePart(ruleSet, 1, null, {R: 'deny'});
// Save and reload.
await driver.find('.test-rules-save').click();
await gu.waitForServer();
await driver.navigate().refresh();
await driver.findWait('.test-rule-set', 5000);
// Verify the results.
ruleSet = findRuleSet(/ClientsTable/, 1);
assert.deepEqual(await ruleSet.findAll('.test-acl-column', el => el.getText()), ['Last_Name']);
assert.equal(await findDefaultRuleSet(/FinancialsTable/).isPresent(), true);
// Remove the installed "Deny" rules to restore the initial state.
await findRuleSet(/ClientsTable/, 1).find('.test-rule-remove').click();
await findRuleSet(/FinancialsTable/, 1).find('.test-rule-remove').click();
await driver.find('.test-rules-save').click();
await gu.waitForServer();
});
it('should support user-attribute rules', async function() {
// Add a table to user for a user-attribute rule. User the API for test simplicity.
const mainSession = await gu.session().teamSite.user('user1').login();
const api = mainSession.createHomeApi();
await api.applyUserActions(docId, [
['AddTable', 'Access', [{id: 'Email'}, {id: 'SharedOnly', type: 'Bool'}]],
['AddRecord', 'Access', null, {Email: gu.translateUser('user1').email, SharedOnly: true}],
['AddRecord', 'Access', null, {Email: gu.translateUser('user2').email, SharedOnly: true}],
['AddRecord', 'Access', null, {Email: gu.translateUser('user3').email, SharedOnly: true}],
]);
// Now use the UI to add a user-attribute rule.
await mainSession.loadDoc(`/doc/${docId}`);
await driver.find('.test-tools-access-rules').click();
await gu.waitForServer();
await driver.findContentWait('button', /Add User Attributes/, 2000).click();
const userAttrRule = await driver.find('.test-rule-userattr');
await userAttrRule.find('.test-rule-userattr-name').click();
await driver.sendKeys("MyAccess", Key.ENTER);
// Type 'Email' into the attribute ace editor, which has 'user.' prefilled.
await userAttrRule.find('.test-rule-userattr-attr').click();
await driver.sendKeys("Email", Key.ENTER);
// Check that Table field offers a dropdown.
await userAttrRule.find('.test-rule-userattr-table').click();
assert.deepEqual(await driver.findAll('.test-select-menu li', el => el.getText()),
['Access', 'ClientsTable', 'ClientsTable [by Shared]', 'FinancialsTable']);
// Select a table and check that the Column field offers a dropdown.
await driver.findContent('.test-select-menu li', 'ClientsTable').click();
await userAttrRule.find('.test-rule-userattr-col').click();
assert.includeMembers(await driver.findAll('.test-select-menu li', el => el.getText()),
['Agent_Email', 'Email', 'First_Name']);
// Select a different table, and check that the Column field dropdown gets updated.
await userAttrRule.find('.test-rule-userattr-table').click();
await driver.sendKeys("Access", Key.ENTER);
await userAttrRule.find('.test-rule-userattr-col').click();
assert.deepEqual(await driver.findAll('.test-select-menu li', el => el.getText()),
['Email', 'SharedOnly', 'id']);
await driver.sendKeys("Email", Key.ENTER);
// Remove ClientTable rules, and add a new one using the new UserAttribute.
if (await findTable(/ClientsTable/).isPresent()) {
await findTable(/ClientsTable/).find('.test-rule-table-menu-btn').click();
await driver.findContent('.grist-floating-menu li', /Delete Table Rules/).click();
}
await driver.findContentWait('button', /Add Table Rules/, 2000).click();
await driver.findContent('.grist-floating-menu li', /ClientsTable/).click();
const ruleSet = findDefaultRuleSet(/ClientsTable/);
await ruleSet.find('.test-rule-part .test-rule-add').click();
// newRec term in the following does nothing, it is just there to test renaming later.
await enterRulePart(ruleSet, 1, `not user.MyAccess.SharedOnly or rec.Shared or newRec.Shared`,
{R: 'allow'});
await enterRulePart(ruleSet, 2, null, 'Deny All');
await driver.find('.test-rules-save').click();
await gu.waitToPass(async () => {
await gu.openPage('ClientsTable');
assert.equal(await gu.getGridRowCount(), 6);
});
// Now toggle the value in the Access table.
await gu.getPageItem('Access').click();
await gu.getCell({col: 'SharedOnly', rowNum: 1}).find('.widget_checkbox').click();
await gu.waitToPass(async () => {
await gu.openPage('ClientsTable');
assert.equal(await gu.getGridRowCount(), 20);
});
});
it('should allow adding an "Everyone Else" rule if one does not exist', async function() {
// Load the page with rules.
const mainSession = await gu.session().teamSite.user('user1').login();
await mainSession.loadDoc(`/doc/${docId}`);
await driver.find('.test-tools-access-rules').click();
await driver.findWait('.test-rule-set', 2000);
// ClientsTable rules, based on previous tests, includes an "Everyone Else" setting.
assert.isTrue(await findTable(/ClientsTable/).isPresent());
let ruleSet = findDefaultRuleSet(/ClientsTable/);
assert.lengthOf(await ruleSet.findAll('.test-rule-part'), 2);
assert.lengthOf(await ruleSet.findAll('.test-rule-add'), 2);
assert.equal(
await ruleSet.find('.test-rule-part-and-memo:nth-child(2) .test-rule-acl-formula').getText(),
"Everyone Else"
);
assert.equal(await ruleSet.find('.test-rule-extra-add').isPresent(), false);
// Delete it, and check that a plus button appears.
await ruleSet.find('.test-rule-part-and-memo:nth-child(2) .test-rule-remove').click();
assert.lengthOf(await ruleSet.findAll('.test-rule-part'), 1);
assert.lengthOf(await ruleSet.findAll('.test-rule-add'), 2);
assert.equal(await ruleSet.find('.test-rule-extra-add').isPresent(), true);
// Click "+" button, check that a rule appears.
await ruleSet.find('.test-rule-extra-add .test-rule-add').click();
assert.lengthOf(await ruleSet.findAll('.test-rule-part'), 2);
assert.lengthOf(await ruleSet.findAll('.test-rule-add'), 2);
assert.equal(
await ruleSet.find('.test-rule-part-and-memo:nth-child(2) .test-rule-acl-formula').getText(),
"Everyone Else"
);
assert.equal(await ruleSet.find('.test-rule-extra-add').isPresent(), false);
// Enter a condition into the new default rule.
await enterRulePart(ruleSet, 2, `True`, 'Deny All');
// A new "+" button should appear.
assert.lengthOf(await ruleSet.findAll('.test-rule-part'), 2);
assert.lengthOf(await ruleSet.findAll('.test-rule-add'), 3);
assert.equal(
await ruleSet.find('.test-rule-part-and-memo:nth-child(2) .test-rule-acl-formula').getText(),
"True"
);
assert.equal(await ruleSet.find('.test-rule-extra-add').isPresent(), true);
// Save
await driver.find('.test-rules-save').click();
await gu.waitForServer();
await driver.findWait('.test-rule-set', 2000);
// Check that the final "+" still appears.
ruleSet = findDefaultRuleSet(/ClientsTable/);
assert.lengthOf(await ruleSet.findAll('.test-rule-part'), 2);
assert.lengthOf(await ruleSet.findAll('.test-rule-add'), 3);
assert.equal(
await ruleSet.find('.test-rule-part-and-memo:nth-child(2) .test-rule-acl-formula').getText(),
"True"
);
assert.equal(await ruleSet.find('.test-rule-extra-add').isPresent(), true);
await gu.undo();
await driver.findWait('.test-rule-set', 2000);
ruleSet = findDefaultRuleSet(/ClientsTable/);
assert.lengthOf(await ruleSet.findAll('.test-rule-part'), 2);
assert.lengthOf(await ruleSet.findAll('.test-rule-add'), 2);
assert.equal(
await ruleSet.find('.test-rule-part-and-memo:nth-child(2) .test-rule-acl-formula').getText(),
"Everyone Else"
);
assert.equal(await ruleSet.find('.test-rule-extra-add').isPresent(), false);
});
it('should support renames and refresh rules when they get updated', async function() {
// Prepare to use the API.
const mainSession = await gu.session().teamSite.user('user1').login();
const api = mainSession.createHomeApi();
// Load the page with rules.
await mainSession.loadDoc(`/doc/${docId}`);
await driver.find('.test-tools-access-rules').click();
await driver.findWait('.test-rule-set', 2000);
// After the previous test case, we have these rules:
// - UserAttribute rule "MyAccess" that looks up user.Email in ClientsTable.Email.
// - On ClientsTable, rule allowing read when "not user.MyAccess.SharedOnly or rec.Shared ...".
// - On ClientsTable, default rule to Deny All.
// Check that it's what we see.
assert.deepEqual(await driver.findAll('.test-rule-userattr-attr', getRuleText),
['user.Email']);
assert.deepEqual(await driver.findAll('.test-rule-userattr .test-select-open', el => el.getText()),
['Access', 'Email']);
assert.match(await driver.find('.test-rule-table-header').getText(), / ClientsTable$/);
assert.lengthOf(await driver.findAll('.test-rule-set'), 2);
assert.deepEqual(await driver.find('.test-rule-set').findAll('.test-rule-acl-formula', getRuleText),
["not user.MyAccess.SharedOnly or rec.Shared or newRec.Shared", "Everyone Else"]);
// Rename some tables (raw view sections) and columns.
await api.applyUserActions(docId, [
['UpdateRecord', '_grist_Views_section', 3, {title: 'CLIENT LIST'}],
['UpdateRecord', '_grist_Views_section', 10, {title: 'ACCESS2'}],
['RenameColumn', 'CLIENT_LIST', 'Shared', 'PUBLIC'],
['RenameColumn', 'ACCESS2', 'Email', 'EMAIL_ADDR'],
['RenameColumn', 'ACCESS2', 'SharedOnly', 'ONLY_SHARED'],
]);
await gu.waitForServer();
// Rules should auto-update.
assert.deepEqual(await driver.findAll('.test-rule-userattr .test-select-open', el => el.getText()),
['ACCESS2', 'EMAIL_ADDR']);
assert.match(await driver.find('.test-rule-table-header').getText(), / CLIENT LIST/);
assert.lengthOf(await driver.findAll('.test-rule-set'), 2);
assert.deepEqual(await driver.find('.test-rule-set').findAll('.test-rule-acl-formula', getRuleText),
["not user.MyAccess.ONLY_SHARED or rec.PUBLIC or newRec.PUBLIC", "Everyone Else"]);
// Table options should update
await driver.findContentWait('button', /Add Table Rules/, 2000).click();
const options = await driver.findAll('.grist-floating-menu li', e => e.getText());
assert.deepEqual(options, ["ACCESS2", "CLIENT LIST", "CLIENT LIST [by Shared]", "FinancialsTable"]);
await driver.sendKeys(Key.ESCAPE); // Close menu.
// Make an unsaved change to the rules, e.g. add a new one.
const ruleSet = findDefaultRuleSet(/CLIENT LIST/);
await ruleSet.find('.test-rule-part .test-rule-add').click();
await enterRulePart(ruleSet, 1, `True`, {R: 'allow'});
// Partially undo the renames.
await api.applyUserActions(docId, [
['UpdateRecord', '_grist_Views_section', 3, {title: 'ClientsTable'}],
['RenameColumn', 'ClientsTable', 'PUBLIC', 'Shared'],
]);
await gu.waitForServer();
// Rules should NOT auto-update, but show a message instead.
assert.match(await driver.find('.test-rule-table-header').getText(), / CLIENT LIST/);
assert.match(await driver.find('.test-access-rules-error').getText(), /Access rules have changed/);
// Click "Reset" to update them.
await driver.find('.test-rules-revert').click();
await gu.waitForServer();
// Finish undoing the renames. Fiddling with a user attribute table currently forces
// a reload.
await api.applyUserActions(docId, [
['UpdateRecord', '_grist_Views_section', 10, {title: 'Access'}],
['RenameColumn', 'Access', 'EMAIL_ADDR', 'Email'],
['RenameColumn', 'Access', 'ONLY_SHARED', 'SharedOnly'],
]);
await gu.waitForServer();
// Check results; it should be what we started with.
await gu.waitToPass(async () =>
assert.deepEqual(await driver.findAll('.test-rule-userattr .test-select-open', el => el.getText()),
['Access', 'Email']));
assert.match(await driver.find('.test-rule-table-header').getText(), / ClientsTable$/);
assert.lengthOf(await driver.findAll('.test-rule-set'), 2);
assert.deepEqual(await driver.find('.test-rule-set').findAll('.test-rule-acl-formula', getRuleText),
["not user.MyAccess.SharedOnly or rec.Shared or newRec.Shared", "Everyone Else"]);
});
it('should support special rules', async function() {
// Prepare to use the API.
const mainSession = await gu.session().teamSite.user('user1').login();
// Load the page with rules.
await mainSession.loadDoc(`/doc/${docId}`);
await driver.find('.test-tools-access-rules').click();
await driver.findWait('.test-rule-set', 2000);
// Check that the special checkboxes are unchecked, and advanced UI is hidden.
assert.deepEqual(await driver.findAll('.test-rule-special-checkbox', isChecked), [false, true, false, false]);
assert.deepEqual(await driver.findAll('.test-rule-special', el => el.find('.test-rule-set').isPresent()),
[false, false, false, false]);
// Mark the 'Allow everyone to view Access Rules' checkbox and save.
await gu.scrollIntoView(driver.find('.test-rule-special-AccessRules .test-rule-special-checkbox')).click();
await driver.find('.test-rules-save').click();
await gu.waitForServer();
// Verify that it's checked after saving.
assert.deepEqual(await driver.findAll('.test-rule-special-checkbox', isChecked), [false, true, true, false]);
// Expand advanced UI and delete the added rule there.
await gu.scrollIntoView(driver.find('.test-rule-special-AccessRules .test-rule-special-expand')).click();
await driver.find('.test-rule-special-AccessRules .test-rule-part-and-memo:nth-child(1) .test-rule-remove').click();
// The checkbox should now be unchecked.
assert.deepEqual(await driver.findAll('.test-rule-special-checkbox', isChecked), [false, true, false, false]);
// Add a new non-standard rule
const ruleSet = await driver.find('.test-rule-special-AccessRules .test-rule-set');
await ruleSet.find('.test-rule-part .test-rule-add').click();
await enterRulePart(ruleSet, 1, 'user.Access == EDITOR', {R: 'allow'});
// The checkbox should now be disabled.
assert.deepEqual(await driver.findAll('.test-rule-special-checkbox', isChecked), [false, true, false, false]);
assert.deepEqual(await driver.findAll('.test-rule-special-checkbox', isDisabled), [false, false, true, false]);
// Mark the checkbox for the other rule.
await gu.scrollIntoView(driver.find('.test-rule-special-FullCopies .test-rule-special-checkbox')).click();
// Save and reload the page.
await driver.find('.test-rules-save').click();
await gu.waitForServer();
await driver.navigate().refresh();
await driver.findWait('.test-rule-set', 5000);
// Verify the state of the checkboxes.
assert.deepEqual(await driver.findAll('.test-rule-special-checkbox', isChecked), [false, true, false, true]);
assert.deepEqual(await driver.findAll('.test-rule-special-checkbox', isDisabled), [false, false, true, false]);
// Check that the advanced UI is shown for only the non-standard rule.
assert.deepEqual(await driver.findAll('.test-rule-special', el => el.find('.test-rule-set').isPresent()),
[false, false, true, false]);
});
it('should allow opening rules when they refer to a deleted table', async function() {
// After deleting a table, an invalid rule will remain. This test is NOT saying that this is
// the desired behavior; it only checks that in the presence of such an invalid rule, we can
// still open the Access Rules page.
const mainSession = await gu.session().teamSite.user('user1').login();
await mainSession.loadDoc(`/doc/${docId}`);
await driver.find('.test-tools-access-rules').click();
await driver.findWait('.test-rule-set', 2000);
assert.match(await driver.find('.test-rule-table-header').getText(), / ClientsTable$/);
// TODO: Something seems wrong about being able to remove a table to which one has no
// update-record or delete-record access. On the other hand, in this situation, an undo of the
// delete does get blocked.
await gu.removePage('ClientsTable', {withData: true});
await gu.waitForServer();
await gu.checkForErrors();
await driver.navigate().refresh();
await driver.findWait('.test-rule-set', 5000);
assert.match(await driver.find('.test-rule-table-header').getText(), / #Invalid \(ClientsTable\)$/);
// Remove access rule
await driver.findContent('.test-rule-table-header', / #Invalid \(ClientsTable\)$/)
.find('.test-rule-table-menu-btn').click();
await driver.findContent('.grist-floating-menu li', /Delete/).click();
await driver.find('.test-rules-save').click();
await gu.waitForServer();
assert.isTrue(await driver.find('.test-rules-non-save').isPresent());
});
it('should offer fixes for rules referring to deleted tables/columns', async function() {
// Create some temporary tables.
const mainSession = await gu.session().teamSite.user('user1').login();
await mainSession.loadDoc(`/doc/${docId}`);
await gu.addNewTable('TmpTable1');
await gu.addNewTable('TmpTable2');
// Add a user attribute referencing a column we will soon delete.
const api = mainSession.createHomeApi();
await api.applyUserActions(docId, [
['AddRecord', '_grist_ACLResources', -1, {tableId: '*', colIds: '*'}],
['AddRecord', '_grist_ACLRules', null, {
resource: -1, userAttributes: JSON.stringify({
name: 'Zig',
tableId: 'TmpTable2',
charId: 'Email',
lookupColId: 'B',
}),
}],
]);
// Open access control rules.
await mainSession.loadDoc(`/doc/${docId}`);
await driver.find('.test-tools-access-rules').click();
await driver.findWait('.test-rule-set', 2000);
// Add a rule for TmpTable1.
await driver.findContentWait('button', /Add Table Rules/, 2000).click();
await driver.findContentWait('.grist-floating-menu li', /TmpTable1/, 3000).click();
let ruleSet = findDefaultRuleSet(/TmpTable1/);
await enterRulePart(ruleSet, 1, null, 'Allow All');
// Add a rule for columns of TmpTable2.
await driver.findContentWait('button', /Add Table Rules/, 2000).click();
await driver.findContentWait('.grist-floating-menu li', /TmpTable2/, 3000).click();
ruleSet = findDefaultRuleSet(/TmpTable2/);
await enterRulePart(ruleSet, 1, null, 'Allow All');
await findTable(/TmpTable2/).find('.test-rule-table-menu-btn').click();
await driver.findContent('.grist-floating-menu li', /Add Column Rule/).click();
ruleSet = findRuleSet(/TmpTable2/, 1);
await ruleSet.find('.test-rule-resource .test-select-open').click();
await driver.findContent('.test-select-menu li', 'A').click();
await ruleSet.find('.test-rule-resource .test-select-open').click();
await driver.findContent('.test-select-menu li', 'B').click();
await ruleSet.find('.test-rule-resource .test-select-open').click();
await driver.findContent('.test-select-menu li', 'C').click();
await enterRulePart(ruleSet, 1, 'True', {R: 'allow'});
// Save the rules.
await driver.find('.test-rules-save').click();
await gu.waitForServer();
// Now remove TmpTable1 and some columns of TmpTable2.
await gu.removePage('TmpTable1', {withData: true});
await api.applyUserActions(docId, [
['RemoveColumn', 'TmpTable2', 'B'],
['RemoveColumn', 'TmpTable2', 'C'],
]);
await driver.find('.test-tools-access-rules').click();
await driver.findWait('.test-rule-set', 5000);
await driver.navigate().refresh();
// Check the list of rules looks plausible.
await driver.findWait('.test-rule-set', 5000);
assert.deepEqual(await driver.findAll('.test-rule-table-header', el => el.getText()),
['Rules for table #Invalid (TmpTable1)',
'Rules for table TmpTable2',
'Default Rules', 'Special Rules']);
// Press the buttons we expect to be offered for clean-up.
await driver.findContentWait('button', /Remove TmpTable1 rules/, 5000).click();
await driver.findContentWait('button', /Remove column B from TmpTable2 rules/, 5000).click();
await driver.findContentWait('button', /Remove column C from TmpTable2 rules/, 5000).click();
await driver.findContentWait('button', /Remove Zig user attribute/, 5000).click();
await driver.find('.test-rules-save').click();
await gu.waitForServer();
// Check the list of rules looks cleaner.
assert.deepEqual(await driver.findAll('.test-rule-table-header', el => el.getText()),
['Rules for table TmpTable2', 'Default Rules', 'Special Rules']);
// Check only the remaining column is mentioned.
assert.deepEqual(await driver.findAll('.test-acl-column', el => el.getText()),
['A']);
// Remove TmpTable2.
await gu.removePage('TmpTable2', {withData: true});
await driver.find('.test-tools-access-rules').click();
await driver.findWait('.test-rule-set', 5000);
await driver.navigate().refresh();
// Press the clean-up button we are offered, then check that "reset" works,
// then press and save.
await driver.findWait('.test-rule-set', 5000);
await driver.findContentWait('button', /Remove TmpTable2 rules/, 5000).click();
await driver.find('.test-rules-revert').click();
await gu.waitForServer();
await driver.findContentWait('button', /Remove TmpTable2 rules/, 5000).click();
await driver.find('.test-rules-save').click();
await gu.waitForServer();
// Check the list of rules looks cleaner.
assert.deepEqual(await driver.findAll('.test-rule-table-header', el => el.getText()),
['Default Rules', 'Special Rules']);
});
it.skip('should prevent combination of Public Edit access with granular ACLs', async function() {
// Share doc with everyone as viewer.
const mainSession = await gu.session().teamSite.user('user1').login();
const doc = await mainSession.tempDoc(cleanup, 'ACL-Test.grist', {load: false});
const api = mainSession.createHomeApi();
await api.updateDocPermissions(doc.id, { users: { 'everyone@getgrist.com': 'viewers' } });
// Open rules. There should be no warning message.
await mainSession.loadDoc(`/doc/${doc.id}`);
await driver.find('.test-tools-access-rules').click();
await driver.findWait('.test-rule-set', 2000);
assert.equal(await driver.find('.test-access-rules-error').getText(), '');
// Add a rule, and save.
await driver.find('.test-rule-special-AccessRules .test-rule-special-checkbox').click();
await driver.find('.test-rules-save').click();
await gu.waitForServer();
// Check that it worked.
assert.isTrue(await isChecked(await driver.find('.test-rule-special-AccessRules .test-rule-special-checkbox')));
assert.equal(await driver.find('.test-rules-save').isDisplayed(), false);
assert.equal(await driver.find('.test-rules-non-save').getText(), 'Saved');
// Try to change public access to editor. It should downgrade to viewer with a warning toast.
await driver.find('.test-tb-share').click();
await driver.findContent('.test-tb-share-option', /Manage Users/).doClick();
await driver.findWait('.test-um-public-access', 3000).click();
assert.match(await driver.find('.test-um-public-member .test-um-member-role').getText(), /Viewer/);
await driver.find('.test-um-public-member .test-um-member-role').click();
await driver.findContent('.test-um-role-option', /Editor/).click();
await gu.saveAcls();
await gu.waitForServer();
const toast = driver.findWait('.test-notifier-toast-wrapper', 500);
assert.match(await toast.getText(), /incompatible.*Reduced to "Viewer"/i);
await toast.find('.test-notifier-toast-close').click(); // Close the toast.
// Remove the rule we added, and save.
await driver.find('.test-rule-special-AccessRules .test-rule-special-checkbox').click();
await driver.find('.test-rules-save').click();
await gu.waitForServer();
assert.isFalse(await isChecked(await driver.find('.test-rule-special-AccessRules .test-rule-special-checkbox')));
// Now change public access to editor. There should be no warning notifications.
await driver.find('.test-tb-share').click();
await driver.findContent('.test-tb-share-option', /Manage Users/).doClick();
await driver.findWait('.test-um-public-access', 3000).click();
assert.match(await driver.find('.test-um-public-member .test-um-member-role').getText(), /Viewer/);
await driver.find('.test-um-public-member .test-um-member-role').click();
await driver.findContent('.test-um-role-option', /Editor/).click();
await gu.saveAcls();
await gu.waitForServer();
assert.lengthOf(await gu.getToasts(), 0);
// Open rules. There should be a warning message.
await driver.find('.test-tools-access-rules').click();
assert.match(await driver.find('.test-access-rules-error').getText(),
/Public "Editor".*incompatible.*remove it or reduce to "Viewer"/i);
// Try to add a rule. We should not be able to save.
await driver.find('.test-rule-special-AccessRules .test-rule-special-checkbox').click();
assert.equal(await driver.find('.test-rules-save').isDisplayed(), false);
assert.equal(await driver.find('.test-rules-non-save').getText(), 'Invalid');
});
it("Should update save button every 1 second when editing a formula", async function() {
// Prepare to use the API.
const mainSession = await gu.session().teamSite.user('user1').login();
// Load the page with rules.
await mainSession.loadDoc(`/doc/${docId}`);
await driver.find('.test-tools-access-rules').click();
await driver.findWait('.test-rule-set', 2000);
// check the save button is in 'saved' state
assert.equal(await driver.find('.test-rules-non-save').getText(), 'Saved');
// add new rule
const ruleSet = findDefaultRuleSet('*');
await ruleSet.find('.test-rule-add').click();
// check the save button is still in 'saved' state
// (nothing useful to save yet)
assert.equal(await driver.find('.test-rules-non-save').getText(), 'Saved');
// start typing invalid formula
const part = ruleSet.find(`.test-rule-part`);
await part.findWait('.test-rule-acl-formula .ace_editor', 500);
await part.find('.test-rule-acl-formula').doClick();
await driver.findWait('.test-rule-acl-formula .ace_focus', 500);
await gu.sendKeys('fdsa');
// check save button is still in 'saved' state
// (UI hasn't caught up with user yet)
assert.equal(await driver.find('.test-rules-non-save').getText(), 'Saved');
// wait 1 second
await driver.sleep(1000);
// check the save button is now enabled
assert.equal(await driver.find('.test-rules-non-save').getText(), 'Invalid');
// click reset
await driver.find('.test-rules-revert').click();
});
it("should have a working dots menu", async function() {
const mainSession = await gu.session().teamSite.user('user1').login();
await mainSession.loadDoc(`/doc/${docId}/p/3`);
let url = '';
// check page is correct
url = await driver.getCurrentUrl();
assert.isTrue(url.includes('p/3'));
assert.equal(await gu.getCurrentPageName(), "Access");
await gu.openAccessRulesDropdown();
const [name1, name2, name3] = ['user1', 'user2', 'user3'].map(u => gu.translateUser(u as any).name)
.map(name => new RegExp(escapeRegExp(name), 'i'));
// All users the doc is shared with should be listed, with correct Access.
assert.equal(await driver.findContent('.grist-floating-menu a', name1).isPresent(), false);
assert.include(await driver.findContent('.grist-floating-menu a', name2).getText(), '(Owner)');
assert.include(await driver.findContent('.grist-floating-menu a', name3).getText(), '(Editor)');
await driver.findContent('.grist-floating-menu a', name3).click();
await gu.waitForUrl(/aclAsUser/);
await gu.waitForDocToLoad();
// Check for a tag in the doc header.
assert.equal(await driver.findWait('.test-view-as-banner', 2000).isPresent(), true);
// check doc is still on same page
url = await driver.getCurrentUrl();
assert.isTrue(url.includes('p/3'));
assert.equal(await gu.getCurrentPageName(), "Access");
// Revert.
await driver.find('.test-view-as-banner .test-revert').click();
await gu.waitForDocToLoad();
});
it('should keep dots menu in sync', async function() {
const checkAccess = async (role: string) => {
await gu.openAccessRulesDropdown();
await gu.waitToPass(async () => {
assert.include(await driver.findContent('.grist-floating-menu a', /kiwi/i).getText(), role);
});
await driver.sendKeys(Key.ESCAPE);
};
// open doc
const mainSession = await gu.session().teamSite.user('user1').login();
await mainSession.loadDoc(`/doc/${docId}`);
// check kiwi matches (Editor)
await checkAccess('(Editor)');
// update kiwi access to viewers
const api = mainSession.createHomeApi();
await api.updateDocPermissions(docId, { users: { [gu.translateUser('user3').email]: 'viewers' } });
// check kiwi matches (Viewer)
await checkAccess('(Viewer)');
});
it('should hide dots menu for users without ACL access', async function() {
// Login as user3 and open doc
const mainSession = gu.session().teamSite.user('user1');
await mainSession.teamSite.user('user3').login();
await mainSession.loadDoc(`/doc/${docId}`);
// Check absence of dots menu
await driver.find('.test-tools-access-rules').mouseMove();
await gu.waitToPass(async () => {
assert.equal(await driver.find('.test-tools-access-rules-trigger').isPresent(), true);
});
assert.equal(await driver.find('.test-tools-access-rules-trigger').isDisplayed(), false);
});
});

View File

@ -0,0 +1,290 @@
/**
* Test of the UI for Granular Access Control, part 3.
*/
import { assert, driver } from 'mocha-webdriver';
import { assertChanged, assertSaved, enterRulePart, findDefaultRuleSet,
findRuleSet, findTable, getRules, hasExtraAdd, removeRules,
removeTable } from 'test/nbrowser/aclTestUtils';
import * as gu from 'test/nbrowser/gristUtils';
import { setupTestSuite } from 'test/nbrowser/testUtils';
describe("AccessRules3", function() {
this.timeout(40000);
const cleanup = setupTestSuite();
let docId: string;
before(async function() {
// Import a test document we've set up for this.
const mainSession = await gu.session().teamSite.user('user1').login();
docId = (await mainSession.tempDoc(cleanup, 'ACL-Test.grist', {load: false})).id;
// Share it with a few users.
const api = mainSession.createHomeApi();
await api.updateDocPermissions(docId, { users: {
[gu.translateUser("user2").email]: 'owners',
[gu.translateUser("user3").email]: 'editors',
} });
return docId;
});
afterEach(() => gu.checkForErrors());
describe('SeedRule special', function() {
// When a tooltip is present, it introduces this extra text into getText() result.
const tooltipMarker = "\n?";
it('can add initial rules based on SeedRule special', async function() {
// Open Access Rules page.
const mainSession = await gu.session().teamSite.user('user1').login();
await mainSession.loadDoc(`/doc/${docId}`);
await driver.find('.test-tools-access-rules').click();
await driver.findWait('.test-rule-set', 2000);
// Check seed rule checkbox is unselected.
const seedRule = await driver.find('div.test-rule-special-SeedRule');
const checkbox = seedRule.find('input[type=checkbox]');
assert.equal(await checkbox.isSelected(), false);
// Expand and check there's an empty rule.
await driver.find('.test-rule-special-SeedRule .test-rule-special-expand').click();
await assertSaved();
await getRules(seedRule);
assert.deepEqual(await getRules(seedRule),
[{ formula: 'Everyone', perm: '' }]);
assert.equal(await hasExtraAdd(seedRule), false);
// Adding rules for a new table/column should look the same as always.
await driver.findContentWait('button', /Add Table Rules/, 2000).click();
await driver.findContentWait('.grist-floating-menu li', /FinancialsTable/, 3000).click();
await assertChanged();
let fin = findTable(/FinancialsTable/);
assert.deepEqual(await getRules(fin),
[{ formula: 'Everyone', perm: '', res: 'All' }]);
await fin.find('.test-rule-table-menu-btn').click();
await driver.findContent('.grist-floating-menu li', /Add Column Rule/).click();
assert.deepEqual(await getRules(fin),
[{ formula: 'Everyone', perm: '', res: '[Add Column]' },
{ formula: 'Everyone', perm: '', res: 'All' + tooltipMarker }]);
await removeTable(/FinancialsTable/);
await assertSaved();
// Now check the box, and see we get the rule we expect.
await checkbox.click();
await assertChanged();
assert.deepEqual(await getRules(seedRule),
[{ formula: 'user.Access in [OWNER]', perm: '+R+U+C+D' }]);
assert.equal(await hasExtraAdd(seedRule), true);
// New table rules should start off with that rule.
await driver.findContentWait('button', /Add Table Rules/, 2000).click();
await driver.findContentWait('.grist-floating-menu li', /FinancialsTable/, 3000).click();
fin = findTable(/FinancialsTable/);
assert.deepEqual(await getRules(fin),
[{ formula: 'user.Access in [OWNER]', perm: '+R+U+C+D', res: 'All' },
{ formula: 'Everyone Else', perm: '', res: 'All' }]);
assert.equal(await hasExtraAdd(fin), false);
// New column rules should start off with that rule.
await fin.find('.test-rule-table-menu-btn').click();
await driver.findContent('.grist-floating-menu li', /Add Column Rule/).click();
assert.deepEqual(await getRules(fin),
[{ formula: 'user.Access in [OWNER]', perm: '+R+U', res: '[Add Column]' },
{ formula: 'Everyone Else', perm: '', res: '[Add Column]' },
{ formula: 'user.Access in [OWNER]', perm: '+R+U+C+D', res: 'All' + tooltipMarker },
{ formula: 'Everyone Else', perm: '', res: 'All' + tooltipMarker }]);
// Make sure that removing and re-adding default rules works as expected.
await removeRules(findDefaultRuleSet(/FinancialsTable/));
assert.equal(await findDefaultRuleSet(/FinancialsTable/).isPresent(), false);
assert.deepEqual(await getRules(fin),
[{ formula: 'user.Access in [OWNER]', perm: '+R+U', res: '[Add Column]' },
{ formula: 'Everyone Else', perm: '', res: '[Add Column]' }]);
await fin.find('.test-rule-table-menu-btn').click();
await driver.findContent('.grist-floating-menu li', /Add Table-wide Rule/).click();
assert.deepEqual(await getRules(fin),
[{ formula: 'user.Access in [OWNER]', perm: '+R+U', res: '[Add Column]' },
{ formula: 'Everyone Else', perm: '', res: '[Add Column]' },
{ formula: 'user.Access in [OWNER]', perm: '+R+U+C+D', res: 'All' + tooltipMarker },
{ formula: 'Everyone Else', perm: '', res: 'All' + tooltipMarker }]);
await removeTable(/FinancialsTable/);
// Check that we can tweak the seed rules if we want.
await seedRule.find('.test-rule-part .test-rule-add').click();
await enterRulePart(seedRule, 1, 'user.Access in [EDITOR]', 'Deny All', 'memo1');
assert.equal(await checkbox.getAttribute('disabled'), 'true');
// New table rules should include the seed rules.
await driver.findContentWait('button', /Add Table Rules/, 2000).click();
await driver.findContentWait('.grist-floating-menu li', /FinancialsTable/, 3000).click();
fin = findTable(/FinancialsTable/);
assert.deepEqual(await getRules(fin),
[{ formula: 'user.Access in [EDITOR]', perm: '-R-U-C-D', res: 'All', memo: 'memo1'},
{ formula: 'user.Access in [OWNER]', perm: '+R+U+C+D', res: 'All' },
{ formula: 'Everyone Else', perm: '', res: 'All' }]);
assert.equal(await hasExtraAdd(fin), false);
await removeTable(/FinancialsTable/);
// Check that returning to the single OWNER rule gets us back to an uncomplicated
// selected checkbox.
await assertChanged();
assert.equal(await checkbox.getAttribute('disabled'), 'true');
assert.equal(await checkbox.isSelected(), false);
await seedRule.find('.test-rule-part .test-rule-remove').click();
assert.equal(await checkbox.getAttribute('disabled'), null);
assert.equal(await checkbox.isSelected(), true);
// Check that removing that rule deselected the checkbox and collapses rule list.
await seedRule.find('.test-rule-part .test-rule-remove').click();
assert.equal(await checkbox.getAttribute('disabled'), null);
assert.equal(await checkbox.isSelected(), false);
await assertSaved();
assert.lengthOf(await seedRule.findAll('.test-rule-set'), 0);
// Expand again, and make sure we are back to default.
await driver.find('.test-rule-special-SeedRule .test-rule-special-expand').click();
assert.lengthOf(await seedRule.findAll('.test-rule-set'), 1);
assert.deepEqual(await getRules(seedRule),
[{ formula: 'Everyone', perm: '' }]);
await assertSaved();
});
it('can save and reload SeedRule special', async function() {
const mainSession = await gu.session().teamSite.user('user1').login();
await mainSession.loadDoc(`/doc/${docId}`);
await driver.find('.test-tools-access-rules').click();
await driver.findWait('.test-rule-set', 2000);
// Initially nothing is selected and all is saved.
let seedRule = await driver.find('div.test-rule-special-SeedRule');
let checkbox = seedRule.find('input[type=checkbox]');
assert.equal(await checkbox.isSelected(), false);
await assertSaved();
// Clicking the checkbox is immediately save-able.
await checkbox.click();
await assertChanged();
// Save, and check state is correctly persisted.
await driver.find('.test-rules-save').click();
await gu.waitForServer();
seedRule = await driver.findWait('div.test-rule-special-SeedRule', 2000);
checkbox = seedRule.find('input[type=checkbox]');
assert.equal(await checkbox.isSelected(), true);
await assertSaved();
// Expand and ensure we see the expected rule.
await driver.find('.test-rule-special-SeedRule .test-rule-special-expand').click();
assert.deepEqual(await getRules(seedRule),
[{ formula: 'user.Access in [OWNER]', perm: '+R+U+C+D' }]);
assert.equal(await hasExtraAdd(seedRule), true);
// Now unselect the checkbox, and make sure that we can save+reload.
await checkbox.click();
await assertChanged();
await driver.find('.test-rules-save').click();
await gu.waitForServer();
seedRule = await driver.findWait('div.test-rule-special-SeedRule', 2000);
checkbox = seedRule.find('input[type=checkbox]');
assert.equal(await checkbox.isSelected(), false);
await assertSaved();
await driver.find('.test-rule-special-SeedRule .test-rule-special-expand').click();
assert.deepEqual(await getRules(seedRule),
[{ formula: 'Everyone', perm: '' }]);
assert.equal(await hasExtraAdd(seedRule), false);
// Select the checkbox again, and save. Then make a custom change.
await checkbox.click();
await assertChanged();
await driver.find('.test-rules-save').click();
await gu.waitForServer();
seedRule = await driver.findWait('div.test-rule-special-SeedRule', 2000);
checkbox = seedRule.find('input[type=checkbox]');
assert.equal(await checkbox.isSelected(), true);
await driver.find('.test-rule-special-SeedRule .test-rule-special-expand').click();
await seedRule.find('.test-rule-part .test-rule-add').click();
await enterRulePart(seedRule, 1, 'user.Access in [EDITOR]', 'Deny All', 'memo2');
assert.equal(await checkbox.getAttribute('disabled'), 'true');
assert.deepEqual(await getRules(seedRule),
[{ formula: 'user.Access in [EDITOR]', perm: '-R-U-C-D', memo: 'memo2' },
{ formula: 'user.Access in [OWNER]', perm: '+R+U+C+D' }]);
await assertChanged();
// Save the custom change, and make sure we can reload it.
await driver.find('.test-rules-save').click();
await gu.waitForServer();
seedRule = await driver.findWait('div.test-rule-special-SeedRule', 2000);
checkbox = seedRule.find('input[type=checkbox]');
assert.equal(await checkbox.isSelected(), false);
assert.equal(await checkbox.getAttribute('disabled'), 'true');
assert.deepEqual(await getRules(seedRule),
[{ formula: 'user.Access in [EDITOR]', perm: '-R-U-C-D', memo: 'memo2' },
{ formula: 'user.Access in [OWNER]', perm: '+R+U+C+D' }]);
await assertSaved();
// Undo; should now again have the simple checked checkbox for seed rules.
await gu.undo();
seedRule = await driver.findWait('div.test-rule-special-SeedRule', 2000);
checkbox = seedRule.find('input[type=checkbox]');
assert.equal(await checkbox.isSelected(), true);
});
it('does not include unavailable bits when saving', async function() {
// Open Access Rules page.
const mainSession = await gu.session().teamSite.user('user1').login();
await mainSession.loadDoc(`/doc/${docId}`);
await driver.find('.test-tools-access-rules').click();
await driver.findWait('.test-rule-set', 2000);
// Click the seed rule checkbox.
const seedRule = await driver.find('div.test-rule-special-SeedRule');
assert.equal(await seedRule.find('input[type=checkbox]').isSelected(), true);
// New table AND column rules should start off with that rule.
await driver.findContentWait('button', /Add Table Rules/, 2000).click();
await driver.findContentWait('.grist-floating-menu li', /FinancialsTable/, 3000).click();
let fin = findTable(/FinancialsTable/);
await fin.find('.test-rule-table-menu-btn').click();
await driver.findContent('.grist-floating-menu li', /Add Column Rule/).click();
const ruleSet = findRuleSet(/FinancialsTable/, 1);
await ruleSet.find('.test-rule-resource .test-select-open').click();
await driver.findContent('.test-select-menu li', 'Year').click();
assert.deepEqual(await getRules(fin),
[{ formula: 'user.Access in [OWNER]', perm: '+R+U', res: 'Year\n[Add Column]' },
{ formula: 'Everyone Else', perm: '', res: 'Year\n[Add Column]' },
{ formula: 'user.Access in [OWNER]', perm: '+R+U+C+D', res: 'All' + tooltipMarker },
{ formula: 'Everyone Else', perm: '', res: 'All' + tooltipMarker }]);
// Check that the Save button is enabled, and save.
await gu.userActionsCollect();
await assertChanged();
await driver.find('.test-rules-save').click();
await gu.waitForServer();
// This is the important check of this test: that for a column rule, we only save the "read"
// and "update" bits.
await gu.userActionsVerify([
["BulkAddRecord", "_grist_ACLResources", [-1, -2], {
colIds: ["Year", "*"],
tableId: ["FinancialsTable", "FinancialsTable"],
}],
["BulkAddRecord", "_grist_ACLRules", [-1, -2], {
resource: [-1, -2],
aclFormula: ["user.Access in [OWNER]", "user.Access in [OWNER]"],
// Specifically, we care that this permissionsText includes only RU bits for column rules.
permissionsText: ["+RU", "+CRUD"],
rulePos: [1/3, 2/3],
memo: ["", ""],
}],
]);
await assertSaved();
// Rules still look correct after saving.
fin = findTable(/FinancialsTable/);
assert.deepEqual(await getRules(fin),
[{ formula: 'user.Access in [OWNER]', perm: '+R+U', res: 'Year' },
{ formula: 'user.Access in [OWNER]', perm: '+R+U+C+D', res: 'All' + tooltipMarker }]);
});
});
});

View File

@ -0,0 +1,89 @@
/**
* Test of the UI for Granular Access Control, part 3.
*/
import { assert } from 'mocha-webdriver';
import * as gu from 'test/nbrowser/gristUtils';
import { setupTestSuite } from 'test/nbrowser/testUtils';
describe("AccessRules4", function() {
this.timeout('20s');
const cleanup = setupTestSuite();
afterEach(() => gu.checkForErrors());
it('allows editor to toggle a column', async function() {
const ownerSession = await gu.session().teamSite.user('user1').login();
const docId = await ownerSession.tempNewDoc(cleanup, undefined, {load: false});
// Create editor for this document.
const api = ownerSession.createHomeApi();
await api.updateDocPermissions(docId, { users: {
[gu.translateUser("user2").email]: 'editors',
}});
await api.applyUserActions(docId, [
// Now create a structure.
['RemoveTable', 'Table1'],
['AddTable', 'Table1', [
{id: 'Toggle', type: 'Bool'},
{id: 'Another', type: 'Text'},
{id: 'User_Access', type: 'Text', formula: 'user.Email', isFormula: false},
]],
// Now add access rules for Table2
['AddRecord', '_grist_ACLResources', -1, {tableId: 'Table1', colIds: '*'}],
// Owner can do anything
['AddRecord', '_grist_ACLRules', null, {
resource: -1, aclFormula: 'user.Access == OWNER', permissionsText: 'all',
}],
// User with an his email address in the User_Access column can do anything
['AddRecord', '_grist_ACLRules', null, {
resource: -1, aclFormula: 'user.Email == rec.User_Access', permissionsText: 'all',
}],
// Otherwise no access
['AddRecord', '_grist_ACLRules', null, {
resource: -1, aclFormula: '', permissionsText: 'none',
}],
]);
await ownerSession.loadDoc(`/doc/${docId}`);
// Make sure we can edit this as an owner.
await gu.sendCommand('insertRecordAfter');
assert.isEmpty(await gu.getCell('Another', 1).getText());
assert.equal(await gu.getCell('User_Access', 1).getText(), gu.translateUser('user1').email);
assert.isFalse(await gu.getCell('Toggle', 1).find('.widget_checkmark').isDisplayed());
await gu.getCell('Another', 1).click();
await gu.enterCell('owner');
await gu.getCell('Toggle', 1).mouseMove();
await gu.getCell('Toggle', 1).find('.widget_checkbox').click();
await gu.waitForServer();
assert.equal(await gu.getCell('Another', 1).getText(), 'owner');
assert.equal(await gu.getCell('User_Access', 1).getText(), gu.translateUser('user1').email);
assert.isTrue(await gu.getCell('Toggle', 1).find('.widget_checkmark').isDisplayed());
// Now login as user2.
const userSession = await gu.session().teamSite.user('user2').login();
await userSession.loadDoc(`/doc/${docId}`);
// Make sure we can edit this as an user2
await gu.sendCommand('insertRecordAfter');
assert.isEmpty(await gu.getCell('Another', 1).getText());
assert.equal(await gu.getCell('User_Access', 1).getText(), gu.translateUser('user2').email);
assert.isFalse(await gu.getCell('Toggle', 1).find('.widget_checkmark').isDisplayed());
await gu.getCell('Another', 1).click();
await gu.enterCell('user2');
await gu.getCell('Toggle', 1).mouseMove();
await gu.getCell('Toggle', 1).find('.widget_checkbox').click();
await gu.waitForServer();
assert.equal(await gu.getCell('Another', 1).getText(), 'user2');
assert.equal(await gu.getCell('User_Access', 1).getText(), gu.translateUser('user2').email);
assert.isTrue(await gu.getCell('Toggle', 1).find('.widget_checkmark').isDisplayed());
});
});

View File

@ -0,0 +1,232 @@
/**
* Test of the UI for the SchemaEdit permission in Granular Access Control.
*/
import { UserAPI } from 'app/common/UserAPI';
import { TableRecordValue } from 'app/common/DocActions';
import { assert, driver } from 'mocha-webdriver';
import { assertChanged, assertSaved, enterRulePart,
findDefaultRuleSet, findTable, getRules } from 'test/nbrowser/aclTestUtils';
import * as gu from 'test/nbrowser/gristUtils';
import { setupTestSuite } from 'test/nbrowser/testUtils';
import pick = require('lodash/pick');
describe("AccessRulesSchemaEdit", function() {
this.timeout(40000);
const cleanup = setupTestSuite();
let docId: string;
let mainSession: gu.Session;
let api: UserAPI;
let editorApi: UserAPI;
before(async function() {
const editorSession = await gu.session().teamSite.user('user2').login();
editorApi = editorSession.createHomeApi();
// Import a test document we've set up for this.
mainSession = await gu.session().teamSite.user('user1').login();
docId = await mainSession.tempNewDoc(cleanup, 'ACL-SchemaEdit', {load: false});
// Share it with a few users.
api = mainSession.createHomeApi();
await api.updateDocPermissions(docId, { users: {
[gu.translateUser("user2").email]: 'editors',
} });
return docId;
});
afterEach(() => gu.checkForErrors());
it('should allow disabling non-owner schemaEdit via checkbox', async function() {
const mainDocApi = api.getDocAPI(docId);
// Open Access Rules page.
await loadAccessRulesPage(mainSession, docId);
// Check the schemaEdit checkbox is checked (editors allowed)
assert.equal(await getSchemaEditCheckbox().isSelected(), true);
// Check that a warning is present.
assert.equal(await driver.find('.test-rule-schema-edit-warning').isDisplayed(), true);
// Check that default rules don't show the schemaEdit bit.
assert.deepEqual((await getRules(findTable('*')))[0],
{res: 'All', formula: 'user.Access in [EDITOR, OWNER]', perm: '+R+U+C+D'});
// Check that an editor CAN make structure changes (default behavior is unchanged).
await assert.isFulfilled(editorApi.applyUserActions(docId, [["RenameTable", 'Table1', 'Renamed1']]));
// Uncheck the box.
await getSchemaEditCheckbox().click();
assert.equal(await getSchemaEditCheckbox().isSelected(), false);
// Check the warning is now absent.
assert.equal(await driver.find('.test-rule-schema-edit-warning').isPresent(), false);
await assertChanged();
// Save the changes.
await saveRules();
// Check that it works: an editor cannot make structure changes.
await assert.isRejected(editorApi.applyUserActions(docId, [["RenameTable", 'Renamed1', 'Table1']]),
/Blocked by table structure access rules/);
// Check that after reload, the box is unchecked and the warning is gone.
await reloadAccessRulesPage();
assert.equal(await getSchemaEditCheckbox().isSelected(), false);
assert.equal(await driver.find('.test-rule-schema-edit-warning').isPresent(), false);
assert.equal(await getSchemaEditRuleSet().isPresent(), false);
// Check what the rules are on the default resource.
const rules = await mainDocApi.getRecords('_grist_ACLRules');
const defaultResourceRef = (await getDefaultResourceRec(api, docId))!.id;
assert.deepEqual(rules.map(r => pick(r.fields, "resource", "aclFormula", "permissionsText")),
[{resource: defaultResourceRef, aclFormula: 'user.Access != OWNER', permissionsText: '-S'}]);
// Revert by checking the box again.
await getSchemaEditCheckbox().click();
assert.equal(await getSchemaEditCheckbox().isSelected(), true);
// Check the warning is now present.
assert.equal(await driver.find('.test-rule-schema-edit-warning').isDisplayed(), true);
// Save the changes.
await saveRules();
// Check that it works: an editor can make structure changes again.
await assert.isFulfilled(editorApi.applyUserActions(docId, [["RenameTable", 'Renamed1', 'Table1']]));
// Check there are no rules left.
assert.lengthOf(await mainDocApi.getRecords('_grist_ACLRules'), 0);
});
it('should allow dismissing the warning', async function() {
const mainDocApi = api.getDocAPI(docId);
await loadAccessRulesPage(mainSession, docId);
// Check we are in the expected default state: checkbox is checked and a warning is present.
assert.equal(await getSchemaEditCheckbox().isSelected(), true);
assert.equal(await driver.find('.test-rule-schema-edit-warning').isPresent(), true);
// Click "Dismiss", and save.
await driver.findContent('.test-rule-schema-edit-warning a', /Dismiss/).click();
await assertChanged();
await saveRules();
// Check that an editor can still make structure changes.
await assert.isFulfilled(editorApi.applyUserActions(docId, [["RenameTable", 'Table1', 'Renamed2']]));
// Check that after reload, the box is checked and warning is gone.
await reloadAccessRulesPage();
assert.equal(await getSchemaEditCheckbox().isSelected(), true);
assert.equal(await driver.find('.test-rule-schema-edit-warning').isPresent(), false);
assert.equal(await getSchemaEditRuleSet().isPresent(), false);
// Check what the rules are on the default resource.
const rules = await mainDocApi.getRecords('_grist_ACLRules');
const defaultResourceRef = (await getDefaultResourceRec(api, docId))!.id;
assert.deepEqual(rules.map(r => pick(r.fields, "resource", "aclFormula", "permissionsText")),
[{resource: defaultResourceRef, aclFormula: 'user.Access == EDITOR', permissionsText: '+S'}]);
// Revert the rule change; wait for page to reload.
await gu.undo();
await driver.findWait('.test-rule-set', 2000);
assert.equal(await getSchemaEditCheckbox().isSelected(), true);
assert.equal(await driver.find('.test-rule-schema-edit-warning').isPresent(), true);
assert.lengthOf(await mainDocApi.getRecords('_grist_ACLRules'), 0);
// Let's revert also the table rename, to keep test cases independent.
await assert.isFulfilled(editorApi.applyUserActions(docId, [["RenameTable", 'Renamed2', 'Table1']]));
});
it('should handle existing rules that mix schemaEdit and other permissions', async function() {
const editorEmailAddr = gu.translateUser("user2").email;
const customAclFormula = `user.Email == "${editorEmailAddr}"`;
// Use the API to add a mixed rule, with both a 'schemaEdit' and a 'delete' permission.
const defaultResourceRef = (await getDefaultResourceRec(api, docId))!.id;
await api.applyUserActions(docId, [
['AddRecord', '_grist_ACLRules', null, {
resource: defaultResourceRef,
aclFormula: customAclFormula,
permissionsText: '-DS',
memo: 'Memo MIXED',
}],
]);
// Load the Access Rules page.
await loadAccessRulesPage(mainSession, docId);
// The new rule should be visible in both the default section, and in SchemaEdit section
// (which should be expanded).
assert.deepEqual((await getRules(findTable('*')))[0],
{formula: customAclFormula, perm: '-D', memo: 'Memo MIXED', res: 'All'});
assert.equal(await getSchemaEditRuleSet().isDisplayed(), true);
assert.deepEqual((await getRules(driver.find('.test-rule-special-SchemaEdit')))[0],
{formula: customAclFormula, perm: '-S', memo: 'Memo MIXED'});
// Check that the checkbox is disabled (since non-standard state).
assert.equal(await getSchemaEditCheckbox().getAttribute('disabled'), 'true');
// Check that the rule works.
let error = await editorApi.applyUserActions(docId, [["RenameTable", 'Table1', 'Renamed3']])
.then(() => null).catch(err => err);
assert.match(error?.message, /Blocked by table structure access rules/);
assert.deepInclude(error?.details, {memos: ['Memo MIXED']});
// Change the memos on both copies of the rule.
await enterRulePart(findDefaultRuleSet('*'), 1, null, {}, 'Memo DDD');
await enterRulePart(getSchemaEditRuleSet(), 1, null, {}, 'Memo SSS');
// Save.
await saveRules();
// The rules should look as before, only the memo is different.
assert.deepEqual((await getRules(findTable('*')))[0],
{formula: customAclFormula, perm: '-D', memo: 'Memo DDD', res: 'All'});
assert.deepEqual((await getRules(driver.find('.test-rule-special-SchemaEdit')))[0],
{formula: customAclFormula, perm: '-S', memo: 'Memo SSS'});
// Check that the changed rule works.
error = await editorApi.applyUserActions(docId, [["RenameTable", 'Table1', 'Renamed3']])
.then(() => null).catch(err => err);
assert.match(error?.message, /Blocked by table structure access rules/);
assert.deepInclude(error?.details, {memos: ['Memo SSS']});
// Check what the rules are on the default resource.
const mainDocApi = api.getDocAPI(docId);
const rules = await mainDocApi.getRecords('_grist_ACLRules');
assert.sameDeepMembers(rules.map(r => pick(r.fields, "resource", "aclFormula", "permissionsText")), [
{resource: defaultResourceRef, aclFormula: customAclFormula, permissionsText: '-S'},
{resource: defaultResourceRef, aclFormula: customAclFormula, permissionsText: '-D'}
]);
});
});
function getSchemaEditCheckbox() {
return driver.find('.test-rule-special-SchemaEdit input[type=checkbox]');
}
function getSchemaEditRuleSet() {
return driver.find('.test-rule-special-SchemaEdit .test-rule-set');
}
function getSaveButton() {
return driver.find('.test-rules-save');
}
async function saveRules() {
await getSaveButton().click();
await gu.waitForServer();
await assertSaved();
}
async function loadAccessRulesPage(session: gu.Session, docId: string) {
await session.loadRelPath(`/doc/${docId}/p/acl`);
await driver.findWait('.test-rule-set', 5000);
}
async function reloadAccessRulesPage() {
await driver.navigate().refresh();
await driver.findWait('.test-rule-set', 5000);
}
async function getDefaultResourceRec(api: UserAPI, docId: string): Promise<TableRecordValue|undefined> {
const records = await api.getDocAPI(docId).getRecords('_grist_ACLResources', {filters: {tableId: ['*']}});
return records[0];
}