mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
11fe3e90d4
This checks whether code can successfully run in the sandbox only when the admin panel needs to report that, rather than at start up. This is motivated by two things: - The desktop app became a lot slower to open with this check, since it uses pyodide by default, and there's been no work on optimizing the pyodide sandbox load times (as opposed to gvisor, where a lot of work was done, and it is also fundamentally faster). - The messages logged by a test sandbox starting and stopping have been confusing people. There is a case for doing the check on startup, especially on servers, so that we can fail early. Still, that isn't what we were doing, and we'd also like to move away from the server refusing to start because of a problem and towards an always-reachable admin page that reports the nature of problems in a clearer way.
192 lines
8.1 KiB
TypeScript
192 lines
8.1 KiB
TypeScript
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';
|
|
import { User } from 'app/gen-server/entity/User';
|
|
import { Workspace } from 'app/gen-server/entity/Workspace';
|
|
import { Activations } from 'app/gen-server/lib/Activations';
|
|
import { HomeDBManager } from 'app/gen-server/lib/homedb/HomeDBManager';
|
|
import { IAuditLogger } from 'app/server/lib/AuditLogger';
|
|
import { IAccessTokens } from 'app/server/lib/AccessTokens';
|
|
import { RequestWithLogin } from 'app/server/lib/Authorizer';
|
|
import { Comm } from 'app/server/lib/Comm';
|
|
import { create } from 'app/server/lib/create';
|
|
import { Hosts } from 'app/server/lib/extractOrg';
|
|
import { GristJobs } from 'app/server/lib/GristJobs';
|
|
import { ICreate } from 'app/server/lib/ICreate';
|
|
import { IDocStorageManager } from 'app/server/lib/IDocStorageManager';
|
|
import { INotifier } from 'app/server/lib/INotifier';
|
|
import { InstallAdmin } from 'app/server/lib/InstallAdmin';
|
|
import { IPermitStore } from 'app/server/lib/Permit';
|
|
import { ISendAppPageOptions } from 'app/server/lib/sendAppPage';
|
|
import { fromCallback } from 'app/server/lib/serverUtils';
|
|
import { Sessions } from 'app/server/lib/Sessions';
|
|
import { ITelemetry } from 'app/server/lib/Telemetry';
|
|
import * as express from 'express';
|
|
import { IncomingMessage } from 'http';
|
|
import { IGristCoreConfig, loadGristCoreConfig } from "./configCore";
|
|
|
|
/**
|
|
* Basic information about a Grist server. Accessible in many
|
|
* contexts, including request handlers and ActiveDoc methods.
|
|
*/
|
|
export interface GristServer {
|
|
readonly create: ICreate;
|
|
settings?: IGristCoreConfig;
|
|
getHost(): string;
|
|
getHomeUrl(req: express.Request, relPath?: string): string;
|
|
getHomeInternalUrl(relPath?: string): string;
|
|
getHomeUrlByDocId(docId: string, relPath?: string): Promise<string>;
|
|
getOwnUrl(): string;
|
|
getOrgUrl(orgKey: string|number): Promise<string>;
|
|
getMergedOrgUrl(req: RequestWithLogin, pathname?: string): string;
|
|
getResourceUrl(resource: Organization|Workspace|Document,
|
|
purpose?: 'api'|'html'): Promise<string>;
|
|
getGristConfig(): GristLoadConfig;
|
|
getPermitStore(): IPermitStore;
|
|
getExternalPermitStore(): IPermitStore;
|
|
getSessions(): Sessions;
|
|
getComm(): Comm;
|
|
getDeploymentType(): GristDeploymentType;
|
|
getHosts(): Hosts;
|
|
getActivations(): Activations;
|
|
getInstallAdmin(): InstallAdmin;
|
|
getHomeDBManager(): HomeDBManager;
|
|
getStorageManager(): IDocStorageManager;
|
|
getAuditLogger(): IAuditLogger;
|
|
getTelemetry(): ITelemetry;
|
|
hasNotifier(): boolean;
|
|
getNotifier(): INotifier;
|
|
getDocTemplate(): Promise<DocTemplate>;
|
|
getTag(): string;
|
|
sendAppPage(req: express.Request, resp: express.Response, options: ISendAppPageOptions): Promise<void>;
|
|
getAccessTokens(): IAccessTokens;
|
|
resolveLoginSystem(): Promise<GristLoginSystem>;
|
|
getPluginUrl(): string|undefined;
|
|
getPlugins(): LocalPlugin[];
|
|
servesPlugins(): boolean;
|
|
getBundledWidgets(): ICustomWidget[];
|
|
getBootKey(): string|undefined;
|
|
getSandboxInfo(): Promise<SandboxInfo>;
|
|
getInfo(key: string): any;
|
|
getJobs(): GristJobs;
|
|
}
|
|
|
|
export interface GristLoginSystem {
|
|
getMiddleware(gristServer: GristServer): Promise<GristLoginMiddleware>;
|
|
deleteUser(user: User): Promise<void>;
|
|
}
|
|
|
|
export interface GristLoginMiddleware {
|
|
getLoginRedirectUrl(req: express.Request, target: URL): Promise<string>;
|
|
getSignUpRedirectUrl(req: express.Request, target: URL): Promise<string>;
|
|
getLogoutRedirectUrl(req: express.Request, nextUrl: URL): Promise<string>;
|
|
// Optional middleware for the GET /login, /signup, and /signin routes.
|
|
getLoginOrSignUpMiddleware?(): express.RequestHandler[];
|
|
// Optional middleware for the GET /logout route.
|
|
getLogoutMiddleware?(): express.RequestHandler[];
|
|
// Optional middleware for all routes.
|
|
getWildcardMiddleware?(): express.RequestHandler[];
|
|
// Returns arbitrary string for log.
|
|
addEndpoints(app: express.Express): Promise<string>;
|
|
// Normally, the profile is obtained from the user's session object, which is set at login, and
|
|
// is identified by a session cookie. When given, overrideProfile() will be called first to
|
|
// extract the profile from each request. Result can be a profile, or null if anonymous
|
|
// (sessions will then not be used), or undefined to fall back to using session info.
|
|
overrideProfile?(req: express.Request|IncomingMessage): Promise<UserProfile|null|undefined>;
|
|
// Called on first visit to an app page after a signup, for reporting or telemetry purposes.
|
|
onFirstVisit?(req: express.Request): void;
|
|
}
|
|
|
|
/**
|
|
* Set the user in the current session.
|
|
*/
|
|
export async function setUserInSession(req: express.Request, gristServer: GristServer, profile: UserProfile) {
|
|
const scopedSession = gristServer.getSessions().getOrCreateSessionFromRequest(req);
|
|
// Make sure session is up to date before operating on it.
|
|
// Behavior on a completely fresh session is a little awkward currently.
|
|
const reqSession = (req as any).session;
|
|
if (reqSession?.save) {
|
|
await fromCallback(cb => reqSession.save(cb));
|
|
}
|
|
await scopedSession.updateUserProfile(req, profile);
|
|
}
|
|
|
|
export interface RequestWithGrist extends express.Request {
|
|
gristServer?: GristServer;
|
|
}
|
|
|
|
export interface DocTemplate {
|
|
page: string,
|
|
tag: string,
|
|
}
|
|
|
|
/**
|
|
* A very minimal GristServer object that throws an error if its bluff is
|
|
* called.
|
|
*/
|
|
export function createDummyGristServer(): GristServer {
|
|
return {
|
|
create,
|
|
settings: loadGristCoreConfig(),
|
|
getHost() { return 'localhost:4242'; },
|
|
getHomeUrl() { return 'http://localhost:4242'; },
|
|
getHomeInternalUrl() { return 'http://localhost:4242'; },
|
|
getHomeUrlByDocId() { return Promise.resolve('http://localhost:4242'); },
|
|
getMergedOrgUrl() { return 'http://localhost:4242'; },
|
|
getOwnUrl() { return 'http://localhost:4242'; },
|
|
getPermitStore() { throw new Error('no permit store'); },
|
|
getExternalPermitStore() { throw new Error('no external permit store'); },
|
|
getGristConfig() { return { homeUrl: '', timestampMs: 0 }; },
|
|
getOrgUrl() { return Promise.resolve(''); },
|
|
getResourceUrl() { return Promise.resolve(''); },
|
|
getSessions() { throw new Error('no sessions'); },
|
|
getComm() { throw new Error('no comms'); },
|
|
getDeploymentType() { return 'core'; },
|
|
getHosts() { throw new Error('no hosts'); },
|
|
getActivations() { throw new Error('no activations'); },
|
|
getInstallAdmin() { throw new Error('no install admin'); },
|
|
getHomeDBManager() { throw new Error('no db'); },
|
|
getStorageManager() { throw new Error('no storage manager'); },
|
|
getAuditLogger() { return createDummyAuditLogger(); },
|
|
getTelemetry() { return createDummyTelemetry(); },
|
|
getNotifier() { throw new Error('no notifier'); },
|
|
hasNotifier() { return false; },
|
|
getDocTemplate() { throw new Error('no doc template'); },
|
|
getTag() { return 'tag'; },
|
|
sendAppPage() { return Promise.resolve(); },
|
|
getAccessTokens() { throw new Error('no access tokens'); },
|
|
resolveLoginSystem() { throw new Error('no login system'); },
|
|
getPluginUrl() { return undefined; },
|
|
servesPlugins() { return false; },
|
|
getPlugins() { return []; },
|
|
getBundledWidgets() { return []; },
|
|
getBootKey() { return undefined; },
|
|
getSandboxInfo() { throw new Error('no sandbox'); },
|
|
getInfo(key: string) { return undefined; },
|
|
getJobs(): GristJobs { throw new Error('no job system'); },
|
|
};
|
|
}
|
|
|
|
export function createDummyAuditLogger(): IAuditLogger {
|
|
return {
|
|
logEvent() { /* do nothing */ },
|
|
logEventAsync() { return Promise.resolve(); },
|
|
};
|
|
}
|
|
|
|
export function createDummyTelemetry(): ITelemetry {
|
|
return {
|
|
addEndpoints() { /* do nothing */ },
|
|
start() { return Promise.resolve(); },
|
|
logEvent() { /* do nothing */ },
|
|
logEventAsync() { return Promise.resolve(); },
|
|
shouldLogEvent() { return false; },
|
|
getTelemetryConfig() { return undefined; },
|
|
fetchTelemetryPrefs() { return Promise.resolve(); },
|
|
};
|
|
}
|