gristlabs_grist-core/app/client/components/DocComm.ts
Dmitry S d8e742aa0d (core) Add getAclResources method for making all tables/columns available when editing ACL rules
Summary:
The goal is that those who can edit ACL rules can create or change rules for
any resource, even if the rules block their own ability to see the resource.

Test Plan: Added a browser test, and a server test for who can call the new method.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D2703
2021-01-14 13:43:55 -05:00

208 lines
8.6 KiB
TypeScript

import {Comm, CommMessage} from 'app/client/components/Comm';
import {reportError, UserError} from 'app/client/models/errors';
import {Notifier} from 'app/client/models/NotifyModel';
import {ActionGroup} from 'app/common/ActionGroup';
import {ActiveDocAPI, ApplyUAOptions, ApplyUAResult} from 'app/common/ActiveDocAPI';
import {DocAction, UserAction} from 'app/common/DocActions';
import {OpenLocalDocResult} from 'app/common/DocListAPI';
import {docUrl} from 'app/common/urlUtils';
import {Events as BackboneEvents} from 'backbone';
import {Disposable, Emitter} from 'grainjs';
// tslint:disable:no-console
export interface DocUserAction extends CommMessage {
docFD: number;
fromSelf?: boolean;
data: {
docActions: DocAction[];
actionGroup: ActionGroup;
};
}
const SLOW_NOTIFICATION_TIMEOUT_MS = 1000; // applies to user actions only
/**
* The type of data.methods object created by openDoc() in app/client/components/Comm.js.
* This is used in much of client-side code, and exposed firstly as GristDoc.docComm.
*/
export class DocComm extends Disposable implements ActiveDocAPI {
// These are all the methods of ActiveDocAPI. Listing them explicitly lets typescript verify
// that we haven't missed any.
// closeDoc has a special implementation below.
public fetchTable = this._wrapMethod("fetchTable");
public fetchTableSchema = this._wrapMethod("fetchTableSchema");
public useQuerySet = this._wrapMethod("useQuerySet");
public disposeQuerySet = this._wrapMethod("disposeQuerySet");
// applyUserActions has a special implementation below.
public applyUserActionsById = this._wrapMethod("applyUserActionsById");
public importFiles = this._wrapMethod("importFiles");
public finishImportFiles = this._wrapMethod("finishImportFiles");
public cancelImportFiles = this._wrapMethod("cancelImportFiles");
public addAttachments = this._wrapMethod("addAttachments");
public findColFromValues = this._wrapMethod("findColFromValues");
public getFormulaError = this._wrapMethod("getFormulaError");
public fetchURL = this._wrapMethod("fetchURL");
public autocomplete = this._wrapMethod("autocomplete");
public shareDoc = this._wrapMethod("shareDoc");
public removeInstanceFromDoc = this._wrapMethod("removeInstanceFromDoc");
public getActionSummaries = this._wrapMethod("getActionSummaries");
public startBundleUserActions = this._wrapMethod("startBundleUserActions");
public stopBundleUserActions = this._wrapMethod("stopBundleUserActions");
public forwardPluginRpc = this._wrapMethod("forwardPluginRpc");
public reloadPlugins = this._wrapMethod("reloadPlugins");
public reloadDoc = this._wrapMethod("reloadDoc");
public fork = this._wrapMethod("fork");
public checkAclFormula = this._wrapMethod("checkAclFormula");
public getAclResources = this._wrapMethod("getAclResources");
public changeUrlIdEmitter = this.autoDispose(new Emitter());
// We save the clientId that was used when opening the doc. If it changes (e.g. reconnecting to
// another server), it would be incorrect to use the new clientId without re-opening the doc
// (which is handled by App.ts). This way, Comm can protect against mismatched clientIds.
private _clientId: string;
private _docFD: number;
private _forkPromise: Promise<void>|null = null;
private _isClosed: boolean = false;
private listenTo: BackboneEvents['listenTo']; // set by Backbone
constructor(private _comm: Comm, openResponse: OpenLocalDocResult, private _docId: string,
private _notifier: Notifier) {
super();
this._setOpenResponse(openResponse);
// If *this* doc is shutdown forcibly (e.g. via reloadDoc call), mark it as closed, so we
// don't attempt to close it again.
this.listenTo(_comm, 'docShutdown', (m: CommMessage) => {
if (this.isActionFromThisDoc(m)) { this._isClosed = true; }
});
this.onDispose(() => this._shutdown());
}
// Returns the URL params that identifying this open document to the DocWorker
// (used e.g. in attachment and download URLs).
public getUrlParams(): {clientId: string, docFD: number} {
return { clientId: this._clientId, docFD: this._docFD };
}
// Completes a path by adding the correct worker host and prefix for this document.
// E.g. "/uploads" becomes "https://host.name/v/ver/o/org/uploads"
public docUrl(path: string) {
return docUrl(this.docWorkerUrl, path);
}
// Returns a base url to the worker serving the current document, e.g.
// "https://host.name/v/ver/"
public get docWorkerUrl() {
return this._comm.getDocWorkerUrl(this._docId);
}
// Returns whether a message received by this Comm object is for the current doc.
public isActionFromThisDoc(message: CommMessage): boolean {
return message.docFD === this._docFD;
}
/**
* Overrides applyUserActions() method to also add the UserActions to a list, for use in tests.
*/
public applyUserActions(actions: UserAction[], options?: ApplyUAOptions): Promise<ApplyUAResult> {
this._comm.addUserActions(actions);
return this._callMethod('applyUserActions', actions, options);
}
/**
* Overrides closeDoc() method to call to Comm directly, without triggering forking logic.
* This is important in particular since it may be called while forking.
*/
public closeDoc(): Promise<void> {
return this._callDocMethod('closeDoc');
}
/**
* Forks the document, making sure the url gets updated, and holding any actions
* until the fork is complete. If a fork has already been started/completed, this
* does nothing.
*/
public async forkAndUpdateUrl(): Promise<void> {
await (this._forkPromise || (this._forkPromise = this._doForkDoc()));
}
// Clean up connection after closing doc.
private async _shutdown() {
console.log(`DocComm: shutdown clientId ${this._clientId} docFD ${this._docFD}`);
try {
// Close the document to unsubscribe from further updates on it.
if (!this._isClosed) {
await this.closeDoc();
}
} catch (err) {
console.warn(`DocComm: closeDoc failed: ${err}`);
} finally {
if (!this._comm.isDisposed()) {
this._comm.releaseDocConnection(this._docId);
}
}
}
/**
* Store important information from the response to openDoc, and
* ensure we have a connection to a docWorker for the document
* identified by the current docId. the caller of _setOpenResponse
* should call _releaseDocConnection for any previous docId.
*/
private _setOpenResponse(openResponse: OpenLocalDocResult) {
this._docFD = openResponse.docFD;
this._clientId = openResponse.clientId;
this._comm.useDocConnection(this._docId);
}
private _wrapMethod<Name extends keyof ActiveDocAPI>(name: Name): ActiveDocAPI[Name] {
return this._callMethod.bind(this, name);
}
private async _callMethod(name: keyof ActiveDocAPI, ...args: any[]): Promise<any> {
return this._notifier.slowNotification(this._doCallMethod(name, ...args), SLOW_NOTIFICATION_TIMEOUT_MS);
}
private async _doCallMethod(name: keyof ActiveDocAPI, ...args: any[]): Promise<any> {
if (this._forkPromise) {
// If a fork is pending or has finished, call the method after waiting for it.
// (If we've gone through a fork, we will not consider forking again.)
await this._forkPromise;
return this._callDocMethod(name, ...args);
}
try {
return await this._callDocMethod(name, ...args);
} catch (err) {
// TODO should be the suggested fork id and fork user.
if (err.shouldFork) {
// If the server suggests to fork, do it now, or wait for the fork already pending.
await this.forkAndUpdateUrl();
return this._callDocMethod(name, ...args);
}
throw err;
}
}
private _callDocMethod(name: keyof ActiveDocAPI, ...args: any[]): Promise<any> {
return this._comm._makeRequest(this._clientId, this._docId, name, this._docFD, ...args);
}
private async _doForkDoc(): Promise<void> {
reportError(new UserError('Preparing your copy...', {key: 'forking'}));
const {urlId, docId} = await this.fork();
// TODO: may want to preserve linkParameters in call to openDoc.
const openResponse = await this._comm.openDoc(docId);
// Close the old doc and release the old connection. Note that the closeDoc call is expected
// to fail, since we close the websocket immediately after it. So let it fail silently.
this.closeDoc().catch(() => null);
this._comm.releaseDocConnection(this._docId);
this._docId = docId;
this._setOpenResponse(openResponse);
this.changeUrlIdEmitter.emit(urlId);
reportError(new UserError('You are now editing your own copy', {key: 'forking'}));
}
}
Object.assign(DocComm.prototype, BackboneEvents);