(core) updates from grist-core

This commit is contained in:
Paul Fitzpatrick
2024-07-29 17:20:32 -04:00
11 changed files with 1181 additions and 136 deletions

View File

@@ -293,6 +293,25 @@ function initialize(appModel: AppModel) {
function requestInterceptor(request: SwaggerUI.Request) {
delete request.headers.Authorization;
const url = new URL(request.url);
// Swagger will use this request interceptor for several kinds of
// requests, such as requesting the API YAML spec from Github:
//
// Function to intercept remote definition, "Try it out",
// and OAuth 2.0 requests.
//
// https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/
//
// We want to ensure that only "Try it out" requests have XHR, so
// that they pass a same origin request, even if they're not GET,
// HEAD, or OPTIONS. "Try it out" requests are the requests to the
// same origin.
if (url.origin === window.origin) {
// Without this header, unauthenticated multipart POST requests
// (i.e. file uploads) would fail in the API console. We want those
// requests to succeed.
request.headers['X-Requested-With'] = 'XMLHttpRequest';
}
return request;
}

View File

@@ -6,7 +6,7 @@ import { GristServer } from 'app/server/lib/GristServer';
import * as express from 'express';
import WS from 'ws';
import fetch from 'node-fetch';
import { DEFAULT_SESSION_SECRET } from 'app/server/lib/coreCreator';
import { DEFAULT_SESSION_SECRET } from 'app/server/lib/ICreate';
/**
* Self-diagnostics useful when installing Grist.

View File

@@ -113,8 +113,6 @@ const SPECIAL_ACTIONS = new Set(['InitNewDoc',
'FillTransformRuleColIds',
'TransformAndFinishImport',
'AddView',
'CopyFromColumn',
'ConvertFromColumn',
'AddHiddenColumn',
'RespondToRequests',
]);
@@ -132,9 +130,7 @@ const OK_ACTIONS = new Set(['Calculate', 'UpdateCurrentTime']);
// Only add an action to OTHER_RECOGNIZED_ACTIONS if you know access control
// has been handled for it, or it is clear that access control can be done
// by looking at the Create/Update/Delete permissions for the DocActions it
// will create. For example, at the time of writing CopyFromColumn should
// not be here, since it could read a column the user is not supposed to
// have access rights to, and it is not handled specially.
// will create.
const OTHER_RECOGNIZED_ACTIONS = new Set([
// Data actions.
'AddRecord',
@@ -149,6 +145,11 @@ const OTHER_RECOGNIZED_ACTIONS = new Set([
'AddOrUpdateRecord',
'BulkAddOrUpdateRecord',
// Certain column actions are handled specially because of reads that
// don't fit the pattern of data actions.
'ConvertFromColumn',
'CopyFromColumn',
// Groups of actions.
'ApplyDocActions',
'ApplyUndoActions',
@@ -818,7 +819,7 @@ export class GranularAccess implements GranularAccessForBundle {
// Checks are in no particular order.
await this._checkSimpleDataActions(docSession, actions);
await this._checkForSpecialOrSurprisingActions(docSession, actions);
await this._checkPossiblePythonFormulaModification(docSession, actions);
await this._checkIfNeedsEarlySchemaPermission(docSession, actions);
await this._checkDuplicateTableAccess(docSession, actions);
await this._checkAddOrUpdateAccess(docSession, actions);
}
@@ -912,7 +913,14 @@ export class GranularAccess implements GranularAccessForBundle {
*/
public needEarlySchemaPermission(a: UserAction|DocAction): boolean {
const name = a[0] as string;
if (name === 'ModifyColumn' || name === 'SetDisplayFormula') {
if (name === 'ModifyColumn' || name === 'SetDisplayFormula' ||
// ConvertFromColumn and CopyFromColumn are hard to reason
// about, especially since they appear in bundles with other
// actions. We throw up our hands a bit here, and just make
// sure the user has schema permissions. Today, in Grist, that
// gives a lot of power. If this gets narrowed down in future,
// we'll have to rethink this.
name === 'ConvertFromColumn' || name === 'CopyFromColumn') {
return true;
} else if (isDataAction(a)) {
const tableId = getTableId(a);
@@ -1362,7 +1370,6 @@ export class GranularAccess implements GranularAccessForBundle {
}
await this._assertOnlyBundledWithSimpleDataActions(ADD_OR_UPDATE_RECORD_ACTIONS, actions);
// Check for read access, and that we're not touching metadata.
await applyToActionsRecursively(actions, async (a) => {
if (!isAddOrUpdateRecordAction(a)) { return; }
@@ -1392,12 +1399,15 @@ export class GranularAccess implements GranularAccessForBundle {
});
}
private async _checkPossiblePythonFormulaModification(docSession: OptDocSession, actions: UserAction[]) {
private async _checkIfNeedsEarlySchemaPermission(docSession: OptDocSession, actions: UserAction[]) {
// If changes could include Python formulas, then user must have
// +S before we even consider passing these to the data engine.
// Since we don't track rule or schema changes at this stage, we
// approximate with the user's access rights at beginning of
// bundle.
// We also check for +S in scenarios that are hard to break down
// in a more granular way, for example ConvertFromColumn and
// CopyFromColumn.
if (scanActionsRecursively(actions, (a) => this.needEarlySchemaPermission(a))) {
await this._assertSchemaAccess(docSession);
}

View File

@@ -13,6 +13,29 @@ import {createSandbox, SpawnFn} from 'app/server/lib/NSandbox';
import {SqliteVariant} from 'app/server/lib/SqliteCommon';
import {ITelemetry} from 'app/server/lib/Telemetry';
// In the past, the session secret was used as an additional
// protection passed on to expressjs-session for security when
// generating session IDs, in order to make them less guessable.
// Quoting the upstream documentation,
//
// Using a secret that cannot be guessed will reduce the ability
// to hijack a session to only guessing the session ID (as
// determined by the genid option).
//
// https://expressjs.com/en/resources/middleware/session.html
//
// However, since this change,
//
// https://github.com/gristlabs/grist-core/commit/24ce54b586e20a260376a9e3d5b6774e3fa2b8b8#diff-d34f5357f09d96e1c2ba63495da16aad7bc4c01e7925ab1e96946eacd1edb094R121-R124
//
// session IDs are now completely randomly generated in a cryptographically
// secure way, so there is no danger of session IDs being guessable.
// This makes the value of the session secret less important. The only
// concern is that changing the secret will invalidate existing
// sessions and force users to log in again.
export const DEFAULT_SESSION_SECRET =
'Phoo2ag1jaiz6Moo2Iese2xoaphahbai3oNg7diemohlah0ohtae9iengafieS2Hae7quungoCi9iaPh';
export interface ICreate {
Billing(dbManager: HomeDBManager, gristConfig: GristServer): IBilling;
@@ -72,6 +95,15 @@ export interface ICreateTelemetryOptions {
create(dbManager: HomeDBManager, gristConfig: GristServer): ITelemetry|undefined;
}
/**
* This function returns a `create` object that defines various core
* aspects of a Grist installation, such as what kind of billing or
* sandbox to use, if any.
*
* The intended use of this function is to initialise Grist with
* different settings and providers, to facilitate different editions
* such as standard, enterprise or cloud-hosted.
*/
export function makeSimpleCreator(opts: {
deploymentType: GristDeploymentType,
sessionSecret?: string,
@@ -116,11 +148,7 @@ export function makeSimpleCreator(opts: {
return createSandbox(opts.sandboxFlavor || 'unsandboxed', options);
},
sessionSecret() {
const secret = process.env.GRIST_SESSION_SECRET || sessionSecret;
if (!secret) {
throw new Error('need GRIST_SESSION_SECRET');
}
return secret;
return process.env.GRIST_SESSION_SECRET || sessionSecret || DEFAULT_SESSION_SECRET;
},
async configure() {
for (const s of storage || []) {

View File

@@ -3,14 +3,8 @@ import { checkMinIOBucket, checkMinIOExternalStorage,
import { makeSimpleCreator } from 'app/server/lib/ICreate';
import { Telemetry } from 'app/server/lib/Telemetry';
export const DEFAULT_SESSION_SECRET =
'Phoo2ag1jaiz6Moo2Iese2xoaphahbai3oNg7diemohlah0ohtae9iengafieS2Hae7quungoCi9iaPh';
export const makeCoreCreator = () => makeSimpleCreator({
deploymentType: 'core',
// This can and should be overridden by GRIST_SESSION_SECRET
// (or generated randomly per install, like grist-omnibus does).
sessionSecret: DEFAULT_SESSION_SECRET,
storage: [
{
name: 'minio',