mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Checks that an ACL formula can be parsed, and prevent saving unparsable ACL rules.
Summary: - Fix error-handling in bundleActions(), and wait for the full bundle to complete. (The omissions here were making it impossibly to react to errors from inside bundleActions()) - Catch problematic rules early enough to undo them, by trying out ruleCollection.update() on updated rules before the updates are applied. - Added checkAclFormula() call to DocComm that checks parsing and compiling formula, and reports errors. - In UI, prevent saving if any aclFormulas are invalid, or while waiting for the to get checked. - Also fixed some lint errors Test Plan: Added a test case of error reporting in ACL formulas. Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D2689
This commit is contained in:
@@ -36,6 +36,7 @@ import {UploadResult} from 'app/common/uploads';
|
||||
import {DocReplacementOptions, DocState} from 'app/common/UserAPI';
|
||||
import {ParseOptions} from 'app/plugin/FileParserAPI';
|
||||
import {GristDocAPI} from 'app/plugin/GristAPI';
|
||||
import {compileAclFormula} from 'app/server/lib/ACLFormula';
|
||||
import {Authorizer} from 'app/server/lib/Authorizer';
|
||||
import {checksumFile} from 'app/server/lib/checksumFile';
|
||||
import {Client} from 'app/server/lib/Client';
|
||||
@@ -898,6 +899,24 @@ export class ActiveDoc extends EventEmitter {
|
||||
return makeForkIds({userId, isAnonymous, trunkDocId, trunkUrlId});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an ACL formula is valid. If not, will throw an error with an explanation.
|
||||
*/
|
||||
public async checkAclFormula(docSession: DocSession, text: string): Promise<void> {
|
||||
// Checks can leak names of tables and columns.
|
||||
if (!await this._granularAccess.canReadEverything(docSession)) { return; }
|
||||
await this.waitForInitialization();
|
||||
try {
|
||||
const parsedAclFormula = await this._pyCall('parse_acl_formula', text);
|
||||
compileAclFormula(parsedAclFormula);
|
||||
// TODO We also need to check the validity of attributes, and of tables and columns
|
||||
// mentioned in resources and userAttribute rules.
|
||||
} catch (e) {
|
||||
e.message = e.message?.replace('[Sandbox] ', '');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public getGristDocAPI(): GristDocAPI {
|
||||
return this.docPluginManager.gristDocAPI;
|
||||
}
|
||||
|
||||
@@ -110,6 +110,7 @@ export class DocWorker {
|
||||
getActionSummaries: activeDocMethod.bind(null, 'viewers', 'getActionSummaries'),
|
||||
reloadDoc: activeDocMethod.bind(null, 'editors', 'reloadDoc'),
|
||||
fork: activeDocMethod.bind(null, 'viewers', 'fork'),
|
||||
checkAclFormula: activeDocMethod.bind(null, 'viewers', 'checkAclFormula'),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,11 @@ import { ACLRuleCollection } from 'app/common/ACLRuleCollection';
|
||||
import { ActionGroup } from 'app/common/ActionGroup';
|
||||
import { createEmptyActionSummary } from 'app/common/ActionSummary';
|
||||
import { Query } from 'app/common/ActiveDocAPI';
|
||||
import { ApiError } from 'app/common/ApiError';
|
||||
import { AsyncCreate } from 'app/common/AsyncCreate';
|
||||
import { AddRecord, BulkAddRecord, BulkColValues, BulkRemoveRecord, BulkUpdateRecord, CellValue,
|
||||
ColValues, DocAction, getTableId, isSchemaAction, RemoveRecord, ReplaceTableData, UpdateRecord } from 'app/common/DocActions';
|
||||
import { AddRecord, BulkAddRecord, BulkColValues, BulkRemoveRecord, BulkUpdateRecord } from 'app/common/DocActions';
|
||||
import { RemoveRecord, ReplaceTableData, UpdateRecord } from 'app/common/DocActions';
|
||||
import { CellValue, ColValues, DocAction, getTableId, isSchemaAction } from 'app/common/DocActions';
|
||||
import { TableDataAction, UserAction } from 'app/common/DocActions';
|
||||
import { DocData } from 'app/common/DocData';
|
||||
import { ErrorWithCode } from 'app/common/ErrorWithCode';
|
||||
@@ -109,7 +111,10 @@ export class GranularAccess {
|
||||
// Flag tracking whether a set of actions have been applied to the database or not.
|
||||
private _applied: boolean = false;
|
||||
|
||||
public constructor(private _docData: DocData, private _fetchQueryFromDB: (query: Query) => Promise<TableDataAction>, private _recoveryMode: boolean) {
|
||||
public constructor(
|
||||
private _docData: DocData,
|
||||
private _fetchQueryFromDB: (query: Query) => Promise<TableDataAction>,
|
||||
private _recoveryMode: boolean) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,10 +150,32 @@ export class GranularAccess {
|
||||
*/
|
||||
public async canApplyDocActions(docSession: OptDocSession, docActions: DocAction[], undo: DocAction[]) {
|
||||
this._applied = false;
|
||||
if (!this._ruleCollection.haveRules()) { return; }
|
||||
this._prepareRowSnapshots(docActions, undo);
|
||||
await Promise.all(
|
||||
docActions.map((action, idx) => this._checkIncomingDocAction(docSession, action, idx)));
|
||||
if (this._ruleCollection.haveRules()) {
|
||||
this._prepareRowSnapshots(docActions, undo);
|
||||
await Promise.all(
|
||||
docActions.map((action, idx) => this._checkIncomingDocAction(docSession, action, idx)));
|
||||
}
|
||||
|
||||
// If the actions change any rules, verify that we'll be able to handle the changed rules. If
|
||||
// they are to cause an error, reject the action to avoid forcing user into recovery mode.
|
||||
if (docActions.some(docAction => ['_grist_ACLRules', '_grist_Resources'].includes(getTableId(docAction)))) {
|
||||
// Create a tmpDocData with just the tables we care about, then update docActions to it.
|
||||
const tmpDocData: DocData = new DocData(
|
||||
(tableId) => { throw new Error("Unexpected DocData fetch"); }, {
|
||||
_grist_ACLResources: this._docData.getTable('_grist_ACLResources')!.getTableDataAction(),
|
||||
_grist_ACLRules: this._docData.getTable('_grist_ACLRules')!.getTableDataAction(),
|
||||
});
|
||||
for (const da of docActions) {
|
||||
tmpDocData.receiveAction(da);
|
||||
}
|
||||
|
||||
// Use the post-actions data to process the rules collection, and throw error if that fails.
|
||||
const ruleCollection = new ACLRuleCollection();
|
||||
await ruleCollection.update(tmpDocData, {log, compile: compileAclFormula});
|
||||
if (ruleCollection.ruleError && !this._recoveryMode) {
|
||||
throw new ApiError(ruleCollection.ruleError.message, 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -164,7 +191,8 @@ export class GranularAccess {
|
||||
this._applied = true;
|
||||
// If there is a rule change, redo from scratch for now.
|
||||
// TODO: this is placeholder code. Should deal with connected clients.
|
||||
if (docActions.some(docAction => getTableId(docAction) === '_grist_ACLRules' || getTableId(docAction) === '_grist_Resources')) {
|
||||
if (docActions.some(docAction => getTableId(docAction) === '_grist_ACLRules' ||
|
||||
getTableId(docAction) === '_grist_Resources')) {
|
||||
await this.update();
|
||||
return;
|
||||
}
|
||||
@@ -656,7 +684,8 @@ export class GranularAccess {
|
||||
}
|
||||
|
||||
// Compute which of the row ids supplied are for rows forbidden for this session.
|
||||
private async _getForbiddenRows(docSession: OptDocSession, data: TableDataAction, ids: Set<number>): Promise<number[]> {
|
||||
private async _getForbiddenRows(docSession: OptDocSession, data: TableDataAction, ids: Set<number>):
|
||||
Promise<number[]> {
|
||||
const rec = new RecordView(data, undefined);
|
||||
const input: AclMatchInput = {user: await this._getUser(docSession), rec};
|
||||
|
||||
@@ -795,7 +824,10 @@ export class GranularAccess {
|
||||
try {
|
||||
// Use lodash's get() that supports paths, e.g. charId of 'a.b' would look up `user.a.b`.
|
||||
// TODO: add indexes to db.
|
||||
rows = await this._fetchQueryFromDB({tableId: clause.tableId, filters: { [clause.lookupColId]: [get(user, clause.charId)] }});
|
||||
rows = await this._fetchQueryFromDB({
|
||||
tableId: clause.tableId,
|
||||
filters: { [clause.lookupColId]: [get(user, clause.charId)] }
|
||||
});
|
||||
} catch (e) {
|
||||
log.warn(`User attribute ${clause.name} failed`, e);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import {WorkCoordinator} from './WorkCoordinator';
|
||||
// processing hub actions or rebasing.
|
||||
interface UserRequest {
|
||||
action: UserActionBundle;
|
||||
docSession: OptDocSession|null,
|
||||
docSession: OptDocSession|null;
|
||||
resolve(result: UserResult): void;
|
||||
reject(err: Error): void;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user