(core) implement cleaner row-level access control for outgoing messages

Summary:
This implements row-level access control for outgoing messages, replacing the document reloading placeholder that was there before.

 * Prior to broadcasting messages, GranularAccess is notified of actions+undo.
 * While broadcasting messages to different sessions, if we find we need row level access control information, rows before and after the change are reconstructed.
 * Messages are rewritten if rows that were previously forbidden are now allowed, and vice versa.

The diff is somewhat under-tested and under-optimized. Next step would be to implement row-level access control for incoming actions, which may result in some rejiggering of the code from this diff to avoid duplication of effort under some conditions.

Test Plan: added test

Reviewers: dsagal

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D2670
This commit is contained in:
Paul Fitzpatrick
2020-11-30 10:50:00 -05:00
parent c1c17bf54e
commit 0e2deecc55
8 changed files with 433 additions and 61 deletions

View File

@@ -82,7 +82,7 @@ const SCHEMA_ACTIONS = new Set(['AddTable', 'RemoveTable', 'RenameTable', 'AddCo
/**
* Determines whether a given action is a schema action or not.
*/
export function isSchemaAction(action: DocAction): boolean {
export function isSchemaAction(action: DocAction): action is AddTable | RemoveTable | RenameTable | AddColumn | RemoveColumn | RenameColumn | ModifyColumn {
return SCHEMA_ACTIONS.has(action[0]);
}

View File

@@ -16,8 +16,14 @@ type FetchTableFunc = (tableId: string) => Promise<TableDataAction>;
export class DocData extends ActionDispatcher {
private _tables: Map<string, TableData> = new Map();
constructor(private _fetchTableFunc: FetchTableFunc, metaTableData: {[tableId: string]: TableDataAction}) {
/**
* If metaTableData is not supplied, then any tables needed should be loaded manually,
* using syncTable(). All column types will be set to Any, which will affect default
* values.
*/
constructor(private _fetchTableFunc: FetchTableFunc, metaTableData: {[tableId: string]: TableDataAction} | null) {
super();
if (metaTableData === null) { return; }
// Create all meta tables, and populate data we already have.
for (const tableId in schema) {
if (schema.hasOwnProperty(tableId)) {
@@ -67,6 +73,17 @@ export class DocData extends ActionDispatcher {
return (!table.isLoaded || force) ? table.fetchData(this._fetchTableFunc) : Promise.resolve();
}
/**
* Fetches the data for tableId unconditionally, and without knowledge of its metadata.
* Columns will be assumed to have type 'Any'.
*/
public async syncTable(tableId: string): Promise<void> {
const tableData = await this._fetchTableFunc(tableId);
const colTypes = fromPairs(Object.keys(tableData[3]).map(c => [c, 'Any']));
colTypes.id = 'Any';
this._tables.set(tableId, this.createTableData(tableId, tableData, colTypes));
}
/**
* Handles an action received from the server, by forwarding it to the appropriate TableData
* object.

View File

@@ -304,6 +304,7 @@ export interface DocAPI {
getRows(tableId: string): Promise<TableColValues>;
updateRows(tableId: string, changes: TableColValues): Promise<number[]>;
addRows(tableId: string, additions: BulkColValues): Promise<number[]>;
removeRows(tableId: string, removals: number[]): Promise<number[]>;
replace(source: DocReplacementOptions): Promise<void>;
getSnapshots(): Promise<DocSnapshots>;
forceReload(): Promise<void>;
@@ -690,6 +691,13 @@ export class DocAPIImpl extends BaseAPI implements DocAPI {
});
}
public async removeRows(tableId: string, removals: number[]): Promise<number[]> {
return this.requestJson(`${this._url}/tables/${tableId}/data/delete`, {
body: JSON.stringify(removals),
method: 'POST'
});
}
public async replace(source: DocReplacementOptions): Promise<void> {
return this.requestJson(`${this._url}/replace`, {
body: JSON.stringify(source),