mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) allow a doc owner to test access as a different user
Summary: This adds back-end support for query parameters `aclAsUser_` and `aclAsUserId_` which, when either is present, direct Grist to process granular access control rules from the point of view of that user (specified by email or id respectively). Some front end support is added, in the form of a tag that shows up when in this mode, and a way to cancel the mode. No friendly way to initiate the mode is offered yet. Test Plan: added test Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2704
This commit is contained in:
@@ -16,7 +16,7 @@ import {menu, menuDivider, menuIcon, menuItem, menuText} from 'app/client/ui2018
|
||||
import {confirmModal} from 'app/client/ui2018/modals';
|
||||
import {AsyncFlow, CancelledError, FlowRunner} from 'app/common/AsyncFlow';
|
||||
import {delay} from 'app/common/delay';
|
||||
import {OpenDocMode} from 'app/common/DocListAPI';
|
||||
import {OpenDocMode, UserOverride} from 'app/common/DocListAPI';
|
||||
import {IGristUrlState, parseUrlId, UrlIdParts} from 'app/common/gristUrls';
|
||||
import {getReconnectTimeout} from 'app/common/gutil';
|
||||
import {canEdit} from 'app/common/roles';
|
||||
@@ -32,6 +32,7 @@ export interface DocInfo extends Document {
|
||||
isPreFork: boolean;
|
||||
isFork: boolean;
|
||||
isRecoveryMode: boolean;
|
||||
userOverride: UserOverride|null;
|
||||
isBareFork: boolean; // a document created without logging in, which is treated as a
|
||||
// fork without an original.
|
||||
idParts: UrlIdParts;
|
||||
@@ -56,6 +57,7 @@ export interface DocPageModel {
|
||||
isPrefork: Observable<boolean>;
|
||||
isFork: Observable<boolean>;
|
||||
isRecoveryMode: Observable<boolean>;
|
||||
userOverride: Observable<UserOverride|null>;
|
||||
isBareFork: Observable<boolean>;
|
||||
isSample: Observable<boolean>;
|
||||
|
||||
@@ -93,6 +95,7 @@ export class DocPageModelImpl extends Disposable implements DocPageModel {
|
||||
public readonly isPrefork = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isPreFork : false);
|
||||
public readonly isFork = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isFork : false);
|
||||
public readonly isRecoveryMode = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isRecoveryMode : false);
|
||||
public readonly userOverride = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.userOverride : null);
|
||||
public readonly isBareFork = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isBareFork : false);
|
||||
public readonly isSample = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isSample : false);
|
||||
|
||||
@@ -245,8 +248,9 @@ export class DocPageModelImpl extends Disposable implements DocPageModel {
|
||||
flow.onDispose(() => comm.releaseDocConnection(doc.id));
|
||||
|
||||
const openDocResponse = await comm.openDoc(doc.id, doc.openMode, linkParameters);
|
||||
if (openDocResponse.recoveryMode) {
|
||||
doc.isRecoveryMode = true;
|
||||
if (openDocResponse.recoveryMode || openDocResponse.userOverride) {
|
||||
doc.isRecoveryMode = Boolean(openDocResponse.recoveryMode);
|
||||
doc.userOverride = openDocResponse.userOverride || null;
|
||||
this.currentDoc.set({...doc});
|
||||
}
|
||||
const gdModule = await gristDocModulePromise;
|
||||
@@ -329,6 +333,7 @@ function buildDocInfo(doc: Document, mode: OpenDocMode): DocInfo {
|
||||
...doc,
|
||||
isFork,
|
||||
isRecoveryMode: false, // we don't know yet, will learn when doc is opened.
|
||||
userOverride: null, // ditto.
|
||||
isSample,
|
||||
isPreFork,
|
||||
isBareFork,
|
||||
|
||||
@@ -44,10 +44,12 @@ export function createTopBarDoc(owner: MultiHolder, appModel: AppModel, pageMode
|
||||
docNameSave: renameDoc,
|
||||
pageNameSave: getRenamePageFn(gristDoc),
|
||||
cancelRecoveryMode: getCancelRecoveryModeFn(gristDoc),
|
||||
cancelUserOverride: getCancelUserOverrideFn(gristDoc),
|
||||
isPageNameReadOnly: (use) => use(gristDoc.isReadonly) || typeof use(gristDoc.activeViewId) !== 'number',
|
||||
isDocNameReadOnly: (use) => use(gristDoc.isReadonly) || use(pageModel.isFork),
|
||||
isFork: pageModel.isFork,
|
||||
isRecoveryMode: pageModel.isRecoveryMode,
|
||||
userOverride: pageModel.userOverride,
|
||||
isFiddle: Computed.create(owner, (use) => use(pageModel.isPrefork) && !use(pageModel.isSample)),
|
||||
isSnapshot: Computed.create(owner, doc, (use, _doc) => Boolean(_doc && _doc.idParts.snapshotId)),
|
||||
isPublic: Computed.create(owner, doc, (use, _doc) => Boolean(_doc && _doc.public)),
|
||||
@@ -100,6 +102,15 @@ function getCancelRecoveryModeFn(gristDoc: GristDoc): () => Promise<void> {
|
||||
};
|
||||
}
|
||||
|
||||
function getCancelUserOverrideFn(gristDoc: GristDoc): () => Promise<void> {
|
||||
return async () => {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('aclAsUser_');
|
||||
url.searchParams.delete('aclAsUserId_');
|
||||
window.location.assign(url.href);
|
||||
};
|
||||
}
|
||||
|
||||
function topBarUndoBtn(iconName: IconName, ...domArgs: DomElementArg[]): Element {
|
||||
return cssHoverCircle(
|
||||
cssTopBarUndoBtn(iconName),
|
||||
|
||||
@@ -9,6 +9,7 @@ import { urlState } from 'app/client/models/gristUrlState';
|
||||
import { colors, testId } from 'app/client/ui2018/cssVars';
|
||||
import { editableLabel } from 'app/client/ui2018/editableLabel';
|
||||
import { icon } from 'app/client/ui2018/icons';
|
||||
import { UserOverride } from 'app/common/DocListAPI';
|
||||
import { BindableValue, dom, Observable, styled } from 'grainjs';
|
||||
import { tooltip } from 'popweasel';
|
||||
|
||||
@@ -85,11 +86,13 @@ export function docBreadcrumbs(
|
||||
docNameSave: (val: string) => Promise<void>,
|
||||
pageNameSave: (val: string) => Promise<void>,
|
||||
cancelRecoveryMode: () => Promise<void>,
|
||||
cancelUserOverride: () => Promise<void>,
|
||||
isDocNameReadOnly?: BindableValue<boolean>,
|
||||
isPageNameReadOnly?: BindableValue<boolean>,
|
||||
isFork: Observable<boolean>,
|
||||
isFiddle: Observable<boolean>,
|
||||
isRecoveryMode: Observable<boolean>,
|
||||
userOverride: Observable<UserOverride|null>,
|
||||
isSnapshot?: Observable<boolean>,
|
||||
isPublic?: Observable<boolean>,
|
||||
}
|
||||
@@ -118,11 +121,17 @@ export function docBreadcrumbs(
|
||||
}
|
||||
if (use(options.isRecoveryMode)) {
|
||||
return cssAlertTag('recovery mode',
|
||||
dom('a', dom.on('click', async () => {
|
||||
await options.cancelRecoveryMode()
|
||||
}), icon('CrossSmall')),
|
||||
dom('a', dom.on('click', () => options.cancelRecoveryMode()),
|
||||
icon('CrossSmall')),
|
||||
testId('recovery-mode-tag'));
|
||||
}
|
||||
const userOverride = use(options.userOverride);
|
||||
if (userOverride) {
|
||||
return cssAlertTag(userOverride.user?.email || 'override',
|
||||
dom('a', dom.on('click', () => options.cancelUserOverride()),
|
||||
icon('CrossSmall')),
|
||||
testId('user-override-tag'));
|
||||
}
|
||||
if (use(options.isFiddle)) {
|
||||
return cssTag('fiddle', tooltip({title: fiddleExplanation}), testId('fiddle-tag'));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user