(core) granular access control in the presence of schema changes

Summary:
 - Support schema changes in the presence of non-trivial ACL rules.
 - Fix update of `aclFormulaParsed` when updating formulas automatically after schema change.
 - Filter private metadata in broadcasts, not just fetches.  Censorship method is unchanged, just refactored.
 - Allow only owners to change ACL rules.
 - Force reloads if rules are changed.
 - Track rule changes within bundle, for clarity during schema changes - tableId and colId changes create a muddle otherwise.
 - Show or forbid pages dynamically depending on user's access to its sections. Logic unchanged, just no longer requires reload.
 - Fix calculation of pre-existing rows touched by a bundle, in the presence of schema changes.
 - Gray out acl page for non-owners.

Test Plan: added tests

Reviewers: dsagal

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D2734
This commit is contained in:
Paul Fitzpatrick
2021-03-01 11:51:30 -05:00
parent aae4a58300
commit 4ab096d179
18 changed files with 930 additions and 454 deletions

View File

@@ -447,32 +447,35 @@ export class Comm extends dispose.Disposable implements GristServerAPI, DocListA
const reqId = message.reqId;
const r = this.pendingRequests.get(reqId);
if (r) {
this.pendingRequests.delete(reqId);
if ('errorCode' in message && message.errorCode === 'AUTH_NO_VIEW') {
// We should only arrive here if the user had view access, and then lost it.
// We should not let the user see the document any more. Let's reload the
// page, reducing this to the problem of arriving at a document the user
// doesn't have access to, which is already handled.
console.log(`Comm response #${reqId} ${r.methodName} issued AUTH_NO_VIEW - closing`);
window.location.reload();
}
if (isCommResponseError(message)) {
const err: any = new Error(message.error);
let code = '';
if (message.errorCode) {
code = ` [${message.errorCode}]`;
err.code = message.errorCode;
try {
if ('errorCode' in message && message.errorCode === 'AUTH_NO_VIEW') {
// We should only arrive here if the user had view access, and then lost it.
// We should not let the user see the document any more. Let's reload the
// page, reducing this to the problem of arriving at a document the user
// doesn't have access to, which is already handled.
console.log(`Comm response #${reqId} ${r.methodName} issued AUTH_NO_VIEW - closing`);
window.location.reload();
}
if (message.details) {
err.details = message.details;
if (isCommResponseError(message)) {
const err: any = new Error(message.error);
let code = '';
if (message.errorCode) {
code = ` [${message.errorCode}]`;
err.code = message.errorCode;
}
if (message.details) {
err.details = message.details;
}
err.shouldFork = message.shouldFork;
console.log(`Comm response #${reqId} ${r.methodName} ERROR:${code} ${message.error}`
+ (message.shouldFork ? ` (should fork)` : ''));
r.reject(err);
} else {
console.log(`Comm response #${reqId} ${r.methodName} OK`);
r.resolve(message.data);
}
err.shouldFork = message.shouldFork;
console.log(`Comm response #${reqId} ${r.methodName} ERROR:${code} ${message.error}`
+ (message.shouldFork ? ` (should fork)` : ''));
r.reject(err);
} else {
console.log(`Comm response #${reqId} ${r.methodName} OK`);
r.resolve(message.data);
} finally {
this.pendingRequests.delete(reqId);
}
} else {
console.log("Comm: Response to unknown reqId " + reqId);
@@ -514,8 +517,8 @@ export class Comm extends dispose.Disposable implements GristServerAPI, DocListA
}
if (error) {
console.log("Comm: Rejecting req #" + reqId + " " + r.methodName + ": " + error);
this.pendingRequests.delete(reqId);
r.reject(new Error('Comm: ' + error));
this.pendingRequests.delete(reqId);
}
}

View File

@@ -17,6 +17,7 @@ export interface DocUserAction extends CommMessage {
data: {
docActions: DocAction[];
actionGroup: ActionGroup;
error?: string;
};
}
@@ -76,7 +77,15 @@ export class DocComm extends Disposable implements ActiveDocAPI {
this.listenTo(_comm, 'docShutdown', (m: CommMessage) => {
if (this.isActionFromThisDoc(m)) { this._isClosed = true; }
});
this.onDispose(() => this._shutdown());
this.onDispose(async () => {
try {
await this._shutdown();
} catch (e) {
if (!String(e).match(/GristWSConnection disposed/)) {
reportError(e);
}
}
});
}
// Returns the URL params that identifying this open document to the DocWorker

View File

@@ -293,6 +293,18 @@ export class GristDoc extends DisposableWithEvents {
public onDocUserAction(message: DocUserAction) {
console.log("GristDoc.onDocUserAction", message);
let schemaUpdated = false;
/**
* If an operation is applied successfully to a document, and then information about
* it is broadcast to clients, and one of those broadcasts has a failure (due to
* granular access control, which is client-specific), then that error is logged on
* the server and also sent to the client via an `error` field. Under normal operation,
* there should be no such errors, but if they do arise it is best to make them as visible
* as possible.
*/
if (message.data.error) {
reportError(new Error(message.data.error));
return;
}
if (this.docComm.isActionFromThisDoc(message)) {
const docActions = message.data.docActions;
for (let i = 0, len = docActions.length; i < len; i++) {