(core) Implement new representation of ACL rules.

Summary:
- Added fields to _grist_ACLRules for the new Granular ACL representation
- Include a corresponding migration.

- Added ACLPermissions module with merging PermissionSets and converting to/from string.
- Implemented parsing of ACL formulas and compiling them into JS functions.
- Add automatic parsing of ACL formulas when ACLRules are added or updated.
- Convert GranularAccess to load and interpret new-style rules.
- Convert ACL UI to load and save new-style rules.

For now, no attempt to do anything better on the server or UI side, only to
reproduce previous behavior.

Test Plan: Added unittests for new files; fixed those for existing files.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D2664
This commit is contained in:
Dmitry S
2020-11-17 16:49:32 -05:00
parent c042935c58
commit bc3a472324
12 changed files with 1131 additions and 482 deletions

View File

@@ -8,8 +8,8 @@ import {primaryButton} from 'app/client/ui2018/buttons';
import {colors} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {menu, menuItem, select} from 'app/client/ui2018/menus';
import {decodeClause, GranularAccessDocClause, serializeClause} from 'app/common/GranularAccessClause';
import {arrayRepeat, setDifference} from 'app/common/gutil';
import {readAclRules} from 'app/common/GranularAccessClause';
import {setDifference} from 'app/common/gutil';
import {Computed, Disposable, dom, ObsArray, obsArray, Observable, styled} from 'grainjs';
import isEqual = require('lodash/isEqual');
@@ -18,19 +18,22 @@ interface AclState {
ownerOnlyStructure: boolean;
}
const MATCH_NON_OWNER = 'user.Access != "owners"';
function buildAclState(gristDoc: GristDoc): AclState {
const {ruleSets} = readAclRules(gristDoc.docData, {log: console});
console.log("FOUND RULE SETS", ruleSets);
const ownerOnlyTableIds = new Set<string>();
let ownerOnlyStructure = false;
const tableData = gristDoc.docModel.aclResources.tableData;
for (const res of tableData.getRecords()) {
const code = String(res.colIds);
const clause = decodeClause(code);
if (clause) {
if (clause.kind === 'doc') {
for (const ruleSet of ruleSets) {
if (ruleSet.tableId === '*' && ruleSet.colIds === '*') {
if (ruleSet.body.find(p => p.aclFormula === MATCH_NON_OWNER && p.permissionsText === '-S')) {
ownerOnlyStructure = true;
}
if (clause.kind === 'table' && clause.tableId) {
ownerOnlyTableIds.add(clause.tableId);
} else if (ruleSet.tableId !== '*' && ruleSet.colIds === '*') {
if (ruleSet.body.find(p => p.aclFormula === MATCH_NON_OWNER && p.permissionsText === 'none')) {
ownerOnlyTableIds.add(ruleSet.tableId);
}
}
}
@@ -63,43 +66,39 @@ export class AccessRules extends Disposable {
// changed by other users), and apply changes, if any, relative to that.
const latestState = buildAclState(this._gristDoc);
const currentState = this._currentState.get();
const tableData = this._gristDoc.docModel.aclResources.tableData;
await tableData.docData.bundleActions('Update Access Rules', async () => {
const docData = this._gristDoc.docData;
const resourcesTable = docData.getTable('_grist_ACLResources')!;
const rulesTable = docData.getTable('_grist_ACLRules')!;
await this._gristDoc.docData.bundleActions('Update Access Rules', async () => {
// If ownerOnlyStructure flag changed, add or remove the relevant resource record.
if (currentState.ownerOnlyStructure !== latestState.ownerOnlyStructure) {
const clause: GranularAccessDocClause = {
kind: 'doc',
match: { kind: 'const', charId: 'Access', value: 'owners' },
};
const colIds = serializeClause(clause);
if (currentState.ownerOnlyStructure) {
await tableData.sendTableAction(['AddRecord', null, {tableId: "", colIds}]);
} else {
const rowId = tableData.findMatchingRowId({tableId: '', colIds});
if (rowId) {
await this._gristDoc.docModel.aclResources.sendTableAction(['RemoveRecord', rowId]);
}
}
const defaultResource = resourcesTable.findMatchingRowId({tableId: '*', colIds: '*'}) ||
await resourcesTable.sendTableAction(['AddRecord', null, {tableId: '*', colIds: '*'}]);
const ruleObj = {resource: defaultResource, aclFormula: MATCH_NON_OWNER, permissionsText: '-S'};
const ruleRowId = rulesTable.findMatchingRowId(ruleObj);
if (currentState.ownerOnlyStructure && !ruleRowId) {
await rulesTable.sendTableAction(['AddRecord', null, ruleObj]);
} else if (!currentState.ownerOnlyStructure && ruleRowId) {
await rulesTable.sendTableAction(['RemoveRecord', ruleRowId]);
}
// Handle tables added to ownerOnlyTableIds.
const tablesAdded = setDifference(currentState.ownerOnlyTableIds, latestState.ownerOnlyTableIds);
if (tablesAdded.size) {
await tableData.sendTableAction(['BulkAddRecord', arrayRepeat(tablesAdded.size, null), {
tableId: [...tablesAdded],
colIds: [...tablesAdded].map(tableId => serializeClause({
kind: 'table',
tableId,
match: { kind: 'const', charId: 'Access', value: 'owners' },
})),
}]);
for (const tableId of tablesAdded) {
const resource = resourcesTable.findMatchingRowId({tableId, colIds: '*'}) ||
await resourcesTable.sendTableAction(['AddRecord', null, {tableId, colIds: '*'}]);
await rulesTable.sendTableAction(
['AddRecord', null, {resource, aclFormula: MATCH_NON_OWNER, permissionsText: 'none'}]);
}
// Handle table removed from ownerOnlyTableIds.
const tablesRemoved = setDifference(latestState.ownerOnlyTableIds, currentState.ownerOnlyTableIds);
if (tablesRemoved.size) {
const rowIds = Array.from(tablesRemoved, t => tableData.findRow('tableId', t)).filter(r => r);
await tableData.sendTableAction(['BulkRemoveRecord', rowIds]);
for (const tableId of tablesRemoved) {
const resource = resourcesTable.findMatchingRowId({tableId, colIds: '*'});
if (resource) {
const rowId = rulesTable.findMatchingRowId({resource, aclFormula: MATCH_NON_OWNER, permissionsText: 'none'});
if (rowId) {
await rulesTable.sendTableAction(['RemoveRecord', rowId]);
}
}
}
});
}