mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) implement a safe mode for opening documents with rule problems
Summary: Adds an "enter safe mode" option and explanation in modal that appears when a document fails to load, if user is owner. If "enter safe mode" is selected, document is reloaded on server in a special mode. Currently, the only difference is that if the acl rules fail to load, they are replaced with a fallback that grants full access to owners and no access to anyone else. An extra tag is shown to mark the document as safe mode, with an "x" for cancelling safe mode. There are other ways a document could fail to load than just acl rules, so this is just a start. Test Plan: added test Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2686
This commit is contained in:
@@ -123,9 +123,11 @@ export class ActiveDoc extends EventEmitter {
|
||||
|
||||
// Timer for shutting down the ActiveDoc a bit after all clients are gone.
|
||||
private _inactivityTimer = new InactivityTimer(() => this.shutdown(), Deps.ACTIVEDOC_TIMEOUT * 1000);
|
||||
private _recoveryMode: boolean = false;
|
||||
|
||||
constructor(docManager: DocManager, docName: string) {
|
||||
constructor(docManager: DocManager, docName: string, wantRecoveryMode?: boolean) {
|
||||
super();
|
||||
if (wantRecoveryMode) { this._recoveryMode = true; }
|
||||
this._docManager = docManager;
|
||||
this._docName = docName;
|
||||
this.docStorage = new DocStorage(docManager.storageManager, docName);
|
||||
@@ -154,6 +156,8 @@ export class ActiveDoc extends EventEmitter {
|
||||
|
||||
public get docName(): string { return this._docName; }
|
||||
|
||||
public get recoveryMode(): boolean { return this._recoveryMode; }
|
||||
|
||||
// Helpers to log a message along with metadata about the request.
|
||||
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); }
|
||||
@@ -411,7 +415,7 @@ export class ActiveDoc extends EventEmitter {
|
||||
await this._actionHistory.initialize();
|
||||
this._granularAccess = new GranularAccess(this.docData, (query) => {
|
||||
return this._fetchQueryFromDB(query, false);
|
||||
});
|
||||
}, this.recoveryMode);
|
||||
await this._granularAccess.update();
|
||||
this._sharing = new Sharing(this, this._actionHistory, this._modificationLock);
|
||||
|
||||
@@ -871,6 +875,10 @@ export class ActiveDoc extends EventEmitter {
|
||||
return this.shutdown();
|
||||
}
|
||||
|
||||
public isOwner(docSession: OptDocSession): boolean {
|
||||
return this._granularAccess.isOwner(docSession);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fork the current document. In fact, all that requires is calculating a good
|
||||
* ID for the fork. TODO: reconcile the two ways there are now of preparing a fork.
|
||||
|
||||
@@ -222,11 +222,22 @@ export class DocWorkerApi {
|
||||
|
||||
// Reload a document forcibly (in fact this closes the doc, it will be automatically
|
||||
// reopened on use).
|
||||
this._app.post('/api/docs/:docId/force-reload', canEdit, withDoc(async (activeDoc, req, res) => {
|
||||
this._app.post('/api/docs/:docId/force-reload', canEdit, throttled(async (req, res) => {
|
||||
const activeDoc = await this._getActiveDoc(req);
|
||||
await activeDoc.reloadDoc();
|
||||
res.json(null);
|
||||
}));
|
||||
|
||||
this._app.post('/api/docs/:docId/recover', canEdit, throttled(async (req, res) => {
|
||||
const recoveryModeRaw = req.body.recoveryMode;
|
||||
const recoveryMode = (typeof recoveryModeRaw === 'boolean') ? recoveryModeRaw : undefined;
|
||||
if (!this._isOwner(req)) { throw new Error('Only owners can control recovery mode'); }
|
||||
const activeDoc = await this._docManager.fetchDoc(docSessionFromRequest(req), getDocId(req), recoveryMode);
|
||||
res.json({
|
||||
recoveryMode: activeDoc.recoveryMode
|
||||
});
|
||||
}));
|
||||
|
||||
// DELETE /api/docs/:docId
|
||||
// Delete the specified doc.
|
||||
this._app.delete('/api/docs/:docId', canEditMaybeRemoved, throttled(async (req, res) => {
|
||||
|
||||
@@ -296,7 +296,8 @@ export class DocManager extends EventEmitter {
|
||||
clientId: docSession.client.clientId,
|
||||
doc: metaTables,
|
||||
log: recentActions,
|
||||
plugins: activeDoc.docPluginManager.getPlugins()
|
||||
plugins: activeDoc.docPluginManager.getPlugins(),
|
||||
recoveryMode: activeDoc.recoveryMode,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -364,12 +365,21 @@ export class DocManager extends EventEmitter {
|
||||
/**
|
||||
* Fetches an ActiveDoc object. Used by openDoc.
|
||||
*/
|
||||
public async fetchDoc(docSession: OptDocSession, docName: string): Promise<ActiveDoc> {
|
||||
public async fetchDoc(docSession: OptDocSession, docName: string,
|
||||
wantRecoveryMode?: boolean): Promise<ActiveDoc> {
|
||||
log.debug('DocManager.fetchDoc', docName);
|
||||
// Repeat until we acquire an ActiveDoc that is not muted (shutting down).
|
||||
for (;;) {
|
||||
if (this._activeDocs.has(docName) && wantRecoveryMode !== undefined) {
|
||||
const activeDoc = await this._activeDocs.get(docName);
|
||||
if (activeDoc && activeDoc.recoveryMode !== wantRecoveryMode && activeDoc.isOwner(docSession)) {
|
||||
// shutting doc down to have a chance to re-open in the correct mode.
|
||||
// TODO: there could be a battle with other users opening it in a different mode.
|
||||
await activeDoc.shutdown();
|
||||
}
|
||||
}
|
||||
if (!this._activeDocs.has(docName)) {
|
||||
const newDoc = this.gristServer.create.ActiveDoc(this, docName);
|
||||
const newDoc = this.gristServer.create.ActiveDoc(this, docName, wantRecoveryMode);
|
||||
// Propagate backupMade events from newly opened activeDocs (consolidate all to DocMan)
|
||||
newDoc.on('backupMade', (bakPath: string) => {
|
||||
this.emit('backupMade', bakPath);
|
||||
|
||||
@@ -109,7 +109,7 @@ 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>) {
|
||||
public constructor(private _docData: DocData, private _fetchQueryFromDB: (query: Query) => Promise<TableDataAction>, private _recoveryMode: boolean) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -292,9 +292,19 @@ export class GranularAccess {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether user has owner-level access to the document.
|
||||
* Check whether user has full access to the document. Currently that is interpreted
|
||||
* as equivalent owner-level access to the document.
|
||||
* TODO: uses of this method should be checked to see if they can be fleshed out
|
||||
* now we have more of the ACL implementation done.
|
||||
*/
|
||||
public hasFullAccess(docSession: OptDocSession): boolean {
|
||||
return this.isOwner(docSession);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether user has owner-level access to the document.
|
||||
*/
|
||||
public isOwner(docSession: OptDocSession): boolean {
|
||||
const access = getDocSessionAccess(docSession);
|
||||
return access === 'owners';
|
||||
}
|
||||
@@ -765,6 +775,12 @@ export class GranularAccess {
|
||||
// TODO: could also get this for websocket access, just via a different route.
|
||||
user.Origin = docSession.req?.get('origin') || null;
|
||||
|
||||
if (this._ruleCollection.ruleError && !this._recoveryMode) {
|
||||
// It is important to signal that the doc is in an unexpected state,
|
||||
// and prevent it opening.
|
||||
throw this._ruleCollection.ruleError;
|
||||
}
|
||||
|
||||
for (const clause of this._ruleCollection.getUserAttributeRules().values()) {
|
||||
if (clause.name in user) {
|
||||
log.warn(`User attribute ${clause.name} ignored; conflicts with an existing one`);
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface ICreate {
|
||||
// should not interfere with each other.
|
||||
ExternalStorage(purpose: 'doc' | 'meta', testExtraPrefix: string): ExternalStorage|undefined;
|
||||
|
||||
ActiveDoc(docManager: DocManager, docName: string): ActiveDoc;
|
||||
ActiveDoc(docManager: DocManager, docName: string, safeMode?: boolean): ActiveDoc;
|
||||
DocManager(storageManager: IDocStorageManager, pluginManager: PluginManager,
|
||||
homeDbManager: HomeDBManager|null, gristServer: GristServer): DocManager;
|
||||
NSandbox(options: ISandboxCreationOptions): ISandbox;
|
||||
|
||||
Reference in New Issue
Block a user