mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
e5c24eb5ea
Summary: This changes how user attributes are loaded. They are now loaded directly from sqlite, with per-session caching. Optimizations considered but not addressed yet are (1) adding indexes to user attribute tables and (2) swapping in a thinner sqlite wrapper. The main benefit of this diff is that changes to user attribute tables now work. Clients whose user attributes are not changed see no effect; clients whose user attributes have changed have their document reloaded. For the purposes of testing, the diff includes a tweak to GristWSConnection to be "sticky" to a specific user when reloading (and support machinery on the server side to honor that). Until now, if a GristWSConnection reloads, it uses whatever the current default user is in the cookie-based session, which can change. This was complicating a test where multiple users were accessing the same document via different clients with occasional document reloads. Code for updating when schema or rule changes happen is moved around but not improved in any meaningful way in this diff. Test Plan: existing tests pass; extended test Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2685
111 lines
4.2 KiB
TypeScript
111 lines
4.2 KiB
TypeScript
import {ScopedSession} from 'app/server/lib/BrowserSession';
|
|
import * as Comm from 'app/server/lib/Comm';
|
|
import {GristServer} from 'app/server/lib/GristServer';
|
|
import {cookieName, SessionStore} from 'app/server/lib/gristSessions';
|
|
import {IInstanceManager} from 'app/server/lib/IInstanceManager';
|
|
import {ILoginSession} from 'app/server/lib/ILoginSession';
|
|
import * as cookie from 'cookie';
|
|
import * as cookieParser from 'cookie-parser';
|
|
import {Request} from 'express';
|
|
|
|
interface Session {
|
|
scopedSession: ScopedSession;
|
|
loginSession?: ILoginSession;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* A collection of all the sessions relevant to this instance of Grist.
|
|
*
|
|
* This collection was previously maintained by the Comm object. This
|
|
* class is added as a stepping stone to disentangling session management
|
|
* from code related to websockets.
|
|
*
|
|
* The collection caches all existing interfaces to sessions.
|
|
* LoginSessions play an important role in standalone Grist and address
|
|
* end-to-end sharing concerns. ScopedSessions play an important role in
|
|
* hosted Grist and address per-organization scoping of identity.
|
|
*
|
|
* TODO: now this is separated out, we could refactor to share sessions
|
|
* across organizations. Currently, when a user moves between organizations,
|
|
* the session interfaces are not shared. This was for simplicity in working
|
|
* with existing code.
|
|
*
|
|
*/
|
|
export class Sessions {
|
|
private _sessions = new Map<string, Session>();
|
|
|
|
constructor(private _sessionSecret: string, private _sessionStore: SessionStore, private _server: GristServer) {
|
|
}
|
|
|
|
/**
|
|
* Get the session id and organization from the request, and return the
|
|
* identified session.
|
|
*/
|
|
public getOrCreateSessionFromRequest(req: Request): Session {
|
|
const sid = this.getSessionIdFromRequest(req);
|
|
const org = (req as any).org;
|
|
if (!sid) { throw new Error("session not found"); }
|
|
return this.getOrCreateSession(sid, org, ''); // TODO: allow for tying to a preferred user.
|
|
}
|
|
|
|
/**
|
|
* Get or create a session given the session id and organization name.
|
|
*/
|
|
public getOrCreateSession(sid: string, domain: string, userSelector: string): Session {
|
|
const key = this._getSessionOrgKey(sid, domain, userSelector);
|
|
if (!this._sessions.has(key)) {
|
|
const scopedSession = new ScopedSession(sid, this._sessionStore, domain, userSelector);
|
|
this._sessions.set(key, {scopedSession});
|
|
}
|
|
return this._sessions.get(key)!;
|
|
}
|
|
|
|
/**
|
|
* Access a LoginSession interface, creating it if necessary. For creation,
|
|
* purposes, Comm, and optionally InstanceManager objects are needed.
|
|
*
|
|
*/
|
|
public getOrCreateLoginSession(sid: string, domain: string, comm: Comm,
|
|
instanceManager: IInstanceManager|null,
|
|
userSelector: string): ILoginSession {
|
|
const sess = this.getOrCreateSession(sid, domain, userSelector);
|
|
if (!sess.loginSession) {
|
|
sess.loginSession = this._server.create.LoginSession(comm, sid, domain, sess.scopedSession,
|
|
instanceManager);
|
|
}
|
|
return sess.loginSession;
|
|
}
|
|
|
|
/**
|
|
* Returns the sessionId from the signed grist cookie.
|
|
*/
|
|
public getSessionIdFromCookie(gristCookie: string) {
|
|
return cookieParser.signedCookie(gristCookie, this._sessionSecret);
|
|
}
|
|
|
|
/**
|
|
* Get the session id from the grist cookie. Returns null if no cookie found.
|
|
*/
|
|
public getSessionIdFromRequest(req: Request): string|null {
|
|
if (req.headers.cookie) {
|
|
const cookies = cookie.parse(req.headers.cookie);
|
|
const sessionId = this.getSessionIdFromCookie(cookies[cookieName]);
|
|
return sessionId;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get a per-organization, per-session key.
|
|
* Grist has historically cached sessions in memory by their session id.
|
|
* With the introduction of per-organization identity, that cache is now
|
|
* needs to be keyed by the session id and organization name.
|
|
* Also, clients may now want to be tied to a particular user available within
|
|
* a session, so we add that into key too.
|
|
*/
|
|
private _getSessionOrgKey(sid: string, domain: string, userSelector: string): string {
|
|
return `${sid}__${domain}__${userSelector}`;
|
|
}
|
|
}
|