(core) Remove LoginSession, which was mainly serving situations that are no longer used.

Summary:
In the past, Cognito sign-ins were intended to give authorization to some AWS
services (like SQS); various tokens were stored in the session for this
purpose. This is no longer used. Profiles from Cognito now serve a limited
purpose: first-time initialization of name and picture, and keeping track of
which login method was used. For these remaining needs, ScopedSession is
sufficient.

Test Plan:
Existing test pass. Tested manually that logins work with Google and
Email + Password. Tested manually that on a clean database, name and picture
are picked up from a Google Login.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D2907
This commit is contained in:
Dmitry S
2021-07-12 12:10:04 -04:00
parent f079ffdcb3
commit 869b2f00ec
14 changed files with 95 additions and 132 deletions

View File

@@ -4,7 +4,7 @@ declare module "app/server/lib/User";
declare module "app/server/lib/Comm" {
import {Client, ClientMethod} from "app/server/lib/Client";
import {LoginSession} from "app/server/lib/LoginSession";
import {ScopedSession} from "app/server/lib/BrowserSession";
import * as http from "http";
class Comm {
@@ -14,7 +14,7 @@ declare module "app/server/lib/Comm" {
public setServerVersion(serverVersion: string|null): void;
public setServerActivation(active: boolean): void;
public getSessionIdFromCookie(gristSidCookie: string): string;
public getOrCreateSession(sessionId: string, req: any): LoginSession;
public getOrCreateSession(sessionId: string, req: any): ScopedSession;
public registerMethods(methods: {[name: string]: ClientMethod}): void;
public getClient(clientId: string): Client;
public testServerShutdown(): Promise<void>;

View File

@@ -1164,7 +1164,7 @@ export class ActiveDoc extends EventEmitter {
await this._granularAccess.assertCanMaybeApplyUserActions(docSession, actions);
const user = docSession.mode === 'system' ? 'grist' :
(client && client.session ? (await client.session.getEmail()) : "");
(client?.getProfile()?.email || '');
// Create the UserActionBundle.
const action: UserActionBundle = {

View File

@@ -8,19 +8,19 @@ export interface SessionUserObj {
// a grist-internal identify for the user, if known.
userId?: number;
// The user profile object. When updated, all clients get a message with the update.
// The user profile object.
profile?: UserProfile;
// Authentication provider string indicating the login method used.
// [UNUSED] Authentication provider string indicating the login method used.
authProvider?: string;
// Login ID token used to access AWS services.
// [UNUSED] Login ID token used to access AWS services.
idToken?: string;
// Login access token used to access other AWS services.
// [UNUSED] Login access token used to access other AWS services.
accessToken?: string;
// Login refresh token used to retrieve new ID and access tokens.
// [UNUSED] Login refresh token used to retrieve new ID and access tokens.
refreshToken?: string;
}
@@ -133,6 +133,26 @@ export class ScopedSession {
return getSessionUser(session, this._org, this._userSelector) || {};
}
// Retrieves the user profile from the session.
public async getSessionProfile(prev?: SessionObj): Promise<UserProfile|null> {
return (await this.getScopedSession(prev)).profile || null;
}
// Updates a user profile. The session may have multiple profiles associated with different
// email addresses. This will update the one with a matching email address, or add a new one.
// This is mainly used to know which emails are logged in in this session; fields like name and
// picture URL come from the database instead.
public async updateUserProfile(profile: UserProfile|null): Promise<void> {
if (profile) {
await this.operateOnScopedSession(async user => {
user.profile = profile;
return user;
});
} else {
await this.clearScopedSession();
}
}
/**
*
* This performs an operation on the session object, limited to a single user entry. The state of that

View File

@@ -8,9 +8,9 @@ import {User} from 'app/gen-server/entity/User';
import {HomeDBManager} from 'app/gen-server/lib/HomeDBManager';
import {ActiveDoc} from 'app/server/lib/ActiveDoc';
import {Authorizer} from 'app/server/lib/Authorizer';
import {ScopedSession} from 'app/server/lib/BrowserSession';
import {DocSession} from 'app/server/lib/DocSession';
import * as log from 'app/server/lib/log';
import {ILoginSession} from 'app/server/lib/ILoginSession';
import {shortDesc} from 'app/server/lib/shortDesc';
import * as crypto from 'crypto';
import * as moment from 'moment';
@@ -61,10 +61,10 @@ void(MESSAGE_TYPES_NO_AUTH);
export class Client {
public readonly clientId: string;
public session: ILoginSession|null = null;
public browserSettings: BrowserSettings = {};
private _session: ScopedSession|null = null;
// Maps docFDs to DocSession objects.
private _docFDs: Array<DocSession|null> = [];
@@ -221,21 +221,16 @@ export class Client {
}
}
// Assigns the client to the given login session and the session to the client.
public setSession(session: ILoginSession): void {
this.unsetSession();
this.session = session;
session.clients.add(this);
// Assigns the given ScopedSession to the client.
public setSession(session: ScopedSession): void {
this._session = session;
}
// Unsets the current login session and removes the client from it.
public unsetSession(): void {
if (this.session) { this.session.clients.delete(this); }
this.session = null;
public getSession(): ScopedSession|null {
return this._session;
}
public destroy() {
this.unsetSession();
this._destroyed = true;
}
@@ -318,6 +313,14 @@ export class Client {
return this._profile;
}
public async getSessionProfile(): Promise<UserProfile|null|undefined> {
return this._session?.getSessionProfile();
}
public async getSessionEmail(): Promise<string|null> {
return (await this.getSessionProfile())?.email || null;
}
public getCachedUserId(): number|null {
return this._userId;
}

View File

@@ -78,7 +78,7 @@ function Comm(server, options) {
this._clients = {}; // Maps clientIds to Client objects.
this.clientList = []; // List of all active Clients, ordered by clientId.
// Maps sessionIds to LoginSession objects.
// Maps sessionIds to ScopedSession objects.
this.sessions = options.sessions;
this._settings = options.settings;
@@ -118,14 +118,13 @@ Comm.prototype.getClient = function(clientId) {
};
/**
* Returns a LoginSession object with the given session id from the list of sessions,
* Returns a ScopedSession object with the given session id from the list of sessions,
* or adds a new one and returns that.
*/
Comm.prototype.getOrCreateSession = function(sid, req, userSelector) {
// LoginSessions are specific to a session id / org combination.
// ScopedSessions are specific to a session id / org combination.
const org = req.org || "";
return this.sessions.getOrCreateLoginSession(sid, org, this,
userSelector);
return this.sessions.getOrCreateSession(sid, org, userSelector);
};
@@ -230,13 +229,13 @@ Comm.prototype._onWebSocketConnection = async function(websocket, req) {
// Add a Session object to the client.
log.info(`Comm ${client}: using session ${sessionId}`);
const loginSession = this.getOrCreateSession(sessionId, req, userSelector);
client.setSession(loginSession);
const scopedSession = this.getOrCreateSession(sessionId, req, userSelector);
client.setSession(scopedSession);
// Delegate message handling to the client
websocket.on('message', client.onMessage.bind(client));
loginSession.getSessionProfile()
scopedSession.getSessionProfile()
.then((profile) => {
log.debug(`Comm ${client}: sending clientConnect with ` +
`${client._missedMessages.length} missed messages`);

View File

@@ -788,15 +788,12 @@ export class FlexServer implements GristServer {
this.app.get('/test/login', expressWrap(async (req, res) => {
log.warn("Serving unauthenticated /test/login endpoint, made available because GRIST_TEST_LOGIN is set.");
const session = this.sessions.getOrCreateSessionFromRequest(req);
const scopedSession = this.sessions.getOrCreateSessionFromRequest(req);
const profile: UserProfile = {
email: optStringParam(req.query.email) || 'chimpy@getgrist.com',
name: optStringParam(req.query.name) || 'Chimpy McBanana',
};
await session.scopedSession.operateOnScopedSession(async user => {
user.profile = profile;
return user;
});
await scopedSession.updateUserProfile(profile);
res.send(`<!doctype html>
<html><body>
<p>Logged in as ${JSON.stringify(profile)}.<p>
@@ -811,8 +808,8 @@ export class FlexServer implements GristServer {
}
this.app.get('/logout', expressWrap(async (req, resp) => {
const session = this.sessions.getOrCreateSessionFromRequest(req);
const userSession = await session.scopedSession.getScopedSession();
const scopedSession = this.sessions.getOrCreateSessionFromRequest(req);
const userSession = await scopedSession.getScopedSession();
// If 'next' param is missing, redirect to "/" on our requested hostname.
const next = optStringParam(req.query.next) || (req.protocol + '://' + req.get('host') + '/');
@@ -823,9 +820,7 @@ export class FlexServer implements GristServer {
// Express-session will save these changes.
const expressSession = (req as any).session;
if (expressSession) { expressSession.users = []; expressSession.orgToUser = {}; }
if (session.loginSession) {
await session.loginSession.clearSession();
}
await scopedSession.clearScopedSession();
resp.redirect(redirectUrl);
}));

View File

@@ -1,18 +1,19 @@
import {HomeDBManager} from 'app/gen-server/lib/HomeDBManager';
import {ActiveDoc} from 'app/server/lib/ActiveDoc';
import {ScopedSession} from 'app/server/lib/BrowserSession';
import * as Comm from 'app/server/lib/Comm';
import {DocManager} from 'app/server/lib/DocManager';
import {ExternalStorage} from 'app/server/lib/ExternalStorage';
import {GristServer} from 'app/server/lib/GristServer';
import {IBilling} from 'app/server/lib/IBilling';
import {ILoginSession} from 'app/server/lib/ILoginSession';
import {INotifier} from 'app/server/lib/INotifier';
import {ISandbox, ISandboxCreationOptions} from 'app/server/lib/ISandbox';
import {IShell} from 'app/server/lib/IShell';
export interface ICreate {
LoginSession(comm: Comm, sid: string, domain: string, scopeSession: ScopedSession): ILoginSession;
// 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;
Shell(): IShell|undefined;

View File

@@ -1,13 +0,0 @@
import {UserProfile} from 'app/common/LoginSessionAPI';
import {Client} from 'app/server/lib/Client';
export interface ILoginSession {
clients: Set<Client>;
getEmail(): Promise<string>;
getSessionProfile(): Promise<UserProfile|null>;
// Log out
clearSession(): Promise<void>;
// For testing only. If no email address, profile is wiped, otherwise it is set.
testSetProfile(profile: UserProfile|null): Promise<void>;
}

View File

@@ -1,17 +1,10 @@
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 {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.
@@ -21,8 +14,7 @@ interface Session {
* 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
* 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
@@ -32,7 +24,7 @@ interface Session {
*
*/
export class Sessions {
private _sessions = new Map<string, Session>();
private _sessions = new Map<string, ScopedSession>();
constructor(private _sessionSecret: string, private _sessionStore: SessionStore, private _server: GristServer) {
}
@@ -41,7 +33,7 @@ export class Sessions {
* Get the session id and organization from the request, and return the
* identified session.
*/
public getOrCreateSessionFromRequest(req: Request): Session {
public getOrCreateSessionFromRequest(req: Request): ScopedSession {
const sid = this.getSessionIdFromRequest(req);
const org = (req as any).org;
if (!sid) { throw new Error("session not found"); }
@@ -51,29 +43,16 @@ export class Sessions {
/**
* Get or create a session given the session id and organization name.
*/
public getOrCreateSession(sid: string, domain: string, userSelector: string): Session {
public getOrCreateSession(sid: string, domain: string, userSelector: string): ScopedSession {
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});
this._server.create.adjustSession(scopedSession);
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,
userSelector: string): ILoginSession {
const sess = this.getOrCreateSession(sid, domain, userSelector);
if (!sess.loginSession) {
sess.loginSession = this._server.create.LoginSession(comm, sid, domain, sess.scopedSession);
}
return sess.loginSession;
}
/**
* Returns the sessionId from the signed grist cookie.
*/

View File

@@ -73,8 +73,8 @@ export class TestingHooks implements ITestingHooks {
public async setLoginSessionProfile(gristSidCookie: string, profile: UserProfile|null, org?: string): Promise<void> {
log.info("TestingHooks.setLoginSessionProfile called with", gristSidCookie, profile, org);
const sessionId = this._comm.getSessionIdFromCookie(gristSidCookie);
const loginSession = this._comm.getOrCreateSession(sessionId, {org});
return await loginSession.testSetProfile(profile);
const scopedSession = this._comm.getOrCreateSession(sessionId, {org});
return await scopedSession.updateUserProfile(profile);
}
public async setServerVersion(version: string|null): Promise<void> {