(core) clean up a collection of small problems affecting grist-core

Summary:
 * Remove adjustSession hack, interfering with loading docs under saml.
 * Allow the anonymous user to receive an empty list of workspaces for
   the merged org.
 * Behave better on first page load when org is in path - this used to
   fail because of lack of cookie.  This is very visible in grist-core,
   as a failure to load localhost:8484 on first visit.
 * Mark cookie explicitly as SameSite=Lax to remove a warning in firefox.
 * Make errorPages available in grist-core.

This changes the default behavior of grist-core to now start off in
anonymous mode, with an explicit sign-in step available.  If SAML is not configured,
the sign-in operation will unconditionally sign the user in as a default
user, without any password check or other security.  The user email is
taken from GRIST_DEFAULT_EMAIL if set.  This is a significant change, but
makes anonymous mode available in grist-core (which is convenient
for testing) and makes behavior with and without SAML much more consistent.

Test Plan: updated test; manual (time to start adding grist-core tests though!)

Reviewers: dsagal

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D2980
This commit is contained in:
Paul Fitzpatrick 2021-08-17 11:22:30 -04:00
parent e6e792655b
commit f9630b3aa4
17 changed files with 193 additions and 132 deletions

17
app/client/errorMain.ts Normal file
View File

@ -0,0 +1,17 @@
import {TopAppModelImpl} from 'app/client/models/AppModel';
import {setUpErrorHandling} from 'app/client/models/errors';
import {createErrPage} from 'app/client/ui/errorPages';
import {buildSnackbarDom} from 'app/client/ui/NotifyUI';
import {addViewportTag} from 'app/client/ui/viewport';
import {attachCssRootVars} from 'app/client/ui2018/cssVars';
import {dom} from 'grainjs';
// Set up the global styles for variables, and root/body styles.
setUpErrorHandling();
const topAppModel = TopAppModelImpl.create(null, {});
attachCssRootVars(topAppModel.productFlavor);
addViewportTag();
dom.update(document.body, dom.maybe(topAppModel.appObs, (appModel) => [
createErrPage(appModel),
buildSnackbarDom(appModel.notifier, appModel),
]));

View File

@ -774,7 +774,10 @@ export class HomeDBManager extends EventEmitter {
public async getOrgWorkspaces(scope: Scope, orgKey: string|number,
options: QueryOptions = {}): Promise<QueryResult<Workspace[]>> {
const query = this._orgWorkspaces(scope, orgKey, options);
const result = await this._verifyAclPermissions(query, { scope });
// Allow an empty result for the merged org for the anonymous user. The anonymous user
// has no home org or workspace. For all other sitations, expect at least one workspace.
const emptyAllowed = this.isMergedOrg(orgKey) && scope.userId === this.getAnonymousUserId();
const result = await this._verifyAclPermissions(query, { scope, emptyAllowed });
// Return the workspaces, not the org(s).
if (result.status === 200) {
// Place ownership information in workspaces, available for the merged org.

View File

@ -94,7 +94,6 @@ export function isSingleUserMode(): boolean {
* - req.users: set for org-and-session-based logins, with list of profiles in session
*/
export async function addRequestUser(dbManager: HomeDBManager, permitStore: IPermitStore,
fallbackEmail: string|null,
req: Request, res: Response, next: NextFunction) {
const mreq = req as RequestWithLogin;
let profile: UserProfile|undefined;
@ -234,18 +233,6 @@ export async function addRequestUser(dbManager: HomeDBManager, permitStore: IPer
}
}
if (!mreq.userId && fallbackEmail) {
const user = await dbManager.getUserByLogin(fallbackEmail);
if (user) {
mreq.user = user;
mreq.userId = user.id;
mreq.userIsAuthorized = true;
const fullUser = dbManager.makeFullUser(user);
mreq.users = [fullUser];
profile = fullUser;
}
}
// If no userId has been found yet, fall back on anonymous.
if (!mreq.userId) {
const anon = dbManager.getAnonymousUser();
@ -309,11 +296,16 @@ export function redirectToLogin(
if (mreq.userIsAuthorized) { return next(); }
try {
// Otherwise it's an anonymous user. Proceed normally only if the org allows anon access.
if (mreq.userId && mreq.org && allowExceptions) {
// Otherwise it's an anonymous user. Proceed normally only if the org allows anon access,
// or if the org is not set (FlexServer._redirectToOrg will deal with that case).
if (mreq.userId && allowExceptions) {
// Anonymous user has qualified access to merged org.
if (dbManager.isMergedOrg(mreq.org)) { return next(); }
const result = await dbManager.getOrg({userId: mreq.userId}, mreq.org || null);
// If no org is set, leave it to other middleware. One common case where the
// org is not set is when it is embedded in the url, and the user visits '/'.
// If we immediately require a login, it could fail if no cookie exists yet.
// Also, '/o/docs' allows anonymous access.
if (!mreq.org || dbManager.isMergedOrg(mreq.org)) { return next(); }
const result = await dbManager.getOrg({userId: mreq.userId}, mreq.org);
if (result.status === 200) { return next(); }
}

View File

@ -96,17 +96,17 @@ export class FlexServer implements GristServer {
public host: string;
public tag: string;
public info = new Array<[string, any]>();
public dbManager: HomeDBManager;
public notifier: INotifier;
public usage: Usage;
public housekeeper: Housekeeper;
public server: http.Server;
public httpsServer?: https.Server;
public comm: Comm;
public settings: any;
public worker: DocWorkerInfo;
public electronServerMethods: ElectronServerMethods;
public readonly docsRoot: string;
private _comm: Comm;
private _dbManager: HomeDBManager;
private _defaultBaseDomain: string|undefined;
private _pluginUrl: string|undefined;
private _billing: IBilling;
@ -249,6 +249,21 @@ export class FlexServer implements GristServer {
return this._sessions;
}
public getComm(): Comm {
if (!this._comm) { throw new Error('no Comm available'); }
return this._comm;
}
public getHosts(): Hosts {
if (!this._hosts) { throw new Error('no hosts available'); }
return this._hosts;
}
public getHomeDBManager(): HomeDBManager {
if (!this._dbManager) { throw new Error('no home db available'); }
return this._dbManager;
}
public addLogging() {
if (this._check('logging')) { return; }
if (process.env.GRIST_LOG_SKIP_HTTP) { return; }
@ -406,30 +421,17 @@ export class FlexServer implements GristServer {
// Prepare cache for managing org-to-host relationship.
public addHosts() {
if (this._check('hosts', 'homedb')) { return; }
this._hosts = new Hosts(this._defaultBaseDomain, this.dbManager, this._pluginUrl);
this._hosts = new Hosts(this._defaultBaseDomain, this._dbManager, this._pluginUrl);
}
public async initHomeDBManager() {
if (this._check('homedb')) { return; }
this.dbManager = new HomeDBManager();
this.dbManager.setPrefix(process.env.GRIST_ID_PREFIX || "");
await this.dbManager.connect();
await this.dbManager.initializeSpecialIds();
// If working without a login system, make sure default user exists.
if (process.env.GRIST_DEFAULT_EMAIL) {
const profile: UserProfile = {
name: 'You',
email: process.env.GRIST_DEFAULT_EMAIL,
};
const user = await this.dbManager.getUserByLoginWithRetry(profile.email, profile);
if (user) {
// No need to survey this user!
user.isFirstTimeUser = false;
await user.save();
}
}
this._dbManager = new HomeDBManager();
this._dbManager.setPrefix(process.env.GRIST_ID_PREFIX || "");
await this._dbManager.connect();
await this._dbManager.initializeSpecialIds();
// Report which database we are using, without sensitive credentials.
this.info.push(['database', getDatabaseUrl(this.dbManager.connection.options, false)]);
this.info.push(['database', getDatabaseUrl(this._dbManager.connection.options, false)]);
}
public addDocWorkerMap() {
@ -448,11 +450,7 @@ export class FlexServer implements GristServer {
// Middleware to redirect landing pages to preferred host
this._redirectToHostMiddleware = this._hosts.redirectHost;
// Middleware to add the userId to the express request object.
// If GRIST_DEFAULT_EMAIL is set, login as that user when no other credentials
// presented.
const fallbackEmail = process.env.GRIST_DEFAULT_EMAIL || null;
this._userIdMiddleware = expressWrap(addRequestUser.bind(null, this.dbManager, this._internalPermitStore,
fallbackEmail));
this._userIdMiddleware = expressWrap(addRequestUser.bind(null, this._dbManager, this._internalPermitStore));
this._trustOriginsMiddleware = expressWrap(trustOriginHandler);
// middleware to authorize doc access to the app. Note that this requires the userId
// to be set on the request by _userIdMiddleware.
@ -460,11 +458,11 @@ export class FlexServer implements GristServer {
this._redirectToLoginWithExceptionsMiddleware = redirectToLogin(true,
this._getLoginRedirectUrl,
this._getSignUpRedirectUrl,
this.dbManager);
this._dbManager);
this._redirectToLoginWithoutExceptionsMiddleware = redirectToLogin(false,
this._getLoginRedirectUrl,
this._getSignUpRedirectUrl,
this.dbManager);
this._dbManager);
this._redirectToLoginUnconditionally = redirectToLoginUnconditionally(this._getLoginRedirectUrl,
this._getSignUpRedirectUrl);
this._redirectToOrgMiddleware = tbind(this._redirectToOrg, this);
@ -519,7 +517,7 @@ export class FlexServer implements GristServer {
// ApiServer's constructor adds endpoints to the app.
// tslint:disable-next-line:no-unused-expression
new ApiServer(this.app, this.dbManager);
new ApiServer(this.app, this._dbManager);
}
public addBillingApi() {
@ -553,9 +551,9 @@ export class FlexServer implements GristServer {
public async close() {
if (this.usage) { await this.usage.close(); }
if (this._hosts) { this._hosts.close(); }
if (this.dbManager) {
this.dbManager.removeAllListeners();
this.dbManager.flushDocAuthCache();
if (this._dbManager) {
this._dbManager.removeAllListeners();
this._dbManager.flushDocAuthCache();
}
if (this.server) { this.server.close(); }
if (this.httpsServer) { this.httpsServer.close(); }
@ -568,7 +566,7 @@ export class FlexServer implements GristServer {
public addDocApiForwarder() {
if (this._check('doc_api_forwarder', '!json', 'homedb', 'api-mw', 'map')) { return; }
const docApiForwarder = new DocApiForwarder(this._docWorkerMap, this.dbManager);
const docApiForwarder = new DocApiForwarder(this._docWorkerMap, this._dbManager);
docApiForwarder.addEndpoints(this.app);
}
@ -605,9 +603,9 @@ export class FlexServer implements GristServer {
this._disabled = true;
} else {
this._disabled = true;
if (this.comm) {
this.comm.setServerActivation(false);
this.comm.destroyAllClients();
if (this._comm) {
this._comm.setServerActivation(false);
this._comm.destroyAllClients();
}
}
this.server.close();
@ -649,7 +647,7 @@ export class FlexServer implements GristServer {
if (this._storageManager) {
this._storageManager.testReopenStorage();
}
this.comm.setServerActivation(true);
this._comm.setServerActivation(true);
if (this.worker) {
await this._startServers(this.server, this.httpsServer, this.name, this.port, false);
await this._addSelfAsWorker(this._docWorkerMap);
@ -694,7 +692,7 @@ export class FlexServer implements GristServer {
// If "welcomeNewUser" is ever added to billing pages, we'd need
// to avoid a redirect loop.
const orgInfo = this.dbManager.unwrapQueryResult(await this.dbManager.getOrg({userId: user.id}, mreq.org));
const orgInfo = this._dbManager.unwrapQueryResult(await this._dbManager.getOrg({userId: user.id}, mreq.org));
if (orgInfo.billingAccount.isManager && orgInfo.billingAccount.product.features.vanityDomain) {
const prefix = isOrgInPathOnly(req.hostname) ? `/o/${mreq.org}` : '';
return res.redirect(`${prefix}/billing/payment?billingTask=signUpLite`);
@ -722,7 +720,7 @@ export class FlexServer implements GristServer {
forceLogin: this._redirectToLoginUnconditionally,
docWorkerMap: isSingleUserMode() ? null : this._docWorkerMap,
sendAppPage: this._sendAppPage,
dbManager: this.dbManager,
dbManager: this._dbManager,
plugins : (await this._addPluginManager()).getPlugins()
});
}
@ -755,7 +753,7 @@ export class FlexServer implements GristServer {
public addComm() {
if (this._check('comm', 'start')) { return; }
this.comm = new Comm(this.server, {
this._comm = new Comm(this.server, {
settings: this.settings,
sessions: this._sessions,
hosts: this._hosts,
@ -784,8 +782,8 @@ export class FlexServer implements GristServer {
}));
}
public addLoginRoutes() {
if (this._check('login', 'org', 'sessions')) { return; }
public async addLoginRoutes() {
if (this._check('login', 'org', 'sessions', 'homedb')) { return; }
// TODO: We do NOT want Comm here at all, it's only being used for handling sessions, which
// should be factored out of it.
this.addComm();
@ -869,13 +867,13 @@ export class FlexServer implements GristServer {
this.app.get('/verified', expressWrap((req, resp) =>
this._sendAppPage(req, resp, {path: 'error.html', status: 200, config: {errPage: 'verified'}})));
const comment = this._loginMiddleware.addEndpoints(this.app, this.comm, this._sessions, this._hosts);
const comment = await this._loginMiddleware.addEndpoints(this.app);
this.info.push(['loginMiddlewareComment', comment]);
}
public async addTestingHooks(workerServers?: FlexServer[]) {
if (process.env.GRIST_TESTING_SOCKET) {
await startTestingHooks(process.env.GRIST_TESTING_SOCKET, this.port, this.comm, this,
await startTestingHooks(process.env.GRIST_TESTING_SOCKET, this.port, this._comm, this,
workerServers || []);
this._hasTestingHooks = true;
}
@ -914,30 +912,30 @@ export class FlexServer implements GristServer {
const docWorkerId = await this._addSelfAsWorker(workers);
const storageManager = new HostedStorageManager(this.docsRoot, docWorkerId, this._disableS3, '', workers,
this.dbManager, this.create);
this._dbManager, this.create);
this._storageManager = storageManager;
} else {
const samples = getAppPathTo(this.appRoot, 'public_samples');
const storageManager = new DocStorageManager(this.docsRoot, samples, this.comm, this);
const storageManager = new DocStorageManager(this.docsRoot, samples, this._comm, this);
this._storageManager = storageManager;
}
const pluginManager = await this._addPluginManager();
this._docManager = this._docManager || new DocManager(this._storageManager, pluginManager,
this.dbManager, this);
this._dbManager, this);
const docManager = this._docManager;
shutdown.addCleanupHandler(null, this._shutdown.bind(this), 25000, 'FlexServer._shutdown');
if (!isSingleUserMode()) {
this.comm.registerMethods({
this._comm.registerMethods({
openDoc: docManager.openDoc.bind(docManager),
});
this._serveDocPage();
}
// Attach docWorker endpoints and Comm methods.
const docWorker = new DocWorker(this.dbManager, {comm: this.comm});
const docWorker = new DocWorker(this._dbManager, {comm: this._comm});
this._docWorker = docWorker;
// Register the websocket comm functions associated with the docworker.
@ -954,7 +952,7 @@ export class FlexServer implements GristServer {
this._addSupportPaths(docAccessMiddleware);
if (!isSingleUserMode()) {
addDocApiRoutes(this.app, docWorker, this._docWorkerMap, docManager, this.dbManager, this);
addDocApiRoutes(this.app, docWorker, this._docWorkerMap, docManager, this._dbManager, this);
}
}
@ -979,9 +977,9 @@ export class FlexServer implements GristServer {
return this._sendAppPage(req, resp, {path: 'error.html', status: 404, config: {errPage: 'not-found'}});
}
// Allow the support user access to billing pages.
const scope = addPermit(getScope(mreq), this.dbManager.getSupportUserId(), {org: orgDomain});
const query = await this.dbManager.getOrg(scope, orgDomain);
const org = this.dbManager.unwrapQueryResult(query);
const scope = addPermit(getScope(mreq), this._dbManager.getSupportUserId(), {org: orgDomain});
const query = await this._dbManager.getOrg(scope, orgDomain);
const org = this._dbManager.unwrapQueryResult(query);
// This page isn't availabe for personal site.
if (org.owner) {
return this._sendAppPage(req, resp, {path: 'error.html', status: 404, config: {errPage: 'not-found'}});
@ -1044,7 +1042,7 @@ export class FlexServer implements GristServer {
if (req.params.page === 'user') {
const name: string|undefined = req.body && req.body.username || undefined;
await this.dbManager.updateUser(userId, {name, isFirstTimeUser: false});
await this._dbManager.updateUser(userId, {name, isFirstTimeUser: false});
redirectPath = '/welcome/info';
} else if (req.params.page === 'info') {
@ -1062,8 +1060,8 @@ export class FlexServer implements GristServer {
//
// TODO With proper forms support, we could give an origin-based permission to submit a
// form to this doc, and do it from the client directly.
const previewerUserId = this.dbManager.getPreviewerUserId();
const docAuth = await this.dbManager.getDocAuthCached({urlId, userId: previewerUserId});
const previewerUserId = this._dbManager.getPreviewerUserId();
const docAuth = await this._dbManager.getDocAuthCached({urlId, userId: previewerUserId});
const docId = docAuth.docId;
if (!docId) {
throw new Error(`Can't resolve ${urlId}: ${docAuth.error}`);
@ -1089,14 +1087,14 @@ export class FlexServer implements GristServer {
// redirect to teams page if users has access to more than one org. Otherwise redirect to
// personal org.
const result = await this.dbManager.getMergedOrgs(userId, userId, domain || null);
const result = await this._dbManager.getMergedOrgs(userId, userId, domain || null);
const orgs = (result.status === 200) ? result.data : null;
if (orgs && orgs.length > 1) {
redirectPath = '/welcome/teams';
}
}
const mergedOrgDomain = this.dbManager.mergedOrgDomain();
const mergedOrgDomain = this._dbManager.mergedOrgDomain();
const redirectUrl = this._getOrgRedirectUrl(mreq, mergedOrgDomain, redirectPath);
resp.json({redirectUrl});
}),
@ -1160,7 +1158,7 @@ export class FlexServer implements GristServer {
// and all that is needed is a refactor to pass that info along. But there is also the
// case of notification(s) from stripe. May need to associate a preferred base domain
// with org/user and persist that?
this.notifier = this.create.Notifier(this.dbManager, this);
this.notifier = this.create.Notifier(this._dbManager, this);
}
public getGristConfig(): GristLoadConfig {
@ -1172,9 +1170,9 @@ export class FlexServer implements GristServer {
* the db for document details without including organization disambiguation.
*/
public async getDocUrl(docId: string): Promise<string> {
if (!this.dbManager) { throw new Error('database missing'); }
const doc = await this.dbManager.getDoc({
userId: this.dbManager.getPreviewerUserId(),
if (!this._dbManager) { throw new Error('database missing'); }
const doc = await this._dbManager.getDoc({
userId: this._dbManager.getPreviewerUserId(),
urlId: docId,
showAll: true
});
@ -1185,19 +1183,19 @@ export class FlexServer implements GristServer {
* Get a url for a team site.
*/
public async getOrgUrl(orgKey: string|number): Promise<string> {
if (!this.dbManager) { throw new Error('database missing'); }
const org = await this.dbManager.getOrg({
userId: this.dbManager.getPreviewerUserId(),
if (!this._dbManager) { throw new Error('database missing'); }
const org = await this._dbManager.getOrg({
userId: this._dbManager.getPreviewerUserId(),
showAll: true
}, orgKey);
return this.getResourceUrl(this.dbManager.unwrapQueryResult(org));
return this.getResourceUrl(this._dbManager.unwrapQueryResult(org));
}
/**
* Get a url for an organization, workspace, or document.
*/
public async getResourceUrl(resource: Organization|Workspace|Document): Promise<string> {
if (!this.dbManager) { throw new Error('database missing'); }
if (!this._dbManager) { throw new Error('database missing'); }
const gristConfig = this.getGristConfig();
const state: IGristUrlState = {};
let org: Organization;
@ -1211,20 +1209,20 @@ export class FlexServer implements GristServer {
state.doc = resource.urlId || resource.id;
state.slug = getSlugIfNeeded(resource);
}
state.org = this.dbManager.normalizeOrgDomain(org.id, org.domain, org.ownerId);
state.org = this._dbManager.normalizeOrgDomain(org.id, org.domain, org.ownerId);
if (!gristConfig.homeUrl) { throw new Error('Computing a resource URL requires a home URL'); }
return encodeUrl(gristConfig, state, new URL(gristConfig.homeUrl));
}
public addUsage() {
if (this._check('usage', 'start', 'homedb')) { return; }
this.usage = new Usage(this.dbManager);
this.usage = new Usage(this._dbManager);
}
public async addHousekeeper() {
if (this._check('housekeeper', 'start', 'homedb', 'map', 'json', 'api-mw')) { return; }
const store = this._docWorkerMap;
this.housekeeper = new Housekeeper(this.dbManager, this, this._internalPermitStore, store);
this.housekeeper = new Housekeeper(this._dbManager, this, this._internalPermitStore, store);
this.housekeeper.addEndpoints(this.app);
await this.housekeeper.start();
}
@ -1427,8 +1425,8 @@ export class FlexServer implements GristServer {
} catch (err) {
log.error("FlexServer shutdown problem", err);
}
if (this.comm) {
this.comm.destroyAllClients();
if (this._comm) {
this._comm.destroyAllClients();
}
log.info("FlexServer shutdown is complete");
}
@ -1439,12 +1437,19 @@ export class FlexServer implements GristServer {
*/
private async _redirectToOrg(req: express.Request, resp: express.Response, next: express.NextFunction) {
const mreq = req as RequestWithLogin;
if (mreq.org || !mreq.userId || !mreq.userIsAuthorized) { return next(); }
if (mreq.org || !mreq.userId) { return next(); }
// Redirect anonymous users to the merged org.
if (!mreq.userIsAuthorized) {
const redirectUrl = this._getOrgRedirectUrl(mreq, this._dbManager.mergedOrgDomain());
log.debug(`Redirecting anonymous user to: ${redirectUrl}`);
return resp.redirect(redirectUrl);
}
// We have a userId, but the request is for an unknown org. Redirect to an org that's
// available to the user. This matters in dev, and in prod when visiting a generic URL, which
// will here redirect to e.g. the user's personal org.
const result = await this.dbManager.getMergedOrgs(mreq.userId, mreq.userId, null);
const result = await this._dbManager.getMergedOrgs(mreq.userId, mreq.userId, null);
const orgs = (result.status === 200) ? result.data : null;
const subdomain = orgs && orgs.length > 0 ? orgs[0].domain : null;
const redirectUrl = subdomain && this._getOrgRedirectUrl(mreq, subdomain);
@ -1512,8 +1517,8 @@ export class FlexServer implements GristServer {
private _getBilling(): IBilling {
if (!this._billing) {
if (!this.dbManager) { throw new Error("need dbManager"); }
this._billing = this.create.Billing(this.dbManager, this);
if (!this._dbManager) { throw new Error("need dbManager"); }
this._billing = this.create.Billing(this._dbManager, this);
}
return this._billing;
}

View File

@ -2,6 +2,7 @@ import { GristLoadConfig } from 'app/common/gristUrls';
import { Document } from 'app/gen-server/entity/Document';
import { Organization } from 'app/gen-server/entity/Organization';
import { Workspace } from 'app/gen-server/entity/Workspace';
import { HomeDBManager } from 'app/gen-server/lib/HomeDBManager';
import * as Comm from 'app/server/lib/Comm';
import { Hosts } from 'app/server/lib/extractOrg';
import { ICreate } from 'app/server/lib/ICreate';
@ -25,6 +26,9 @@ export interface GristServer {
getPermitStore(): IPermitStore;
getExternalPermitStore(): IPermitStore;
getSessions(): Sessions;
getComm(): Comm;
getHosts(): Hosts;
getHomeDBManager(): HomeDBManager;
}
export interface GristLoginMiddleware {
@ -33,5 +37,5 @@ export interface GristLoginMiddleware {
getLogoutRedirectUrl(req: express.Request, nextUrl: URL): Promise<string>;
// Returns arbitrary string for log.
addEndpoints(app: express.Express, comm: Comm, sessions: Sessions, hosts: Hosts): string;
addEndpoints(app: express.Express): Promise<string>;
}

View File

@ -1,6 +1,5 @@
import {HomeDBManager} from 'app/gen-server/lib/HomeDBManager';
import {ActiveDoc} from 'app/server/lib/ActiveDoc';
import {ScopedSession} from 'app/server/lib/BrowserSession';
import {DocManager} from 'app/server/lib/DocManager';
import {ExternalStorage} from 'app/server/lib/ExternalStorage';
import {GristServer} from 'app/server/lib/GristServer';
@ -10,9 +9,6 @@ import {ISandbox, ISandboxCreationOptions} from 'app/server/lib/ISandbox';
import {IShell} from 'app/server/lib/IShell';
export interface ICreate {
// A ScopedSession knows which user is logged in to an org. This method may be used to replace
// its behavior with stubs when logins aren't available.
adjustSession(scopedSession: ScopedSession): void;
Billing(dbManager: HomeDBManager, gristConfig: GristServer): IBilling;
Notifier(dbManager: HomeDBManager, gristConfig: GristServer): INotifier;

View File

@ -0,0 +1,53 @@
import { UserProfile } from 'app/common/UserAPI';
import { GristLoginMiddleware, GristServer } from 'app/server/lib/GristServer';
import { Request } from 'express';
/**
* Return a login system that supports a single hard-coded user.
*/
export async function getMinimalLoginMiddleware(gristServer: GristServer): Promise<GristLoginMiddleware> {
// Login and logout, redirecting immediately back. Signup is treated as login,
// no nuance here.
return {
async getLoginRedirectUrl(req: Request, url: URL) {
await setSingleUser(req, gristServer);
return url.href;
},
async getLogoutRedirectUrl(req: Request, url: URL) {
return url.href;
},
async getSignUpRedirectUrl(req: Request, url: URL) {
await setSingleUser(req, gristServer);
return url.href;
},
async addEndpoints() {
// If working without a login system, make sure default user exists.
const dbManager = gristServer.getHomeDBManager();
const profile = getDefaultProfile();
const user = await dbManager.getUserByLoginWithRetry(profile.email, profile);
if (user) {
// No need to survey this user!
user.isFirstTimeUser = false;
await user.save();
}
return "no-logins";
}
};
}
/**
* Set the user in the current session to the single hard-coded user.
*/
async function setSingleUser(req: Request, gristServer: GristServer) {
const scopedSession = gristServer.getSessions().getOrCreateSessionFromRequest(req);
await scopedSession.operateOnScopedSession(async (user) => Object.assign(user, {
profile: getDefaultProfile()
}));
}
function getDefaultProfile(): UserProfile {
return {
email: process.env.GRIST_DEFAULT_EMAIL || 'you@example.com',
name: 'You',
};
}

View File

@ -57,9 +57,7 @@ import * as express from 'express';
import * as fse from 'fs-extra';
import * as saml2 from 'saml2-js';
import * as Comm from 'app/server/lib/Comm';
import {expressWrap} from 'app/server/lib/expressWrap';
import {Hosts} from 'app/server/lib/extractOrg';
import {GristLoginMiddleware, GristServer} from 'app/server/lib/GristServer';
import * as log from 'app/server/lib/log';
import {Permit} from 'app/server/lib/Permit';
@ -254,8 +252,8 @@ export async function getSamlLoginMiddleware(gristServer: GristServer): Promise<
// TODO: is there a better link to give here?
getSignUpRedirectUrl: samlConfig.getLoginRedirectUrl.bind(samlConfig),
getLogoutRedirectUrl: samlConfig.getLogoutRedirectUrl.bind(samlConfig),
addEndpoints(app: express.Express, comm: Comm, sessions: Sessions, hosts: Hosts) {
samlConfig.addSamlEndpoints(app, sessions);
async addEndpoints(app: express.Express) {
samlConfig.addSamlEndpoints(app, gristServer.getSessions());
return 'saml';
}
};

View File

@ -1,5 +1,4 @@
import {ScopedSession} from 'app/server/lib/BrowserSession';
import {GristServer} from 'app/server/lib/GristServer';
import {cookieName, SessionStore} from 'app/server/lib/gristSessions';
import * as cookie from 'cookie';
import * as cookieParser from 'cookie-parser';
@ -26,7 +25,7 @@ import {Request} from 'express';
export class Sessions {
private _sessions = new Map<string, ScopedSession>();
constructor(private _sessionSecret: string, private _sessionStore: SessionStore, private _server: GristServer) {
constructor(private _sessionSecret: string, private _sessionStore: SessionStore) {
}
/**
@ -47,7 +46,6 @@ export class Sessions {
const key = this._getSessionOrgKey(sid, domain, userSelector);
if (!this._sessions.has(key)) {
const scopedSession = new ScopedSession(sid, this._sessionStore, domain, userSelector);
this._server.create.adjustSession(scopedSession);
this._sessions.set(key, scopedSession);
}
return this._sessions.get(key)!;

View File

@ -81,7 +81,7 @@ export class TestingHooks implements ITestingHooks {
log.info("TestingHooks.setServerVersion called with", version);
this._comm.setServerVersion(version);
for (const server of this._workerServers) {
server.comm.setServerVersion(version);
server.getComm().setServerVersion(version);
}
}
@ -89,7 +89,7 @@ export class TestingHooks implements ITestingHooks {
log.info("TestingHooks.disconnectClients called");
this._comm.destroyAllClients();
for (const server of this._workerServers) {
server.comm.destroyAllClients();
server.getComm().destroyAllClients();
}
}
@ -97,7 +97,7 @@ export class TestingHooks implements ITestingHooks {
log.info("TestingHooks.commShutdown called");
await this._comm.testServerShutdown();
for (const server of this._workerServers) {
await server.comm.testServerShutdown();
await server.getComm().testServerShutdown();
}
}
@ -105,7 +105,7 @@ export class TestingHooks implements ITestingHooks {
log.info("TestingHooks.commRestart called");
await this._comm.testServerRestart();
for (const server of this._workerServers) {
await server.comm.testServerRestart();
await server.getComm().testServerRestart();
}
}
@ -115,7 +115,7 @@ export class TestingHooks implements ITestingHooks {
log.info("TestingHooks.setClientPersistence called with", ttlMs);
this._comm.testSetClientPersistence(ttlMs);
for (const server of this._workerServers) {
server.comm.testSetClientPersistence(ttlMs);
server.getComm().testSetClientPersistence(ttlMs);
}
}
@ -153,9 +153,9 @@ export class TestingHooks implements ITestingHooks {
public async flushAuthorizerCache(): Promise<void> {
log.info("TestingHooks.flushAuthorizerCache called");
this._server.dbManager.flushDocAuthCache();
this._server.getHomeDBManager().flushDocAuthCache();
for (const server of this._workerServers) {
server.dbManager.flushDocAuthCache();
server.getHomeDBManager().flushDocAuthCache();
}
}

View File

@ -132,6 +132,8 @@ export function initGristSessions(instanceRoot: string, server: GristServer) {
requestDomain: getCookieDomain,
genid: generateId,
cookie: {
sameSite: 'lax',
// We do not initially set max-age, leaving the cookie as a
// session cookie until there's a successful login. On the
// redis back-end, the session associated with the cookie will
@ -144,7 +146,7 @@ export function initGristSessions(instanceRoot: string, server: GristServer) {
store: sessionStore
});
const sessions = new Sessions(sessionSecret, sessionStore, server);
const sessions = new Sessions(sessionSecret, sessionStore);
return {sessions, sessionSecret, sessionStore, sessionMiddleware, sessionStoreCreator};
}

View File

@ -117,7 +117,7 @@ export async function main(port: number, serverTypes: ServerType[],
server.addNotifier();
await server.addHousekeeper();
}
server.addLoginRoutes();
await server.addLoginRoutes();
server.addBillingPages();
server.addWelcomePaths();
server.addLogEndpoint();

View File

@ -6,6 +6,7 @@ module.exports = {
target: 'web',
entry: {
main: "app/client/app.js",
errorPages: "app/client/errorMain.js",
},
output: {
filename: "[name].bundle.js",

View File

@ -1,17 +1,11 @@
import {ActiveDoc} from 'app/server/lib/ActiveDoc';
import {ICreate} from 'app/server/lib/ICreate';
import {ScopedSession} from 'app/server/lib/BrowserSession';
import {NSandboxCreator} from 'app/server/lib/NSandbox';
// Use raw python - update when pynbox or other solution is set up for core.
const sandboxCreator = new NSandboxCreator({defaultFlavor: 'unsandboxed'});
export const create: ICreate = {
adjustSession(scopedSession: ScopedSession): void {
const email = process.env.GRIST_DEFAULT_EMAIL || 'anon@getgrist.com';
const profile = {email, name: email};
scopedSession.getSessionProfile = async () => profile;
},
Billing() {
return {
addEndpoints() { /* do nothing */ },

View File

@ -1,13 +1,9 @@
import {GristLoginMiddleware, GristServer} from 'app/server/lib/GristServer';
import {getSamlLoginMiddleware} from 'app/server/lib/SamlConfig';
import { GristLoginMiddleware, GristServer } from 'app/server/lib/GristServer';
import { getMinimalLoginMiddleware } from 'app/server/lib/MinimalLogin';
import { getSamlLoginMiddleware } from 'app/server/lib/SamlConfig';
export async function getLoginMiddleware(gristServer: GristServer): Promise<GristLoginMiddleware> {
const saml = await getSamlLoginMiddleware(gristServer);
if (saml) { return saml; }
return {
async getLoginRedirectUrl() { throw new Error('logins not implemented'); },
async getLogoutRedirectUrl() { throw new Error('logins not implemented'); },
async getSignUpRedirectUrl() { throw new Error('logins not implemented'); },
addEndpoints() { return "no-logins"; }
};
return getMinimalLoginMiddleware(gristServer);
}

View File

@ -15,8 +15,8 @@ if (!debugging) {
setDefaultEnv('GRIST_LOG_SKIP_HTTP', 'true');
}
// Use a distinct cookie.
setDefaultEnv('GRIST_SESSION_COOKIE', 'grist_core');
// Use a distinct cookie. Bump version to 2.
setDefaultEnv('GRIST_SESSION_COOKIE', 'grist_core2');
import {updateDb} from 'app/server/lib/dbUtils';
import {main as mergedServerMain} from 'app/server/mergedServerMain';
@ -41,7 +41,7 @@ export async function main() {
console.log('For full logs, re-run with DEBUG=1');
}
// If SAML is not configured, there's no login system, so force a default email address.
// If SAML is not configured, there's no login system, so provide a default email address.
if (!process.env.GRIST_SAML_SP_HOST) {
setDefaultEnv('GRIST_DEFAULT_EMAIL', 'you@example.com');
}

View File

@ -1617,6 +1617,8 @@ export async function openUserProfile() {
// Since the AccountWidget loads orgs and the user data asynchronously, the menu
// can expand itself causing the click to land on a wrong button.
await waitForServer();
await driver.findWait('.test-usermenu-org', 1000);
await driver.sleep(250); // There's still some jitter (scroll-bar? other user accounts?)
await driver.findContent('.grist-floating-menu li', 'Profile Settings').click();
await driver.findWait('.test-login-method', 5000);
}