(core) read document as owner in pre-fork mode, if have sufficient access to it

Summary:
This tweaks pre-fork mode to make the user's experience a bit more seamless.
Pre-fork mode is where the user has opened a document with intent to
fork it, but actual forking (with allocation of a new document id)
is postponed until they make their first change.

The tweak makes the user an owner for granular access purposes, if
forking is permitted.  So data visible only to owners because of
access rules will be visible to them.  As always, any edits would
go to a separate new copy.

A remaining tricky corner is what to do about "View As" functionality
on forks.  Fork sharing cannot be controlled, so the "Users -> View As"
functionality isn't available.  Perhaps the "Users" button on a fork
could encourage doing a save-copy and inviting users, or offer some
dummy users?  In any case, this diff doesn't change anything with
that corner.

Test Plan: added test

Reviewers: dsagal

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D2931
pull/9/head
Paul Fitzpatrick 3 years ago
parent 291bcd17ff
commit 95cc2eb282

@ -577,6 +577,22 @@ export class ActiveDoc extends EventEmitter {
return this._granularAccess.canCopyEverything(docSession);
}
// Check if it is appropriate for the user to be treated as an owner of
// the document for granular access purposes when in "prefork" mode
// (meaning a document has been opened with the intent to fork it, but
// an initial modification has not yet been made).
// Currently, we decide it is appropriate if the user has access to all
// the data in the document, either directly or via the special
// "FullCopies" permission.
public async canForkAsOwner(docSession: OptDocSession) {
return this._granularAccess.canCopyEverything(docSession);
}
// Remove cached access information for a given session.
public flushAccess(docSession: OptDocSession) {
return this._granularAccess.flushAccess(docSession);
}
/**
* Fetches a particular table from the data engine to return to the client.
* @param {String} tableId: The string identifier of the table.

@ -288,11 +288,38 @@ export class DocManager extends EventEmitter {
throw new Error(`document ${docId} cannot be opened right now`);
}
// Get a fresh DocSession object.
const docSession = activeDoc.addClient(client, auth);
// If opening in (pre-)fork mode, check if it is appropriate to treat the user as
// an owner for granular access purposes.
if (mode === 'fork') {
if (await activeDoc.canForkAsOwner(docSession)) {
// Mark the session specially and flush any cached access
// information. It is easier to make this a property of the
// session than to try computing it later in the heat of
// battle, since it introduces a loop where a user property
// (user.Access) depends on evaluating rules, but rules need
// the user properties in order to be evaluated. It is also
// somewhat justifiable even if permissions change later on
// the theory that the fork is theoretically happening at this
// instance).
docSession.forkingAsOwner = true;
activeDoc.flushAccess(docSession);
} else {
// TODO: it would be kind to pass on a message to the client
// to let them know they won't be able to fork. They'll get
// an error when they make their first change. But currently
// we only have the blunt instrument of throwing an error,
// which would prevent access to the document entirely.
}
}
const [metaTables, recentActions] = await Promise.all([
activeDoc.fetchMetaTables(docSession),
activeDoc.getRecentActions(docSession, false)
]);
this.emit('open-doc', this.storageManager.getPath(activeDoc.docName));
return {

@ -19,6 +19,7 @@ export interface OptDocSession {
// special permissions for creating, plugins, system, and share access
mode?: 'nascent'|'plugin'|'system'|'share';
authorizer?: Authorizer;
forkingAsOwner?: boolean; // Set if it is appropriate in a pre-fork state to become an owner.
}
export function makeOptDocSession(client: Client|null, browserSettings?: BrowserSettings): OptDocSession {
@ -67,6 +68,8 @@ export class DocSession implements OptDocSession {
*/
public linkId?: number;
public forkingAsOwner?: boolean;
constructor(
public readonly activeDoc: ActiveDoc,
public readonly client: Client,

@ -179,6 +179,11 @@ export class GranularAccess implements GranularAccessForBundle {
public getGranularAccessForBundle(docSession: OptDocSession, docActions: DocAction[], undo: DocAction[],
userActions: UserAction[], isDirect: boolean[]): void {
if (this._activeBundle) { throw new Error('Cannot start a bundle while one is already in progress'); }
// This should never happen - attempts to write to a pre-fork session should be
// caught by an Authorizer. But let's be paranoid, since we may be pretending to
// be an owner for granular access purposes, and owners can write if we're not
// careful!
if (docSession.forkingAsOwner) { throw new Error('Should never modify a prefork'); }
this._activeBundle = {
docSession, docActions, undo, userActions, isDirect,
applied: false, hasDeliberateRuleChange: false, hasAnyRuleChange: false
@ -695,17 +700,28 @@ export class GranularAccess implements GranularAccessForBundle {
(_docSession) => this._filterDocUpdate(_docSession, message));
}
// Remove cached access information for a given session.
public flushAccess(docSession: OptDocSession) {
this._ruler.flushAccess(docSession);
this._userAttributesMap.delete(docSession);
this._prevUserAttributesMap?.delete(docSession);
}
/**
* Get the role the session user has for this document. User may be overridden,
* in which case the role of the override is returned.
* The forkingAsOwner flag of docSession should not be respected for non-owners,
* so that the pseudo-ownership it offers is restricted to granular access within a
* document (as opposed to document-level operations).
*/
private async _getNominalAccess(docSession: OptDocSession): Promise<Role> {
const linkParameters = docSession.authorizer?.getLinkParameters() || {};
if (linkParameters.aclAsUserId || linkParameters.aclAsUser) {
const baseAccess = getDocSessionAccess(docSession);
if ((linkParameters.aclAsUserId || linkParameters.aclAsUser) && baseAccess === 'owners') {
const info = await this._getUser(docSession);
return info.Access as Role;
}
return getDocSessionAccess(docSession);
return baseAccess;
}
/**
@ -1163,6 +1179,14 @@ export class GranularAccess implements GranularAccessForBundle {
const attrs = this._getUserAttributes(docSession);
access = getDocSessionAccess(docSession);
if (docSession.forkingAsOwner) {
// For granular access purposes, we become an owner.
// It is a bit of a bluff, done on the understanding that this session will
// never be used to edit the document, and that any edits will be done on a
// fork.
access = 'owners';
}
// If aclAsUserId/aclAsUser is set, then override user for acl purposes.
if (linkParameters.aclAsUserId || linkParameters.aclAsUser) {
if (access !== 'owners') { throw new Error('only an owner can override user'); }
@ -1656,6 +1680,10 @@ export class Ruler {
async () => new PermissionInfo(this.ruleCollection, {user: await this._owner.getUser(docSession)}));
}
public flushAccess(docSession: OptDocSession) {
this._permissionInfoMap.delete(docSession);
}
/**
* Update granular access from DocData.
*/

Loading…
Cancel
Save