mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) process GristDocAPI calls from custom widgets in the client
Summary: Processing these calls in the client, rather than passing them on to the backend, means that access rules are more straightforward to apply. An unrelated fix is included to filter _grist_ tables when fetched individually - metadata could leak through this path. Test Plan: added tests Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2954
This commit is contained in:
parent
1d1a9297f8
commit
6b3ac07ca7
@ -10,10 +10,10 @@ import * as DataTableModel from 'app/client/models/DataTableModel';
|
|||||||
import { ViewFieldRec, ViewSectionRec } from 'app/client/models/DocModel';
|
import { ViewFieldRec, ViewSectionRec } from 'app/client/models/DocModel';
|
||||||
import { CustomViewSectionDef } from 'app/client/models/entities/ViewSectionRec';
|
import { CustomViewSectionDef } from 'app/client/models/entities/ViewSectionRec';
|
||||||
import {SortedRowSet} from 'app/client/models/rowset';
|
import {SortedRowSet} from 'app/client/models/rowset';
|
||||||
import { BulkColValues, RowRecord } from 'app/common/DocActions';
|
import {BulkColValues, fromTableDataAction, RowRecord} from 'app/common/DocActions';
|
||||||
import {extractInfoFromColType, reencodeAsAny} from 'app/common/gristTypes';
|
import {extractInfoFromColType, reencodeAsAny} from 'app/common/gristTypes';
|
||||||
import { PluginInstance } from 'app/common/PluginInstance';
|
import { PluginInstance } from 'app/common/PluginInstance';
|
||||||
import {GristView} from 'app/plugin/GristAPI';
|
import {GristDocAPI, GristView} from 'app/plugin/GristAPI';
|
||||||
import {Events as BackboneEvents} from 'backbone';
|
import {Events as BackboneEvents} from 'backbone';
|
||||||
import {MsgType, Rpc} from 'grain-rpc';
|
import {MsgType, Rpc} from 'grain-rpc';
|
||||||
import * as ko from 'knockout';
|
import * as ko from 'knockout';
|
||||||
@ -238,15 +238,23 @@ export class CustomView extends Disposable {
|
|||||||
const someAccess = (access !== 'none');
|
const someAccess = (access !== 'none');
|
||||||
const fullAccess = (access === 'full');
|
const fullAccess = (access === 'full');
|
||||||
|
|
||||||
// Create an Rpc object to manage messaging. If full access is granted,
|
// Create an Rpc object to manage messaging.
|
||||||
// allow forwarding to the back-end; otherwise restrict to APIs explicitly
|
const rpc = new Rpc({});
|
||||||
// made available here.
|
|
||||||
const rpc = fullAccess ? this.gristDoc.docPluginManager.makeAnonForwarder() :
|
|
||||||
new Rpc({});
|
|
||||||
// Now, we create a listener for message events (if access was granted), making sure
|
// Now, we create a listener for message events (if access was granted), making sure
|
||||||
// to respond only to messages from our iframe.
|
// to respond only to messages from our iframe.
|
||||||
const listener = someAccess ? (event: MessageEvent) => {
|
const listener = someAccess ? (event: MessageEvent) => {
|
||||||
if (event.source === iframe.contentWindow) {
|
if (event.source === iframe.contentWindow) {
|
||||||
|
// Previously, we forwarded messages targeted at "grist" to the back-end.
|
||||||
|
// Now, we process them immediately in the context of the client for access
|
||||||
|
// control purposes. To do that, any message that comes in with mdest of
|
||||||
|
// "grist" will have that destination wiped, and we provide a local
|
||||||
|
// implementation of the interface.
|
||||||
|
// It feels like it should be possible to deal with the mdest more cleanly,
|
||||||
|
// with a rpc.registerForwarder('grist', { ... }), but it seems somehow hard
|
||||||
|
// to call a locally registered interface of an rpc object?
|
||||||
|
if (event.data.mdest === 'grist') {
|
||||||
|
event.data.mdest = '';
|
||||||
|
}
|
||||||
rpc.receiveMessage(event.data);
|
rpc.receiveMessage(event.data);
|
||||||
if (event.data.mtype === MsgType.Ready) {
|
if (event.data.mtype === MsgType.Ready) {
|
||||||
// After, the "ready" message, send a notification with cursor
|
// After, the "ready" message, send a notification with cursor
|
||||||
@ -274,6 +282,34 @@ export class CustomView extends Disposable {
|
|||||||
fetchSelectedTable: () => this._getSelectedTable(),
|
fetchSelectedTable: () => this._getSelectedTable(),
|
||||||
fetchSelectedRecord: (rowId: number) => this._getSelectedRecord(rowId),
|
fetchSelectedRecord: (rowId: number) => this._getSelectedRecord(rowId),
|
||||||
});
|
});
|
||||||
|
// Add a GristDocAPI implementation. Apart from getDocName (which I think
|
||||||
|
// is from a time before names and ids diverged, so I'm not actually sure
|
||||||
|
// what it should return), require full access since the methods can view/edit
|
||||||
|
// parts of the document beyond the table the widget is associated with.
|
||||||
|
// Access rights will be that of the user viewing the document.
|
||||||
|
// TODO: add something to calls to identify the origin, so it could be
|
||||||
|
// controlled by access rules if desired.
|
||||||
|
const assertFullAccess = () => {
|
||||||
|
if (!fullAccess) { throw new Error('full access not granted'); }
|
||||||
|
};
|
||||||
|
rpc.registerImpl<GristDocAPI>('GristDocAPI', {
|
||||||
|
getDocName: () => this.gristDoc.docId,
|
||||||
|
listTables: async () => {
|
||||||
|
assertFullAccess();
|
||||||
|
// Could perhaps read tableIds from this.gristDoc.docModel.allTableIds.all()?
|
||||||
|
const tables = await this.gristDoc.docComm.fetchTable('_grist_Tables');
|
||||||
|
// Tables the user doesn't have access to are just blanked out.
|
||||||
|
return tables[3].tableId.filter(tableId => tableId !== '');
|
||||||
|
},
|
||||||
|
fetchTable: async (tableId: string) => {
|
||||||
|
assertFullAccess();
|
||||||
|
return fromTableDataAction(await this.gristDoc.docComm.fetchTable(tableId));
|
||||||
|
},
|
||||||
|
applyUserActions: (actions: any[][]) => {
|
||||||
|
assertFullAccess();
|
||||||
|
return this.gristDoc.docComm.applyUserActions(actions, {desc: undefined});
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// Direct messages to /dev/null otherwise. Important to setSendMessage
|
// Direct messages to /dev/null otherwise. Important to setSendMessage
|
||||||
// or they will be queued indefinitely.
|
// or they will be queued indefinitely.
|
||||||
@ -285,7 +321,8 @@ export class CustomView extends Disposable {
|
|||||||
// There's an existing RPC object we are replacing.
|
// There's an existing RPC object we are replacing.
|
||||||
// Unregister anything that may have been registered previously.
|
// Unregister anything that may have been registered previously.
|
||||||
// TODO: add a way to clean up more systematically to grain-rpc.
|
// TODO: add a way to clean up more systematically to grain-rpc.
|
||||||
this._rpc.unregisterForwarder('*');
|
this._rpc.unregisterForwarder('grist');
|
||||||
|
this._rpc.unregisterImpl('GristDocAPI');
|
||||||
this._rpc.unregisterImpl('GristView');
|
this._rpc.unregisterImpl('GristView');
|
||||||
}
|
}
|
||||||
this._rpc = rpc;
|
this._rpc = rpc;
|
||||||
|
@ -621,6 +621,20 @@ export class ActiveDoc extends EventEmitter {
|
|||||||
|
|
||||||
this._granularAccess.assertCanRead(tableAccess);
|
this._granularAccess.assertCanRead(tableAccess);
|
||||||
|
|
||||||
|
if (query.tableId.startsWith('_gristsys_')) {
|
||||||
|
throw new Error('Cannot fetch _gristsys tables');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.tableId.startsWith('_grist_') && !await this._granularAccess.canReadEverything(docSession)) {
|
||||||
|
// Metadata tables may need filtering, and this can't be done by looking at a single
|
||||||
|
// table. So we pick out the table we want from fetchMetaTables (which has applied
|
||||||
|
// filtering).
|
||||||
|
const tables = await this.fetchMetaTables(docSession);
|
||||||
|
const table = tables[query.tableId];
|
||||||
|
if (table) { return table; }
|
||||||
|
// If table not found, continue, to give a consistent error for a table not found.
|
||||||
|
}
|
||||||
|
|
||||||
// Some tests read _grist_ tables via the api. The _fetchQueryFromDB method
|
// Some tests read _grist_ tables via the api. The _fetchQueryFromDB method
|
||||||
// currently cannot read those tables, so we load them from the data engine
|
// currently cannot read those tables, so we load them from the data engine
|
||||||
// when ready.
|
// when ready.
|
||||||
|
@ -636,7 +636,7 @@ export class GranularAccess implements GranularAccessForBundle {
|
|||||||
// If user has right to read everything, return immediately.
|
// If user has right to read everything, return immediately.
|
||||||
if (await this.canReadEverything(docSession)) { return tables; }
|
if (await this.canReadEverything(docSession)) { return tables; }
|
||||||
// If we are going to modify metadata, make a copy.
|
// If we are going to modify metadata, make a copy.
|
||||||
tables = JSON.parse(JSON.stringify(tables));
|
tables = cloneDeep(tables);
|
||||||
|
|
||||||
const permInfo = await this._getAccess(docSession);
|
const permInfo = await this._getAccess(docSession);
|
||||||
const censor = new CensorshipInfo(permInfo, this._ruler.ruleCollection, tables,
|
const censor = new CensorshipInfo(permInfo, this._ruler.ruleCollection, tables,
|
||||||
|
Loading…
Reference in New Issue
Block a user