mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(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:
parent
e6e792655b
commit
f9630b3aa4
17
app/client/errorMain.ts
Normal file
17
app/client/errorMain.ts
Normal 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),
|
||||
]));
|
@ -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.
|
||||
|
@ -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(); }
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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;
|
||||
|
53
app/server/lib/MinimalLogin.ts
Normal file
53
app/server/lib/MinimalLogin.ts
Normal 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',
|
||||
};
|
||||
}
|
@ -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';
|
||||
}
|
||||
};
|
||||
|
@ -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)!;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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};
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -6,6 +6,7 @@ module.exports = {
|
||||
target: 'web',
|
||||
entry: {
|
||||
main: "app/client/app.js",
|
||||
errorPages: "app/client/errorMain.js",
|
||||
},
|
||||
output: {
|
||||
filename: "[name].bundle.js",
|
||||
|
@ -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 */ },
|
||||
|
@ -1,13 +1,9 @@
|
||||
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);
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user