mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(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:
parent
5d349e603b
commit
d1803dddb7
505
test/nbrowser/AccessRules1.ts
Normal file
505
test/nbrowser/AccessRules1.ts
Normal 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',
|
||||||
|
'',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
862
test/nbrowser/AccessRules2.ts
Normal file
862
test/nbrowser/AccessRules2.ts
Normal 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
290
test/nbrowser/AccessRules3.ts
Normal file
290
test/nbrowser/AccessRules3.ts
Normal 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 }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
89
test/nbrowser/AccessRules4.ts
Normal file
89
test/nbrowser/AccessRules4.ts
Normal 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());
|
||||||
|
});
|
||||||
|
});
|
232
test/nbrowser/AccessRulesSchemaEdit.ts
Normal file
232
test/nbrowser/AccessRulesSchemaEdit.ts
Normal 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];
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user