mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) make user role available in ActiveDoc methods
Summary: This makes the user's role (owner/editor/viewer) available in ActiveDoc methods. No use of that information is made yet, other than to log it. The bulk of the diff is getting a handle on the various ways the methods can be called, and systematizing it a bit more. In passing, access control is added to broadcasts of document changes, so users who no longer have access to a document do not receive changes if they still have the document open. Test Plan: existing tests pass; test for broadcast access control added Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2599
This commit is contained in:
parent
7a8debae16
commit
526fda4eba
@ -7,7 +7,6 @@
|
||||
import * as assert from 'assert';
|
||||
import * as bluebird from 'bluebird';
|
||||
import {EventEmitter} from 'events';
|
||||
import {Request} from 'express';
|
||||
import {IMessage, MsgType} from 'grain-rpc';
|
||||
import * as imageSize from 'image-size';
|
||||
import flatten = require('lodash/flatten');
|
||||
@ -36,7 +35,7 @@ import {UploadResult} from 'app/common/uploads';
|
||||
import {DocReplacementOptions} from 'app/common/UserAPI';
|
||||
import {ParseOptions} from 'app/plugin/FileParserAPI';
|
||||
import {GristDocAPI} from 'app/plugin/GristAPI';
|
||||
import {Authorizer, getUserId} from 'app/server/lib/Authorizer';
|
||||
import {Authorizer} from 'app/server/lib/Authorizer';
|
||||
import {checksumFile} from 'app/server/lib/checksumFile';
|
||||
import {Client} from 'app/server/lib/Client';
|
||||
import {DEFAULT_CACHE_TTL, DocManager} from 'app/server/lib/DocManager';
|
||||
@ -52,7 +51,8 @@ import {ActionHistoryImpl} from './ActionHistoryImpl';
|
||||
import {ActiveDocImport} from './ActiveDocImport';
|
||||
import {DocClients} from './DocClients';
|
||||
import {DocPluginManager} from './DocPluginManager';
|
||||
import {DocSession, OptDocSession} from './DocSession';
|
||||
import {DocSession, getDocSessionAccess, getDocSessionUserId, makeExceptionalDocSession,
|
||||
OptDocSession} from './DocSession';
|
||||
import {DocStorage} from './DocStorage';
|
||||
import {expandQuery} from './ExpandedQuery';
|
||||
import {OnDemandActions} from './OnDemandActions';
|
||||
@ -147,16 +147,18 @@ export class ActiveDoc extends EventEmitter {
|
||||
public get docName(): string { return this._docName; }
|
||||
|
||||
// Helpers to log a message along with metadata about the request.
|
||||
public logDebug(c: Client|OptDocSession|null, msg: string, ...args: any[]) { this._log('debug', c, msg, ...args); }
|
||||
public logInfo(c: Client|OptDocSession|null, msg: string, ...args: any[]) { this._log('info', c, msg, ...args); }
|
||||
public logWarn(c: Client|OptDocSession|null, msg: string, ...args: any[]) { this._log('warn', c, msg, ...args); }
|
||||
public logError(c: Client|OptDocSession|null, msg: string, ...args: any[]) { this._log('error', c, msg, ...args); }
|
||||
public logDebug(s: OptDocSession, msg: string, ...args: any[]) { this._log('debug', s, msg, ...args); }
|
||||
public logInfo(s: OptDocSession, msg: string, ...args: any[]) { this._log('info', s, msg, ...args); }
|
||||
public logWarn(s: OptDocSession, msg: string, ...args: any[]) { this._log('warn', s, msg, ...args); }
|
||||
public logError(s: OptDocSession, msg: string, ...args: any[]) { this._log('error', s, msg, ...args); }
|
||||
|
||||
// Constructs metadata for logging, given a Client or an OptDocSession.
|
||||
public getLogMeta(cli: Client|OptDocSession|null, docMethod?: string): log.ILogMeta {
|
||||
const client = cli && ('getProfile' in cli ? cli : cli.client);
|
||||
public getLogMeta(docSession: OptDocSession, docMethod?: string): log.ILogMeta {
|
||||
const client = docSession.client;
|
||||
const access = getDocSessionAccess(docSession);
|
||||
return {
|
||||
docId: this._docName,
|
||||
access,
|
||||
...(docMethod ? {docMethod} : {}),
|
||||
...(client ? client.getLogMeta() : {}),
|
||||
};
|
||||
@ -183,9 +185,9 @@ export class ActiveDoc extends EventEmitter {
|
||||
* earliest actions first, later actions later. If `summarize` is set,
|
||||
* action summaries are computed and included.
|
||||
*/
|
||||
public async getRecentActions(client: Client|null, summarize: boolean): Promise<ActionGroup[]> {
|
||||
public async getRecentActions(docSession: OptDocSession, summarize: boolean): Promise<ActionGroup[]> {
|
||||
const actions = await this._actionHistory.getRecentActions(MAX_RECENT_ACTIONS);
|
||||
return actions.map(act => asActionGroup(this._actionHistory, act, {client, summarize}));
|
||||
return actions.map(act => asActionGroup(this._actionHistory, act, {client: docSession.client, summarize}));
|
||||
}
|
||||
|
||||
/** expose action history for tests */
|
||||
@ -204,7 +206,7 @@ export class ActiveDoc extends EventEmitter {
|
||||
|
||||
// If we had a shutdown scheduled, unschedule it.
|
||||
if (this._inactivityTimer.isEnabled()) {
|
||||
this.logInfo(client, "will stay open");
|
||||
this.logInfo(docSession, "will stay open");
|
||||
this._inactivityTimer.disable();
|
||||
}
|
||||
return docSession;
|
||||
@ -215,11 +217,12 @@ export class ActiveDoc extends EventEmitter {
|
||||
* @returns {Promise} Promise for when database and data engine are done shutting down.
|
||||
*/
|
||||
public async shutdown(removeThisActiveDoc: boolean = true): Promise<void> {
|
||||
this.logDebug(null, "shutdown starting");
|
||||
const docSession = makeExceptionalDocSession('system');
|
||||
this.logDebug(docSession, "shutdown starting");
|
||||
this._inactivityTimer.disable();
|
||||
if (this.docClients.clientCount() > 0) {
|
||||
this.logWarn(null, `Doc being closed with ${this.docClients.clientCount()} clients left`);
|
||||
this.docClients.broadcastDocMessage(null, 'docShutdown', null);
|
||||
this.logWarn(docSession, `Doc being closed with ${this.docClients.clientCount()} clients left`);
|
||||
await this.docClients.broadcastDocMessage(null, 'docShutdown', null);
|
||||
this.docClients.removeAllClients();
|
||||
}
|
||||
|
||||
@ -246,9 +249,9 @@ export class ActiveDoc extends EventEmitter {
|
||||
} catch (err) {
|
||||
// Initialization errors do not matter at this point.
|
||||
}
|
||||
this.logDebug(null, "shutdown complete");
|
||||
this.logDebug(docSession, "shutdown complete");
|
||||
} catch (err) {
|
||||
this.logError(null, "failed to shutdown some resources", err);
|
||||
this.logError(docSession, "failed to shutdown some resources", err);
|
||||
}
|
||||
}
|
||||
|
||||
@ -289,7 +292,7 @@ export class ActiveDoc extends EventEmitter {
|
||||
docSession);
|
||||
if (isNew) {
|
||||
await this.createDoc(docSession);
|
||||
await this.addInitialTable();
|
||||
await this.addInitialTable(docSession);
|
||||
} else {
|
||||
await this.docStorage.openFile();
|
||||
const tableNames = await this._loadOpenDoc(docSession);
|
||||
@ -361,7 +364,7 @@ export class ActiveDoc extends EventEmitter {
|
||||
*/
|
||||
public async _initDoc(docSession: OptDocSession|null): Promise<void> {
|
||||
const metaTableData = await this._dataEngine.pyCall('fetch_meta_tables');
|
||||
this.docData = new DocData(tableId => this.fetchTable(null, tableId), metaTableData);
|
||||
this.docData = new DocData(tableId => this.fetchTable(makeExceptionalDocSession('system'), tableId), metaTableData);
|
||||
this._onDemandActions = new OnDemandActions(this.docStorage, this.docData);
|
||||
|
||||
await this._actionHistory.initialize();
|
||||
@ -377,8 +380,8 @@ export class ActiveDoc extends EventEmitter {
|
||||
/**
|
||||
* Adds a small table to start off a newly-created blank document.
|
||||
*/
|
||||
public addInitialTable() {
|
||||
return this._applyUserActions(null, [["AddEmptyTable"]]);
|
||||
public addInitialTable(docSession: OptDocSession) {
|
||||
return this._applyUserActions(docSession, [["AddEmptyTable"]]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -437,18 +440,13 @@ export class ActiveDoc extends EventEmitter {
|
||||
* This function saves attachments from a given upload and creates an entry for them in the database.
|
||||
* It returns the list of rowIds for the rows created in the _grist_Attachments table.
|
||||
*/
|
||||
public async addAttachments(docSessOrReq: DocSession|Request, uploadId: number): Promise<number[]> {
|
||||
// TODO Refactor to accept Request generally when DocSession is absent (for API calls), and
|
||||
// include user/org info in logging too.
|
||||
const [docSession, userId] = ('authorizer' in docSessOrReq ?
|
||||
[docSessOrReq, docSessOrReq.authorizer.getUserId()] :
|
||||
[{client: null}, getUserId(docSessOrReq)]);
|
||||
|
||||
public async addAttachments(docSession: OptDocSession, uploadId: number): Promise<number[]> {
|
||||
const userId = getDocSessionUserId(docSession);
|
||||
const upload: UploadInfo = globalUploadSet.getUploadInfo(uploadId, this.makeAccessId(userId));
|
||||
try {
|
||||
const userActions: UserAction[] = await Promise.all(
|
||||
upload.files.map(file => this._prepAttachment(docSession, file)));
|
||||
const result = await this._applyUserActions(docSession.client, userActions);
|
||||
const result = await this._applyUserActions(docSession, userActions);
|
||||
return result.retValues;
|
||||
} finally {
|
||||
await globalUploadSet.cleanup(uploadId);
|
||||
@ -479,18 +477,18 @@ export class ActiveDoc extends EventEmitter {
|
||||
* field of the _grist_Attachments table).
|
||||
* @returns {Promise<Buffer>} Promise for the data of this attachment; rejected on error.
|
||||
*/
|
||||
public async getAttachmentData(client: Client|null, fileIdent: string): Promise<Buffer> {
|
||||
public async getAttachmentData(docSession: OptDocSession, fileIdent: string): Promise<Buffer> {
|
||||
const data = await this.docStorage.getFileData(fileIdent);
|
||||
if (!data) { throw new ApiError("Invalid attachment identifier", 404); }
|
||||
this.logInfo(client, "getAttachment: %s -> %s bytes", fileIdent, data.length);
|
||||
this.logInfo(docSession, "getAttachment: %s -> %s bytes", fileIdent, data.length);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the meta tables to return to the client when first opening a document.
|
||||
*/
|
||||
public async fetchMetaTables(client: Client) {
|
||||
this.logInfo(client, "fetchMetaTables");
|
||||
public async fetchMetaTables(docSession: OptDocSession) {
|
||||
this.logInfo(docSession, "fetchMetaTables");
|
||||
if (!this.docData) { throw new Error("No doc data"); }
|
||||
// Get metadata from local cache rather than data engine, so that we can
|
||||
// still get it even if data engine is busy calculating.
|
||||
@ -521,7 +519,7 @@ export class ActiveDoc extends EventEmitter {
|
||||
* @returns {Promise} Promise for the TableData object, which is a BulkAddRecord-like array of the
|
||||
* form of the form ["TableData", table_id, row_ids, column_values].
|
||||
*/
|
||||
public async fetchTable(docSession: DocSession|null, tableId: string,
|
||||
public async fetchTable(docSession: OptDocSession, tableId: string,
|
||||
waitForFormulas: boolean = false): Promise<TableDataAction> {
|
||||
this.logInfo(docSession, "fetchTable(%s, %s)", docSession, tableId);
|
||||
return this.fetchQuery(docSession, {tableId, filters: {}}, waitForFormulas);
|
||||
@ -534,7 +532,7 @@ export class ActiveDoc extends EventEmitter {
|
||||
* @param {Boolean} waitForFormulas: If true, wait for all data to be loaded/calculated. If false,
|
||||
* special "pending" values may be returned.
|
||||
*/
|
||||
public async fetchQuery(docSession: DocSession|null, query: Query,
|
||||
public async fetchQuery(docSession: OptDocSession, query: Query,
|
||||
waitForFormulas: boolean = false): Promise<TableDataAction> {
|
||||
this._inactivityTimer.ping(); // The doc is in active use; ping it to stay open longer.
|
||||
|
||||
@ -581,7 +579,7 @@ export class ActiveDoc extends EventEmitter {
|
||||
* Makes a query (documented elsewhere) and subscribes to it, so that the client receives
|
||||
* docActions that affect this query's results.
|
||||
*/
|
||||
public async useQuerySet(docSession: DocSession, query: Query): Promise<QueryResult> {
|
||||
public async useQuerySet(docSession: OptDocSession, query: Query): Promise<QueryResult> {
|
||||
this.logInfo(docSession, "useQuerySet(%s, %s)", docSession, query);
|
||||
// TODO implement subscribing to the query.
|
||||
// - Convert tableId+colIds to TableData/ColData references
|
||||
@ -651,7 +649,7 @@ export class ActiveDoc extends EventEmitter {
|
||||
// there'll be a deadlock.
|
||||
await this.waitForInitialization();
|
||||
const newOptions = {linkId: docSession.linkId, ...options};
|
||||
const result: ApplyUAResult = await this._applyUserActions(docSession.client, actions, newOptions);
|
||||
const result: ApplyUAResult = await this._applyUserActions(docSession, actions, newOptions);
|
||||
docSession.linkId = docSession.shouldBundleActions ? result.actionNum : 0;
|
||||
return result;
|
||||
}
|
||||
@ -726,11 +724,11 @@ export class ActiveDoc extends EventEmitter {
|
||||
|
||||
public async removeInstanceFromDoc(docSession: DocSession): Promise<void> {
|
||||
const instanceId = await this._sharing.removeInstanceFromDoc();
|
||||
await this._applyUserActions(docSession.client, [['RemoveInstance', instanceId]]);
|
||||
await this._applyUserActions(docSession, [['RemoveInstance', instanceId]]);
|
||||
}
|
||||
|
||||
public async renameDocTo(client: Client, newName: string): Promise<void> {
|
||||
this.logDebug(client, 'renameDoc', newName);
|
||||
public async renameDocTo(docSession: OptDocSession, newName: string): Promise<void> {
|
||||
this.logDebug(docSession, 'renameDoc', newName);
|
||||
await this.docStorage.renameDocTo(newName);
|
||||
this._docName = newName;
|
||||
}
|
||||
@ -812,7 +810,7 @@ export class ActiveDoc extends EventEmitter {
|
||||
|
||||
// Get recent actions in ActionGroup format with summaries included.
|
||||
public async getActionSummaries(docSession: DocSession): Promise<ActionGroup[]> {
|
||||
return this.getRecentActions(docSession.client, true);
|
||||
return this.getRecentActions(docSession, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -882,10 +880,22 @@ export class ActiveDoc extends EventEmitter {
|
||||
return this._docManager.makeAccessId(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast document changes to all the document's clients. Doesn't involve
|
||||
* ActiveDoc directly, but placed here to facilitate future work on granular
|
||||
* access control.
|
||||
*/
|
||||
public async broadcastDocUpdate(client: Client|null, type: string, message: {
|
||||
actionGroup: ActionGroup,
|
||||
docActions: DocAction[]
|
||||
}) {
|
||||
await this.docClients.broadcastDocMessage(client, 'docUserAction', message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an open document from DocStorage. Returns a list of the tables it contains.
|
||||
*/
|
||||
protected async _loadOpenDoc(docSession: OptDocSession|null): Promise<string[]> {
|
||||
protected async _loadOpenDoc(docSession: OptDocSession): Promise<string[]> {
|
||||
// Fetch the schema version of document and sandbox, and migrate if the sandbox is newer.
|
||||
const [schemaVersion, docInfoData] = await Promise.all([
|
||||
this._dataEngine.pyCall('get_version'),
|
||||
@ -947,9 +957,10 @@ export class ActiveDoc extends EventEmitter {
|
||||
* isModification: true if document was changed by one or more actions.
|
||||
* }
|
||||
*/
|
||||
protected async _applyUserActions(client: Client|null, actions: UserAction[],
|
||||
protected async _applyUserActions(docSession: OptDocSession, actions: UserAction[],
|
||||
options: ApplyUAOptions = {}): Promise<ApplyUAResult> {
|
||||
this.logDebug(client, "_applyUserActions(%s, %s)", client, shortDesc(actions));
|
||||
const client = docSession.client;
|
||||
this.logDebug(docSession, "_applyUserActions(%s, %s)", client, shortDesc(actions));
|
||||
this._inactivityTimer.ping(); // The doc is in active use; ping it to stay open longer.
|
||||
|
||||
const user = client && client.session ? (await client.session.getEmail()) : "";
|
||||
@ -970,7 +981,7 @@ export class ActiveDoc extends EventEmitter {
|
||||
const result: ApplyUAResult = await new Promise<ApplyUAResult>(
|
||||
(resolve, reject) =>
|
||||
this._sharing!.addUserAction({action, client, resolve, reject}));
|
||||
this.logDebug(client, "_applyUserActions returning %s", util.inspect(result));
|
||||
this.logDebug(docSession, "_applyUserActions returning %s", util.inspect(result));
|
||||
|
||||
if (result.isModification) {
|
||||
this._fetchCache.clear(); // This could be more nuanced.
|
||||
@ -1024,7 +1035,7 @@ export class ActiveDoc extends EventEmitter {
|
||||
* expect different schema versions. The returned actions at the moment aren't even shared with
|
||||
* collaborators.
|
||||
*/
|
||||
private async _migrate(docSession: OptDocSession|null): Promise<void> {
|
||||
private async _migrate(docSession: OptDocSession): Promise<void> {
|
||||
// TODO: makeBackup should possibly be in docManager directly.
|
||||
const backupPath = await this._docManager.storageManager.makeBackup(this._docName, "migrate");
|
||||
this.logInfo(docSession, "_migrate: backup made at %s", backupPath);
|
||||
@ -1069,7 +1080,7 @@ export class ActiveDoc extends EventEmitter {
|
||||
private async _finishInitialization(docSession: OptDocSession, pendingTableNames: string[], startTime: number) {
|
||||
try {
|
||||
await this._loadTables(docSession, pendingTableNames);
|
||||
await this._applyUserActions(null, [['Calculate']]);
|
||||
await this._applyUserActions(docSession, [['Calculate']]);
|
||||
await this._reportDataEngineMemory();
|
||||
this._fullyLoaded = true;
|
||||
const endTime = Date.now();
|
||||
@ -1115,8 +1126,8 @@ export class ActiveDoc extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
private _log(level: string, cli: Client|OptDocSession|null, msg: string, ...args: any[]) {
|
||||
log.origLog(level, `ActiveDoc ` + msg, ...args, this.getLogMeta(cli));
|
||||
private _log(level: string, docSession: OptDocSession, msg: string, ...args: any[]) {
|
||||
log.origLog(level, `ActiveDoc ` + msg, ...args, this.getLogMeta(docSession));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -340,18 +340,26 @@ export interface ResourceSummary {
|
||||
|
||||
/**
|
||||
*
|
||||
* Handle authorization for a single resource accessed by a given user.
|
||||
* Handle authorization for a single document accessed by a given user.
|
||||
*
|
||||
*/
|
||||
export interface Authorizer {
|
||||
// get the id of user, or null if no authorization in place.
|
||||
getUserId(): number|null;
|
||||
|
||||
// get the id of the document.
|
||||
getDocId(): string;
|
||||
|
||||
// Fetch the doc metadata from HomeDBManager.
|
||||
getDoc(): Promise<Document>;
|
||||
|
||||
// Check access, throw error if the requested level of access isn't available.
|
||||
assertAccess(role: 'viewers'|'editors'): Promise<void>;
|
||||
|
||||
// Get the lasted access information calculated for the doc. This is useful
|
||||
// for logging - but access control itself should use assertAccess() to
|
||||
// ensure the data is fresh.
|
||||
getCachedAuth(): DocAuthResult;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -364,6 +372,7 @@ export class DocAuthorizer implements Authorizer {
|
||||
private _dbManager: HomeDBManager,
|
||||
private _key: DocAuthKey,
|
||||
public readonly openMode: OpenDocMode,
|
||||
private _docAuth?: DocAuthResult
|
||||
) {
|
||||
}
|
||||
|
||||
@ -371,21 +380,40 @@ export class DocAuthorizer implements Authorizer {
|
||||
return this._key.userId;
|
||||
}
|
||||
|
||||
public getDocId(): string {
|
||||
// We've been careful to require urlId === docId, see DocManager.
|
||||
return this._key.urlId;
|
||||
}
|
||||
|
||||
public async getDoc(): Promise<Document> {
|
||||
return this._dbManager.getDoc(this._key);
|
||||
}
|
||||
|
||||
public async assertAccess(role: 'viewers'|'editors'): Promise<void> {
|
||||
const docAuth = await this._dbManager.getDocAuthCached(this._key);
|
||||
this._docAuth = docAuth;
|
||||
assertAccess(role, docAuth, {openMode: this.openMode});
|
||||
}
|
||||
|
||||
public getCachedAuth(): DocAuthResult {
|
||||
if (!this._docAuth) { throw Error('no cached authentication'); }
|
||||
return this._docAuth;
|
||||
}
|
||||
}
|
||||
|
||||
export class DummyAuthorizer implements Authorizer {
|
||||
constructor(public role: Role|null) {}
|
||||
constructor(public role: Role|null, public docId: string) {}
|
||||
public getUserId() { return null; }
|
||||
public getDocId() { return this.docId; }
|
||||
public async getDoc(): Promise<Document> { throw new Error("Not supported in standalone"); }
|
||||
public async assertAccess() { /* noop */ }
|
||||
public getCachedAuth(): DocAuthResult {
|
||||
return {
|
||||
access: this.role,
|
||||
docId: this.docId,
|
||||
removed: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -11,6 +11,7 @@ import { ActiveDoc } from "app/server/lib/ActiveDoc";
|
||||
import { assertAccess, getOrSetDocAuth, getTransitiveHeaders, getUserId, isAnonymousUser,
|
||||
RequestWithLogin } from 'app/server/lib/Authorizer';
|
||||
import { DocManager } from "app/server/lib/DocManager";
|
||||
import { makeExceptionalDocSession } from "app/server/lib/DocSession";
|
||||
import { DocWorker } from "app/server/lib/DocWorker";
|
||||
import { expressWrap } from 'app/server/lib/expressWrap';
|
||||
import { GristServer } from 'app/server/lib/GristServer';
|
||||
@ -100,7 +101,8 @@ export class DocWorkerApi {
|
||||
throw new ApiError("Invalid query: filter values must be arrays", 400);
|
||||
}
|
||||
const tableId = req.params.tableId;
|
||||
const tableData = await handleSandboxError(tableId, [], activeDoc.fetchQuery(null, {tableId, filters}, true));
|
||||
const tableData = await handleSandboxError(tableId, [], activeDoc.fetchQuery(
|
||||
{client: null, req}, {tableId, filters}, true));
|
||||
// Apply sort/limit parameters, if set. TODO: move sorting/limiting into data engine
|
||||
// and sql.
|
||||
const params = getQueryParameters(req);
|
||||
@ -111,7 +113,7 @@ export class DocWorkerApi {
|
||||
// Returns the list of rowIds for the rows created in the _grist_Attachments table.
|
||||
this._app.post('/api/docs/:docId/attachments', canEdit, withDoc(async (activeDoc, req, res) => {
|
||||
const uploadResult = await handleUpload(req, res);
|
||||
res.json(await activeDoc.addAttachments(req, uploadResult.uploadId));
|
||||
res.json(await activeDoc.addAttachments({client: null, req}, uploadResult.uploadId));
|
||||
}));
|
||||
|
||||
// Returns the metadata for a given attachment ID (i.e. a rowId in _grist_Attachments table).
|
||||
@ -129,7 +131,7 @@ export class DocWorkerApi {
|
||||
const ext = path.extname(fileIdent);
|
||||
const origName = attRecord.fileName as string;
|
||||
const fileName = ext ? path.basename(origName, path.extname(origName)) + ext : origName;
|
||||
const fileData = await activeDoc.getAttachmentData(null, fileIdent);
|
||||
const fileData = await activeDoc.getAttachmentData({client: null, req}, fileIdent);
|
||||
res.status(200)
|
||||
.type(ext)
|
||||
// Construct a content-disposition header of the form 'attachment; filename="NAME"'
|
||||
@ -331,7 +333,10 @@ export class DocWorkerApi {
|
||||
const isAnonymous = isAnonymousUser(req);
|
||||
const {docId} = makeForkIds({userId, isAnonymous, trunkDocId: NEW_DOCUMENT_CODE,
|
||||
trunkUrlId: NEW_DOCUMENT_CODE});
|
||||
await this._docManager.fetchDoc({ client: null, req: req as RequestWithLogin, browserSettings }, docId);
|
||||
await this._docManager.fetchDoc(makeExceptionalDocSession('nascent', {
|
||||
req: req as RequestWithLogin,
|
||||
browserSettings
|
||||
}), docId);
|
||||
return res.status(200).json(docId);
|
||||
}));
|
||||
}
|
||||
|
@ -74,13 +74,28 @@ export class DocClients {
|
||||
* @param {String} type: The type of the message, e.g. 'docUserAction'.
|
||||
* @param {Object} messageData: The data for this type of message.
|
||||
*/
|
||||
public broadcastDocMessage(client: Client|null, type: string, messageData: any): void {
|
||||
for (let i = 0, len = this._docSessions.length; i < len; i++) {
|
||||
const curr = this._docSessions[i];
|
||||
public async broadcastDocMessage(client: Client|null, type: string, messageData: any): Promise<void> {
|
||||
await Promise.all(this._docSessions.map(async curr => {
|
||||
const fromSelf = (curr.client === client);
|
||||
|
||||
sendDocMessage(curr.client, curr.fd, type, messageData, fromSelf);
|
||||
}
|
||||
try {
|
||||
// Make sure user still has view access.
|
||||
await curr.authorizer.assertAccess('viewers');
|
||||
sendDocMessage(curr.client, curr.fd, type, messageData, fromSelf);
|
||||
} catch (e) {
|
||||
if (e.code === 'AUTH_NO_VIEW') {
|
||||
// Skip sending data to this user, they have no view access.
|
||||
log.rawDebug('skip broadcastDocMessage because AUTH_NO_VIEW', {
|
||||
docId: curr.authorizer.getDocId(),
|
||||
...curr.client.getLogMeta()
|
||||
});
|
||||
// Go further and trigger a shutdown for this user, in case they are granted
|
||||
// access again later.
|
||||
sendDocMessage(curr.client, curr.fd, 'docShutdown', null, fromSelf);
|
||||
} else {
|
||||
throw(e);
|
||||
}
|
||||
}
|
||||
}));
|
||||
if (type === "docUserAction" && messageData.docActions) {
|
||||
for (const action of messageData.docActions) {
|
||||
this.activeDoc.docPluginManager.receiveAction(action);
|
||||
|
@ -16,7 +16,7 @@ import {HomeDBManager} from 'app/gen-server/lib/HomeDBManager';
|
||||
import {assertAccess, Authorizer, DocAuthorizer, DummyAuthorizer,
|
||||
isSingleUserMode} from 'app/server/lib/Authorizer';
|
||||
import {Client} from 'app/server/lib/Client';
|
||||
import {makeOptDocSession, OptDocSession} from 'app/server/lib/DocSession';
|
||||
import {makeExceptionalDocSession, makeOptDocSession, OptDocSession} from 'app/server/lib/DocSession';
|
||||
import * as docUtils from 'app/server/lib/docUtils';
|
||||
import {GristServer} from 'app/server/lib/GristServer';
|
||||
import {IDocStorageManager} from 'app/server/lib/IDocStorageManager';
|
||||
@ -109,8 +109,9 @@ export class DocManager extends EventEmitter {
|
||||
*/
|
||||
public async createNewDoc(client: Client): Promise<string> {
|
||||
log.debug('DocManager.createNewDoc');
|
||||
const activeDoc: ActiveDoc = await this.createNewEmptyDoc(makeOptDocSession(client), 'Untitled');
|
||||
await activeDoc.addInitialTable();
|
||||
const docSession = makeExceptionalDocSession('nascent', {client});
|
||||
const activeDoc: ActiveDoc = await this.createNewEmptyDoc(docSession, 'Untitled');
|
||||
await activeDoc.addInitialTable(docSession);
|
||||
return activeDoc.docName;
|
||||
}
|
||||
|
||||
@ -172,7 +173,8 @@ export class DocManager extends EventEmitter {
|
||||
if (!this._homeDbManager) { throw new Error("HomeDbManager not available"); }
|
||||
|
||||
const accessId = this.makeAccessId(userId);
|
||||
const result = await this._doImportDoc(makeOptDocSession(null, browserSettings),
|
||||
const docSession = makeExceptionalDocSession('nascent', {browserSettings});
|
||||
const result = await this._doImportDoc(docSession,
|
||||
globalUploadSet.getUploadInfo(uploadId, accessId), {
|
||||
naming: workspaceId ? 'saved' : 'unsaved',
|
||||
userId,
|
||||
@ -265,14 +267,16 @@ export class DocManager extends EventEmitter {
|
||||
// than a docId.
|
||||
throw new Error(`openDoc expected docId ${docAuth.docId} not urlId ${docId}`);
|
||||
}
|
||||
auth = new DocAuthorizer(dbManager, key, mode);
|
||||
auth = new DocAuthorizer(dbManager, key, mode, docAuth);
|
||||
} else {
|
||||
log.debug(`DocManager.openDoc not using authorization for ${docId} because GRIST_SINGLE_USER`);
|
||||
auth = new DummyAuthorizer('owners');
|
||||
auth = new DummyAuthorizer('owners', docId);
|
||||
}
|
||||
|
||||
// Fetch the document, and continue when we have the ActiveDoc (which may be immediately).
|
||||
const activeDoc: ActiveDoc = await this.fetchDoc(makeOptDocSession(client), docId);
|
||||
const docSessionPrecursor = makeOptDocSession(client);
|
||||
docSessionPrecursor.authorizer = auth;
|
||||
const activeDoc: ActiveDoc = await this.fetchDoc(docSessionPrecursor, docId);
|
||||
|
||||
if (activeDoc.muted) {
|
||||
log.debug('DocManager.openDoc interrupting, called for a muted doc', docId);
|
||||
@ -282,8 +286,8 @@ export class DocManager extends EventEmitter {
|
||||
|
||||
const docSession = activeDoc.addClient(client, auth);
|
||||
const [metaTables, recentActions] = await Promise.all([
|
||||
activeDoc.fetchMetaTables(client),
|
||||
activeDoc.getRecentActions(client, false)
|
||||
activeDoc.fetchMetaTables(docSession),
|
||||
activeDoc.getRecentActions(docSession, false)
|
||||
]);
|
||||
this.emit('open-doc', this.storageManager.getPath(activeDoc.docName));
|
||||
|
||||
@ -333,7 +337,7 @@ export class DocManager extends EventEmitter {
|
||||
const docPromise = this._activeDocs.get(oldName);
|
||||
if (docPromise) {
|
||||
const adoc: ActiveDoc = await docPromise;
|
||||
await adoc.renameDocTo(client, newName);
|
||||
await adoc.renameDocTo({client}, newName);
|
||||
this._activeDocs.set(newName, docPromise);
|
||||
this._activeDocs.delete(oldName);
|
||||
} else {
|
||||
|
@ -10,6 +10,7 @@ import {GristDocAPI} from "app/plugin/GristAPI";
|
||||
import {Storage} from 'app/plugin/StorageAPI';
|
||||
import {ActiveDoc} from 'app/server/lib/ActiveDoc';
|
||||
import {DocPluginData} from 'app/server/lib/DocPluginData';
|
||||
import {makeExceptionalDocSession} from 'app/server/lib/DocSession';
|
||||
import {FileParserElement} from 'app/server/lib/FileParserElement';
|
||||
import {GristServer} from 'app/server/lib/GristServer';
|
||||
import * as log from 'app/server/lib/log';
|
||||
@ -38,11 +39,12 @@ class GristDocAPIImpl implements GristDocAPI {
|
||||
}
|
||||
|
||||
public async fetchTable(tableId: string): Promise<TableColValues> {
|
||||
return fromTableDataAction(await this._activeDoc.fetchTable(null, tableId));
|
||||
return fromTableDataAction(await this._activeDoc.fetchTable(
|
||||
makeExceptionalDocSession('plugin'), tableId));
|
||||
}
|
||||
|
||||
public applyUserActions(actions: any[][]): Promise<ApplyUAResult> {
|
||||
return this._activeDoc.applyUserActions({client: null}, actions);
|
||||
return this._activeDoc.applyUserActions(makeExceptionalDocSession('plugin'), actions);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {BrowserSettings} from 'app/common/BrowserSettings';
|
||||
import {Role} from 'app/common/roles';
|
||||
import {ActiveDoc} from 'app/server/lib/ActiveDoc';
|
||||
import {Authorizer, RequestWithLogin} from 'app/server/lib/Authorizer';
|
||||
import {Authorizer, getUserId, RequestWithLogin} from 'app/server/lib/Authorizer';
|
||||
import {Client} from 'app/server/lib/Client';
|
||||
|
||||
/**
|
||||
@ -13,6 +14,8 @@ export interface OptDocSession {
|
||||
linkId?: number;
|
||||
browserSettings?: BrowserSettings;
|
||||
req?: RequestWithLogin;
|
||||
mode?: 'nascent'|'plugin'|'system'; // special permissions for creating, plugins, and system access
|
||||
authorizer?: Authorizer;
|
||||
}
|
||||
|
||||
export function makeOptDocSession(client: Client|null, browserSettings?: BrowserSettings): OptDocSession {
|
||||
@ -20,6 +23,22 @@ export function makeOptDocSession(client: Client|null, browserSettings?: Browser
|
||||
return {client, browserSettings};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an OptDocSession with special access rights.
|
||||
* - nascent: user is treated as owner (because doc is being created)
|
||||
* - plugin: user is treated as editor (because plugin access control is crude)
|
||||
* - system: user is treated as owner (because of some operation bypassing access control)
|
||||
*/
|
||||
export function makeExceptionalDocSession(mode: 'nascent'|'plugin'|'system',
|
||||
options: {client?: Client,
|
||||
req?: RequestWithLogin,
|
||||
browserSettings?: BrowserSettings} = {}): OptDocSession {
|
||||
const docSession = makeOptDocSession(options.client || null, options.browserSettings);
|
||||
docSession.mode = mode;
|
||||
docSession.req = options.req;
|
||||
return docSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* DocSession objects maintain information for a single session<->doc instance.
|
||||
*/
|
||||
@ -47,3 +66,48 @@ export class DocSession implements OptDocSession {
|
||||
// Browser settings (like timezone) obtained from the Client.
|
||||
public get browserSettings(): BrowserSettings { return this.client.browserSettings; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract userId from OptDocSession. Use Authorizer if available (for web socket
|
||||
* sessions), or get it from the Request if that is available (for rest api calls),
|
||||
* or from the Client if that is available. Returns null if userId information is
|
||||
* not available or not cached.
|
||||
*/
|
||||
export function getDocSessionUserId(docSession: OptDocSession): number|null {
|
||||
if (docSession.authorizer) {
|
||||
return docSession.authorizer.getUserId();
|
||||
}
|
||||
if (docSession.req) {
|
||||
return getUserId(docSession.req);
|
||||
}
|
||||
if (docSession.client) {
|
||||
return docSession.client.getCachedUserId();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract user's role from OptDocSession. Method depends on whether using web
|
||||
* sockets or rest api. Assumes that access has already been checked by wrappers
|
||||
* for api methods and that cached access information is therefore available.
|
||||
*/
|
||||
export function getDocSessionAccess(docSession: OptDocSession): Role {
|
||||
// "nascent" DocSessions are for when a document is being created, and user is
|
||||
// its only owner as yet.
|
||||
// "system" DocSessions are for access without access control.
|
||||
if (docSession.mode === 'nascent' || docSession.mode === 'system') { return 'owners'; }
|
||||
// "plugin" DocSessions are for access from plugins, which is currently quite crude,
|
||||
// and granted only to editors.
|
||||
if (docSession.mode === 'plugin') { return 'editors'; }
|
||||
if (docSession.authorizer) {
|
||||
const access = docSession.authorizer.getCachedAuth().access;
|
||||
if (!access) { throw new Error('getDocSessionAccess expected authorizer.getCachedAuth'); }
|
||||
return access;
|
||||
}
|
||||
if (docSession.req) {
|
||||
const access = docSession.req.docAuth?.access;
|
||||
if (!access) { throw new Error('getDocSessionAccess expected req.docAuth.access'); }
|
||||
return access;
|
||||
}
|
||||
throw new Error('getDocSessionAccess could not find access information in DocSession');
|
||||
}
|
||||
|
@ -4,10 +4,10 @@
|
||||
*/
|
||||
import {HomeDBManager} from 'app/gen-server/lib/HomeDBManager';
|
||||
import {ActionHistoryImpl} from 'app/server/lib/ActionHistoryImpl';
|
||||
import {ActiveDoc} from 'app/server/lib/ActiveDoc';
|
||||
import {assertAccess, getOrSetDocAuth, getUserId, RequestWithLogin} from 'app/server/lib/Authorizer';
|
||||
import {Client} from 'app/server/lib/Client';
|
||||
import * as Comm from 'app/server/lib/Comm';
|
||||
import {DocSession} from 'app/server/lib/DocSession';
|
||||
import {IDocStorageManager} from 'app/server/lib/IDocStorageManager';
|
||||
import * as log from 'app/server/lib/log';
|
||||
import {integerParam, optStringParam, stringParam} from 'app/server/lib/requestUtils';
|
||||
@ -35,9 +35,9 @@ export class DocWorker {
|
||||
|
||||
public async getAttachment(req: express.Request, res: express.Response): Promise<void> {
|
||||
try {
|
||||
const client = this._comm.getClient(stringParam(req.query.clientId));
|
||||
const activeDoc = this._getActiveDoc(stringParam(req.query.clientId),
|
||||
integerParam(req.query.docFD));
|
||||
const docSession = this._getDocSession(stringParam(req.query.clientId),
|
||||
integerParam(req.query.docFD));
|
||||
const activeDoc = docSession.activeDoc;
|
||||
const ext = path.extname(stringParam(req.query.ident));
|
||||
const type = mimeTypes.lookup(ext);
|
||||
|
||||
@ -48,7 +48,7 @@ export class DocWorker {
|
||||
// Construct a content-disposition header of the form 'inline|attachment; filename="NAME"'
|
||||
const contentDispType = inline ? "inline" : "attachment";
|
||||
const contentDispHeader = contentDisposition(stringParam(req.query.name), {type: contentDispType});
|
||||
const data = await activeDoc.getAttachmentData(client, stringParam(req.query.ident));
|
||||
const data = await activeDoc.getAttachmentData(docSession, stringParam(req.query.ident));
|
||||
res.status(200)
|
||||
.type(ext)
|
||||
.set('Content-Disposition', contentDispHeader)
|
||||
@ -136,8 +136,8 @@ export class DocWorker {
|
||||
let urlId: string|undefined;
|
||||
try {
|
||||
if (optStringParam(req.query.clientId)) {
|
||||
const activeDoc = this._getActiveDoc(stringParam(req.query.clientId),
|
||||
integerParam(req.query.docFD));
|
||||
const activeDoc = this._getDocSession(stringParam(req.query.clientId),
|
||||
integerParam(req.query.docFD)).activeDoc;
|
||||
// TODO: The docId should be stored in the ActiveDoc class. Currently docName is
|
||||
// used instead, which will coincide with the docId for hosted grist but not for
|
||||
// standalone grist.
|
||||
@ -158,10 +158,9 @@ export class DocWorker {
|
||||
}
|
||||
}
|
||||
|
||||
private _getActiveDoc(clientId: string, docFD: number): ActiveDoc {
|
||||
private _getDocSession(clientId: string, docFD: number): DocSession {
|
||||
const client = this._comm.getClient(clientId);
|
||||
const docSession = client.getDocSession(docFD);
|
||||
return docSession.activeDoc;
|
||||
return client.getDocSession(docFD);
|
||||
}
|
||||
}
|
||||
|
||||
@ -175,7 +174,7 @@ async function activeDocMethod(role: 'viewers'|'editors'|null, methodName: strin
|
||||
const activeDoc = docSession.activeDoc;
|
||||
if (role) { await docSession.authorizer.assertAccess(role); }
|
||||
// Include a basic log record for each ActiveDoc method call.
|
||||
log.rawDebug('activeDocMethod', activeDoc.getLogMeta(client, methodName));
|
||||
log.rawDebug('activeDocMethod', activeDoc.getLogMeta(docSession, methodName));
|
||||
return (activeDoc as any)[methodName](docSession, ...args);
|
||||
}
|
||||
|
||||
|
@ -275,7 +275,7 @@ export class Sharing {
|
||||
retValues: sandboxActionBundle.retValues,
|
||||
summarize: true,
|
||||
});
|
||||
await this._activeDoc.docClients.broadcastDocMessage(client || null, 'docUserAction', {
|
||||
await this._activeDoc.broadcastDocUpdate(client || null, 'docUserAction', {
|
||||
actionGroup,
|
||||
docActions: getEnvContent(localActionBundle.stored).concat(
|
||||
getEnvContent(localActionBundle.calc))
|
||||
|
@ -37,7 +37,7 @@ function generateCSV(req, res, comm) {
|
||||
|
||||
res.set('Content-Type', 'text/csv');
|
||||
res.setHeader('Content-Disposition', contentDisposition(name));
|
||||
return makeCSV(activeDoc, viewSectionId, activeSortOrder)
|
||||
return makeCSV(activeDoc, viewSectionId, activeSortOrder, req)
|
||||
.then(data => res.send(data));
|
||||
}
|
||||
exports.generateCSV = generateCSV;
|
||||
@ -51,7 +51,7 @@ exports.generateCSV = generateCSV;
|
||||
* @param {Integer[]} activeSortOrder (optional) - overriding sort order.
|
||||
* @return {Promise<string>} Promise for the resulting CSV.
|
||||
*/
|
||||
function makeCSV(activeDoc, viewSectionId, sortOrder) {
|
||||
function makeCSV(activeDoc, viewSectionId, sortOrder, req) {
|
||||
return Promise.try(() => {
|
||||
const tables = activeDoc.docData.getTables();
|
||||
const viewSection = tables.get('_grist_Views_section').getRecord(viewSectionId);
|
||||
@ -88,7 +88,7 @@ function makeCSV(activeDoc, viewSectionId, sortOrder) {
|
||||
return directionalColRef > 0 ? effectiveColRef : -effectiveColRef;
|
||||
});
|
||||
|
||||
return [activeDoc.fetchTable(null, table.tableId, true), tableColumns, viewColumns];
|
||||
return [activeDoc.fetchTable({client: null, req}, table.tableId, true), tableColumns, viewColumns];
|
||||
}).spread((data, tableColumns, viewColumns) => {
|
||||
const rowIds = data[2];
|
||||
const dataByColId = data[3];
|
||||
|
Loading…
Reference in New Issue
Block a user