(core) support locking document structure to be controlled by owners only

Summary:
This is an incremental step in granular access control.  Using
a temporary `{colIds: '~o structure'}` representation in the
`_grist_ACLResources` table, the document structure can be set
to be controlled by owners only.

Test Plan: added test

Reviewers: dsagal

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D2613
This commit is contained in:
Paul Fitzpatrick 2020-09-17 07:34:54 -04:00
parent 2087ae5f67
commit b44e4a94ab
2 changed files with 38 additions and 18 deletions

View File

@ -525,7 +525,7 @@ export class ActiveDoc extends EventEmitter {
// Check if user has rights to download this doc.
public canDownload(docSession: OptDocSession) {
return this._granularAccess.hasViewAccess(docSession) &&
!this._granularAccess.hasNuancedAccess(docSession);
this._granularAccess.canReadEverything(docSession);
}
/**
@ -633,9 +633,9 @@ export class ActiveDoc extends EventEmitter {
*/
public async findColFromValues(docSession: DocSession, values: any[], n: number,
optTableId?: string): Promise<number[]> {
// This could leak information about private tables, so if there are any nuanced
// permissions in force and the user does not have full access, do nothing.
if (this._granularAccess.hasNuancedAccess(docSession)) { return []; }
// This could leak information about private tables, so if user cannot read entire
// document, do nothing.
if (!this._granularAccess.canReadEverything(docSession)) { return []; }
this.logInfo(docSession, "findColFromValues(%s, %s, %s)", docSession, values, n);
await this.waitForInitialization();
return this._dataEngine.pyCall('find_col_from_values', values, n, optTableId);
@ -783,7 +783,7 @@ export class ActiveDoc extends EventEmitter {
public async autocomplete(docSession: DocSession, txt: string, tableId: string): Promise<string[]> {
// Autocompletion can leak names of tables and columns.
if (this._granularAccess.hasNuancedAccess(docSession)) { return []; }
if (!this._granularAccess.canReadEverything(docSession)) { return []; }
await this.waitForInitialization();
return this._dataEngine.pyCall('autocomplete', txt, tableId);
}
@ -828,7 +828,7 @@ export class ActiveDoc extends EventEmitter {
* ID for the fork. TODO: reconcile the two ways there are now of preparing a fork.
*/
public async fork(docSession: DocSession): Promise<ForkResult> {
if (this._granularAccess.hasNuancedAccess(docSession)) {
if (!this._granularAccess.canReadEverything(docSession)) {
throw new Error('cannot confirm authority to copy document');
}
const userId = docSession.client.getCachedUserId();
@ -1183,7 +1183,7 @@ export class ActiveDoc extends EventEmitter {
actionGroup: ActionGroup,
docActions: DocAction[]
}) {
if (!this._granularAccess.hasNuancedAccess(docSession)) { return message; }
if (this._granularAccess.canReadEverything(docSession)) { return message; }
const result = {
actionGroup: this._granularAccess.filterActionGroup(docSession, message.actionGroup),
docActions: this._granularAccess.filterOutgoingDocActions(docSession, message.docActions),

View File

@ -47,18 +47,24 @@ const SURPRISING_ACTIONS = new Set(['AddUser',
const OK_ACTIONS = new Set(['Calculate', 'AddEmptyTable']);
/**
* Manage granular access to a document. This allows nuances other than the coarse
* owners/editors/viewers distinctions.
*
* Currently the only supported nuance is to mark certain tables as accessible by
* owners only. To do so, in the _grist_ACLResources table, add a row like the
* one already there, but with "~o" as the colIds, and the desired tableId set.
* This is just a placeholder for a future representation.
* Manage granular access to a document. This allows nuances other than the coarse
* owners/editors/viewers distinctions. As a placeholder for a future representation,
* nuances are stored in the _grist_ACLResources table. Supported nauances:
*
* - {tableId, colIds: '~o'}: mark specified table as accessible by owners only.
* - {tableId: '', colIds: '~o structure'}: mark doc structure as editable by owners only.
*
*/
export class GranularAccess {
private _resources: TableData;
// Tables marked as accessible only by owners.
private _ownerOnlyTableIds = new Set<string>();
// Document structure modifiable only by owners?
private _onlyOwnersCanModifyStructure: boolean = false;
public constructor(private _docData: DocData) {
this.update();
}
@ -70,9 +76,13 @@ export class GranularAccess {
this._resources = this._docData.getTable('_grist_ACLResources')!;
this._ownerOnlyTableIds.clear();
for (const res of this._resources.getRecords()) {
if (res.tableId && String(res.colIds).startsWith('~o')) {
const code = String(res.colIds);
if (res.tableId && code === '~o') {
this._ownerOnlyTableIds.add(String(res.tableId));
}
if (!res.tableId && code === '~o structure') {
this._onlyOwnersCanModifyStructure = true;
}
}
}
@ -115,7 +125,7 @@ export class GranularAccess {
* to filter acceptible parts of ActionGroup, rather than denying entirely.
*/
public allowActionGroup(docSession: OptDocSession, actionGroup: ActionGroup): boolean {
return !this.hasNuancedAccess(docSession);
return this.canReadEverything(docSession);
}
/**
@ -169,10 +179,20 @@ export class GranularAccess {
* access is simple and without nuance.
*/
public hasNuancedAccess(docSession: OptDocSession): boolean {
if (this._ownerOnlyTableIds.size === 0) { return false; }
if (this._ownerOnlyTableIds.size === 0 && !this._onlyOwnersCanModifyStructure) {
return false;
}
return !this.hasFullAccess(docSession);
}
/**
* Check whether user can read everything in document.
*/
public canReadEverything(docSession: OptDocSession): boolean {
if (this._ownerOnlyTableIds.size === 0) { return true; }
return this.hasFullAccess(docSession);
}
/**
* Check whether user has owner-level access to the document.
*/
@ -210,8 +230,8 @@ export class GranularAccess {
*/
public filterMetaTables(docSession: OptDocSession,
tables: {[key: string]: TableDataAction}): {[key: string]: TableDataAction} {
// If there are no nuances, return immediately.
if (!this.hasNuancedAccess(docSession)) { return tables; }
// If user has right to read everything, return immediately.
if (this.canReadEverything(docSession)) { return tables; }
// If we are going to modify metadata, make a copy.
tables = JSON.parse(JSON.stringify(tables));
// Collect a list of all tables (by tableRef) to which the user has no access.