mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) add a sandbox check to admin panel, and start reconciling boot and admin pages
Summary: This adds a basic sandbox check to the admin panel. It also makes the "probes" used in the boot page available from the admin panel, though they are not yet displayed. The sandbox check is built as a probe. In the interests of time, a lot of steps had to be deferred: * Reconcile fully the admin panel and boot page. Specifically, the admin panel should be equally robust to common configuration problems. * Add tests for the sandbox check. * Generalize to multi-server setups. The read-out will not yet be useful for setups where doc workers and home servers are configured separately. Test Plan: Added new test Reviewers: jarek, georgegevoian Reviewed By: georgegevoian Differential Revision: https://phab.getgrist.com/D4241
This commit is contained in:
@@ -95,12 +95,14 @@ import {checksumFile} from 'app/server/lib/checksumFile';
|
||||
import {Client} from 'app/server/lib/Client';
|
||||
import {getMetaTables} from 'app/server/lib/DocApi';
|
||||
import {DEFAULT_CACHE_TTL, DocManager} from 'app/server/lib/DocManager';
|
||||
import {GristServer} from 'app/server/lib/GristServer';
|
||||
import {ICreateActiveDocOptions} from 'app/server/lib/ICreate';
|
||||
import {makeForkIds} from 'app/server/lib/idUtils';
|
||||
import {GRIST_DOC_SQL, GRIST_DOC_WITH_TABLE1_SQL} from 'app/server/lib/initialDocSql';
|
||||
import {ISandbox} from 'app/server/lib/ISandbox';
|
||||
import log from 'app/server/lib/log';
|
||||
import {LogMethods} from "app/server/lib/LogMethods";
|
||||
import {ISandboxOptions} from 'app/server/lib/NSandbox';
|
||||
import {NullSandbox, UnavailableSandboxMethodError} from 'app/server/lib/NullSandbox';
|
||||
import {DocRequests} from 'app/server/lib/Requests';
|
||||
import {shortDesc} from 'app/server/lib/shortDesc';
|
||||
@@ -2764,11 +2766,9 @@ export class ActiveDoc extends EventEmitter {
|
||||
}
|
||||
}
|
||||
}
|
||||
return this._docManager.gristServer.create.NSandbox({
|
||||
comment: this._docName,
|
||||
logCalls: false,
|
||||
logTimes: true,
|
||||
logMeta: {docId: this._docName},
|
||||
return createSandbox({
|
||||
server: this._docManager.gristServer,
|
||||
docId: this._docName,
|
||||
preferredPythonVersion,
|
||||
sandboxOptions: {
|
||||
exports: {
|
||||
@@ -2951,3 +2951,23 @@ export async function getRealTableId(
|
||||
export function sanitizeApplyUAOptions(options?: ApplyUAOptions): ApplyUAOptions {
|
||||
return pick(options||{}, ['desc', 'otherId', 'linkId', 'parseStrings']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a sandbox in its default initial state and with default logging.
|
||||
*/
|
||||
export function createSandbox(options: {
|
||||
server: GristServer,
|
||||
docId: string,
|
||||
preferredPythonVersion: '2' | '3' | undefined,
|
||||
sandboxOptions?: Partial<ISandboxOptions>,
|
||||
}) {
|
||||
const {docId, preferredPythonVersion, sandboxOptions, server} = options;
|
||||
return server.create.NSandbox({
|
||||
comment: docId,
|
||||
logCalls: false,
|
||||
logTimes: true,
|
||||
logMeta: {docId},
|
||||
preferredPythonVersion,
|
||||
sandboxOptions,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,13 +18,16 @@ export class BootProbes {
|
||||
|
||||
public constructor(private _app: express.Application,
|
||||
private _server: GristServer,
|
||||
private _base: string) {
|
||||
private _base: string,
|
||||
private _middleware: express.Handler[] = []) {
|
||||
this._addProbes();
|
||||
}
|
||||
|
||||
public addEndpoints() {
|
||||
// Return a list of available probes.
|
||||
this._app.use(`${this._base}/probe$`, expressWrap(async (_, res) => {
|
||||
this._app.use(`${this._base}/probe$`,
|
||||
...this._middleware,
|
||||
expressWrap(async (_, res) => {
|
||||
res.json({
|
||||
'probes': this._probes.map(probe => {
|
||||
return { id: probe.id, name: probe.name };
|
||||
@@ -33,7 +36,9 @@ export class BootProbes {
|
||||
}));
|
||||
|
||||
// Return result of running an individual probe.
|
||||
this._app.use(`${this._base}/probe/:probeId`, expressWrap(async (req, res) => {
|
||||
this._app.use(`${this._base}/probe/:probeId`,
|
||||
...this._middleware,
|
||||
expressWrap(async (req, res) => {
|
||||
const probe = this._probeById.get(req.params.probeId);
|
||||
if (!probe) {
|
||||
throw new ApiError('unknown probe', 400);
|
||||
@@ -52,6 +57,7 @@ export class BootProbes {
|
||||
this._probes.push(_userProbe);
|
||||
this._probes.push(_bootProbe);
|
||||
this._probes.push(_hostHeaderProbe);
|
||||
this._probes.push(_sandboxingProbe);
|
||||
this._probeById = new Map(this._probes.map(p => [p.id, p]));
|
||||
}
|
||||
}
|
||||
@@ -183,3 +189,16 @@ const _hostHeaderProbe: Probe = {
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
const _sandboxingProbe: Probe = {
|
||||
id: 'sandboxing',
|
||||
name: 'Sandboxing is working',
|
||||
apply: async (server, req) => {
|
||||
const details = server.getSandboxInfo();
|
||||
return {
|
||||
success: details?.configured && details?.functional,
|
||||
details,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import {getOrgUrlInfo} from 'app/common/gristUrls';
|
||||
import {isAffirmative, safeJsonParse} from 'app/common/gutil';
|
||||
import {InstallProperties} from 'app/common/InstallAPI';
|
||||
import {UserProfile} from 'app/common/LoginSessionAPI';
|
||||
import {SandboxInfo} from 'app/common/SandboxInfo';
|
||||
import {tbind} from 'app/common/tbind';
|
||||
import * as version from 'app/common/version';
|
||||
import {ApiServer, getOrgFromRequest} from 'app/gen-server/ApiServer';
|
||||
@@ -23,6 +24,7 @@ import {HomeDBManager} from 'app/gen-server/lib/HomeDBManager';
|
||||
import {Housekeeper} from 'app/gen-server/lib/Housekeeper';
|
||||
import {Usage} from 'app/gen-server/lib/Usage';
|
||||
import {AccessTokens, IAccessTokens} from 'app/server/lib/AccessTokens';
|
||||
import {createSandbox} from 'app/server/lib/ActiveDoc';
|
||||
import {attachAppEndpoint} from 'app/server/lib/AppEndpoint';
|
||||
import {appSettings} from 'app/server/lib/AppSettings';
|
||||
import {addRequestUser, getTransitiveHeaders, getUser, getUserId, isAnonymousUser,
|
||||
@@ -42,7 +44,7 @@ import {expressWrap, jsonErrorHandler, secureJsonErrorHandler} from 'app/server/
|
||||
import {Hosts, RequestWithOrg} from 'app/server/lib/extractOrg';
|
||||
import {addGoogleAuthEndpoint} from "app/server/lib/GoogleAuth";
|
||||
import {DocTemplate, GristLoginMiddleware, GristLoginSystem, GristServer,
|
||||
RequestWithGrist} from 'app/server/lib/GristServer';
|
||||
RequestWithGrist} from 'app/server/lib/GristServer';
|
||||
import {initGristSessions, SessionStore} from 'app/server/lib/gristSessions';
|
||||
import {HostedStorageManager} from 'app/server/lib/HostedStorageManager';
|
||||
import {IBilling} from 'app/server/lib/IBilling';
|
||||
@@ -181,6 +183,7 @@ export class FlexServer implements GristServer {
|
||||
private _isReady: boolean = false;
|
||||
private _probes: BootProbes;
|
||||
private _updateManager: UpdateManager;
|
||||
private _sandboxInfo: SandboxInfo;
|
||||
|
||||
constructor(public port: number, public name: string = 'flexServer',
|
||||
public readonly options: FlexServerOptions = {}) {
|
||||
@@ -1367,6 +1370,47 @@ export class FlexServer implements GristServer {
|
||||
}
|
||||
}
|
||||
|
||||
public async checkSandbox() {
|
||||
if (this._check('sandbox', 'doc')) { return; }
|
||||
const flavor = process.env.GRIST_SANDBOX_FLAVOR || 'unknown';
|
||||
const info = this._sandboxInfo = {
|
||||
flavor,
|
||||
configured: flavor !== 'unsandboxed',
|
||||
functional: false,
|
||||
effective: false,
|
||||
sandboxed: false,
|
||||
lastSuccessfulStep: 'none',
|
||||
} as SandboxInfo;
|
||||
try {
|
||||
const sandbox = createSandbox({
|
||||
server: this,
|
||||
docId: 'test', // The id is just used in logging - no
|
||||
// document is created or read at this level.
|
||||
// In olden times, and in SaaS, Python 2 is supported. In modern
|
||||
// times Python 2 is long since deprecated and defunct.
|
||||
preferredPythonVersion: '3',
|
||||
});
|
||||
info.flavor = sandbox.getFlavor();
|
||||
info.configured = info.flavor !== 'unsandboxed';
|
||||
info.lastSuccessfulStep = 'create';
|
||||
const result = await sandbox.pyCall('get_version');
|
||||
if (typeof result !== 'number') {
|
||||
throw new Error(`Expected a number: ${result}`);
|
||||
}
|
||||
info.lastSuccessfulStep = 'use';
|
||||
await sandbox.shutdown();
|
||||
info.lastSuccessfulStep = 'all';
|
||||
info.functional = true;
|
||||
info.effective = ![ 'skip', 'unsandboxed' ].includes(info.flavor);
|
||||
} catch (e) {
|
||||
info.error = String(e);
|
||||
}
|
||||
}
|
||||
|
||||
public getSandboxInfo(): SandboxInfo|undefined {
|
||||
return this._sandboxInfo;
|
||||
}
|
||||
|
||||
public disableExternalStorage() {
|
||||
if (this.deps.has('doc')) {
|
||||
throw new Error('disableExternalStorage called too late');
|
||||
@@ -1827,6 +1871,8 @@ export class FlexServer implements GristServer {
|
||||
this.app.get('/admin', ...adminPageMiddleware, expressWrap(async (req, resp) => {
|
||||
return this.sendAppPage(req, resp, {path: 'app.html', status: 200, config: {}});
|
||||
}));
|
||||
const probes = new BootProbes(this.app, this, '/admin', adminPageMiddleware);
|
||||
probes.addEndpoints();
|
||||
|
||||
// Restrict this endpoint to install admins too, for the same reason as the /admin page.
|
||||
this.app.get('/api/install/prefs', requireInstallAdmin, expressWrap(async (_req, resp) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ICustomWidget } from 'app/common/CustomWidget';
|
||||
import { GristDeploymentType, GristLoadConfig } from 'app/common/gristUrls';
|
||||
import { LocalPlugin } from 'app/common/plugin';
|
||||
import { SandboxInfo } from 'app/common/SandboxInfo';
|
||||
import { UserProfile } from 'app/common/UserAPI';
|
||||
import { Document } from 'app/gen-server/entity/Document';
|
||||
import { Organization } from 'app/gen-server/entity/Organization';
|
||||
@@ -64,6 +65,7 @@ export interface GristServer {
|
||||
servesPlugins(): boolean;
|
||||
getBundledWidgets(): ICustomWidget[];
|
||||
hasBoot(): boolean;
|
||||
getSandboxInfo(): SandboxInfo|undefined;
|
||||
}
|
||||
|
||||
export interface GristLoginSystem {
|
||||
@@ -154,6 +156,7 @@ export function createDummyGristServer(): GristServer {
|
||||
getPlugins() { return []; },
|
||||
getBundledWidgets() { return []; },
|
||||
hasBoot() { return false; },
|
||||
getSandboxInfo() { return undefined; },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface ISandbox {
|
||||
shutdown(): Promise<unknown>; // TODO: tighten up this type.
|
||||
pyCall(funcName: string, ...varArgs: unknown[]): Promise<any>;
|
||||
reportMemoryUsage(): Promise<void>;
|
||||
getFlavor(): string;
|
||||
}
|
||||
|
||||
export interface ISandboxCreator {
|
||||
|
||||
@@ -230,6 +230,10 @@ export class NSandbox implements ISandbox {
|
||||
log.rawDebug('Sandbox memory', {memory, ...this._logMeta});
|
||||
}
|
||||
|
||||
public getFlavor() {
|
||||
return this._logMeta.flavor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ready to communicate with a sandbox process using stdin,
|
||||
* stdout, and stderr.
|
||||
@@ -466,6 +470,10 @@ const spawners = {
|
||||
gvisor, // Gvisor's runsc sandbox.
|
||||
macSandboxExec, // Use "sandbox-exec" on Mac.
|
||||
pyodide, // Run data engine using pyodide.
|
||||
skip: unsandboxed, // Same as unsandboxed. Used to mean that the
|
||||
// user deliberately doesn't want sandboxing.
|
||||
// The "unsandboxed" setting is ambiguous in this
|
||||
// respect.
|
||||
};
|
||||
|
||||
function isFlavor(flavor: string): flavor is keyof typeof spawners {
|
||||
|
||||
@@ -18,4 +18,8 @@ export class NullSandbox implements ISandbox {
|
||||
public async reportMemoryUsage() {
|
||||
throw new UnavailableSandboxMethodError('reportMemoryUsage is not available');
|
||||
}
|
||||
|
||||
public getFlavor() {
|
||||
return 'null';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +179,12 @@ export async function main(port: number, serverTypes: ServerType[],
|
||||
server.checkOptionCombinations();
|
||||
server.summary();
|
||||
server.ready();
|
||||
|
||||
// Some tests have their timing perturbed by having this earlier
|
||||
// TODO: update those tests.
|
||||
if (includeDocs) {
|
||||
await server.checkSandbox();
|
||||
}
|
||||
return server;
|
||||
} catch(e) {
|
||||
await server.close();
|
||||
|
||||
Reference in New Issue
Block a user