mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) back-end support for tables that are accessible only by owners
Summary:
This makes it possible to serve a table or tables only to owners.
* The _grist_ACLResources table is abused (temporarily) such that rows of the form `{colId: '~o', tableId}` are interpreted as meaning that `tableId` is private to owners.
* Many websocket and api endpoints are updated to preserve the privacy of these tables.
* In a document where some tables are private, a lot of capabilities are turned off for non-owners to avoid leaking info indirectly.
* The client is tweaked minimally, to show '-' where a page with some private material would otherwise go.
No attempt is made to protect data from private tables pulled into non-private tables via formulas.
There are some known leaks remaining:
* Changes to the schema of private tables are still broadcast to all clients (fixable).
* Non-owner may be able to access snapshots or make forks or use other corners of API (fixable).
* Changing name of table makes it public, since tableId in ACLResource is not updated (fixable).
Security will require some work, the attack surface is large.
Test Plan: added tests
Reviewers: dsagal
Reviewed By: dsagal
Differential Revision: https://phab.getgrist.com/D2604
This commit is contained in:
@@ -7,8 +7,7 @@ import * as express from 'express';
|
||||
import fetch, {RequestInit, Response as FetchResponse} from 'node-fetch';
|
||||
|
||||
import {ApiError} from 'app/common/ApiError';
|
||||
import {getSlugIfNeeded, isOrgInPathOnly,
|
||||
parseSubdomainStrictly} from 'app/common/gristUrls';
|
||||
import {getSlugIfNeeded, parseSubdomainStrictly} from 'app/common/gristUrls';
|
||||
import {removeTrailingSlash} from 'app/common/gutil';
|
||||
import {Document as APIDocument} from 'app/common/UserAPI';
|
||||
import {Document} from "app/gen-server/entity/Document";
|
||||
@@ -19,7 +18,7 @@ import {DocStatus, IDocWorkerMap} from 'app/server/lib/DocWorkerMap';
|
||||
import {expressWrap} from 'app/server/lib/expressWrap';
|
||||
import {getAssignmentId} from 'app/server/lib/idUtils';
|
||||
import * as log from 'app/server/lib/log';
|
||||
import {adaptServerUrl, pruneAPIResult, trustOrigin} from 'app/server/lib/requestUtils';
|
||||
import {adaptServerUrl, addOrgToPathIfNeeded, pruneAPIResult, trustOrigin} from 'app/server/lib/requestUtils';
|
||||
import {ISendAppPageOptions} from 'app/server/lib/sendAppPage';
|
||||
|
||||
export interface AttachOptions {
|
||||
@@ -199,16 +198,13 @@ export function attachAppEndpoint(options: AttachOptions): void {
|
||||
const preferredUrlId = doc.urlId || doc.id;
|
||||
if (urlId !== preferredUrlId || slugMismatch) {
|
||||
// Prepare to redirect to canonical url for document.
|
||||
// Preserve org in url path if necessary.
|
||||
const prefix = isOrgInPathOnly(req.hostname) ? `/o/${mreq.org}` : '';
|
||||
// Preserve any query parameters or fragments.
|
||||
const queryOrFragmentCheck = req.originalUrl.match(/([#?].*)/);
|
||||
const queryOrFragment = (queryOrFragmentCheck && queryOrFragmentCheck[1]) || '';
|
||||
if (slug) {
|
||||
res.redirect(`${prefix}/${preferredUrlId}/${slug}${req.params.remainder}${queryOrFragment}`);
|
||||
} else {
|
||||
res.redirect(`${prefix}/doc/${preferredUrlId}${req.params.remainder}${queryOrFragment}`);
|
||||
}
|
||||
const target = slug ?
|
||||
`/${preferredUrlId}/${slug}${req.params.remainder}${queryOrFragment}` :
|
||||
`/doc/${preferredUrlId}${req.params.remainder}${queryOrFragment}`;
|
||||
res.redirect(addOrgToPathIfNeeded(req, target));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user