(core) support adding user characteristic tables for granular ACLs

Summary:
This is a prototype for expanding the conditions that can be used in granular ACLs.

When processing ACLs, the following variables (called "characteristics") are now available in conditions:
 * UserID
 * Email
 * Name
 * Access (owners, editors, viewers)

The set of variables can be expanded by adding a "characteristic" clause.  This is a clause which specifies:
 * A tableId
 * The name of an existing characteristic
 * A colId
The effect of the clause is to expand the available characteristics with all the columns in the table, with values taken from the record where there is a match between the specified characteristic and the specified column.

Existing clauses are generalized somewhat to demonstrate and test the use these variables. That isn't the main point of this diff though, and I propose to leave generalizing+systematizing those clauses for a future diff.

Issues I'm not dealing with here:
 * How clauses combine.  (The scope on GranularAccessRowClause is a hack to save me worrying about that yet).
 * The full set of matching methods we'll allow.
 * Refreshing row access in clients when the tables mentioned in characteristic tables change.
 * Full CRUD permission control.
 * Default rules (part of combination).
 * Reporting errors in access rules.

That said, with this diff it is possible to e.g. assign a City to editors by their email address or name, and have only rows for those Cities be visible in their client. Ability to modify those rows, and remain updates about them, remains under incomplete control.

Test Plan: added tests

Reviewers: dsagal

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D2642
This commit is contained in:
Paul Fitzpatrick
2020-10-19 10:25:21 -04:00
parent 27fd894fc7
commit c879393a8e
9 changed files with 308 additions and 83 deletions

View File

@@ -8,6 +8,7 @@ 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 {Computed, Disposable, dom, ObsArray, obsArray, Observable, styled} from 'grainjs';
import isEqual = require('lodash/isEqual');
@@ -23,11 +24,14 @@ function buildAclState(gristDoc: GristDoc): AclState {
const tableData = gristDoc.docModel.aclResources.tableData;
for (const res of tableData.getRecords()) {
const code = String(res.colIds);
if (res.tableId && code === '~o') {
ownerOnlyTableIds.add(String(res.tableId));
}
if (!res.tableId && code === '~o structure') {
ownerOnlyStructure = true;
const clause = decodeClause(code);
if (clause) {
if (clause.kind === 'doc') {
ownerOnlyStructure = true;
}
if (clause.kind === 'table' && clause.tableId) {
ownerOnlyTableIds.add(clause.tableId);
}
}
}
return {ownerOnlyTableIds, ownerOnlyStructure};
@@ -63,10 +67,15 @@ export class AccessRules extends Disposable {
await tableData.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: "~o structure"}]);
await tableData.sendTableAction(['AddRecord', null, {tableId: "", colIds}]);
} else {
const rowId = tableData.findMatchingRowId({tableId: '', colIds: '~o structure'});
const rowId = tableData.findMatchingRowId({tableId: '', colIds});
if (rowId) {
await this._gristDoc.docModel.aclResources.sendTableAction(['RemoveRecord', rowId]);
}
@@ -78,11 +87,15 @@ export class AccessRules extends Disposable {
if (tablesAdded.size) {
await tableData.sendTableAction(['BulkAddRecord', arrayRepeat(tablesAdded.size, null), {
tableId: [...tablesAdded],
colIds: arrayRepeat(tablesAdded.size, "~o"),
colIds: [...tablesAdded].map(tableId => serializeClause({
kind: 'table',
tableId,
match: { kind: 'const', charId: 'Access', value: 'owners' },
})),
}]);
}
// Handle table removed from ownerOnlyTaleIds.
// 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);