Skip to content

Commit bee7d84

Browse files
authored
Server tests to validate we don't pollute the session store when using an API key (#2246)
## Context Making Rest API calls pollutes session store uselessly. To be exact, this issue has been addressed recently: e6a3351 ## Proposed solution As the issue is already addressed, I suggest to add non-regression tests to ensure this won't happen anymore. Handling anonymous sessions is trickier and will be done in a future PR. ## Has this been tested? <!-- Put an `x` in the box that applies: --> - [x] 👍 yes, I added tests to the test suite - [ ] 💭 no, because this PR is a draft and still needs work - [ ] 🙅 no, because this is not relevant here - [ ] 🙋 no, because I need help <!-- Detail how we can help you -->
1 parent 01eb26e commit bee7d84

4 files changed

Lines changed: 54 additions & 7 deletions

File tree

app/server/lib/Authorizer.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,16 @@ import { User } from "app/gen-server/entity/User";
99
import { HomeDBManager } from "app/gen-server/lib/homedb/HomeDBManager";
1010
import { DocAuthResult, HomeDBAuth } from "app/gen-server/lib/homedb/Interfaces";
1111
import { AccessTokenInfo } from "app/server/lib/AccessTokens";
12-
import { forceSessionChange, getSessionProfiles, getSessionUser, getSignInStatus, linkOrgWithEmail, SessionObj,
13-
SessionUserObj, SignInStatus } from "app/server/lib/BrowserSession";
12+
import {
13+
forceSessionChange, generateAltSessionID, getSessionProfiles,
14+
getSessionUser, getSignInStatus, linkOrgWithEmail, SessionObj, SessionUserObj, SignInStatus,
15+
} from "app/server/lib/BrowserSession";
1416
import { expressWrap } from "app/server/lib/expressWrap";
1517
import { RequestWithOrg } from "app/server/lib/extractOrg";
1618
import { GristServer } from "app/server/lib/GristServer";
1719
import { COOKIE_MAX_AGE,
1820
cookieName as sessionCookieName, getAllowedOrgForSessionID, getCookieDomain } from "app/server/lib/gristSessions";
1921
import { getBootKey } from "app/server/lib/gristSettings";
20-
import { makeId } from "app/server/lib/idUtils";
2122
import log from "app/server/lib/log";
2223
import { IPermitStore, Permit } from "app/server/lib/Permit";
2324
import { allowHost, buildXForwardedForHeader, getOriginUrl, optStringParam } from "app/server/lib/requestUtils";
@@ -490,11 +491,10 @@ export async function addRequestUser(
490491
// access-token requests are GETs for attachments (no trigger formulas) and
491492
// boot-key requests are admin-only. The alternative — threading authDone through
492493
// the IdentityResult — would complicate the interface for no practical benefit.
493-
if (!skipSession) {
494+
if (!skipSession && !identity.hasApiKey) {
494495
const session = mreq.session;
495496
if (session && !session.altSessionId) {
496-
session.altSessionId = makeId();
497-
forceSessionChange(session);
497+
generateAltSessionID(session);
498498
}
499499
mreq.altSessionId = session?.altSessionId;
500500
}

app/server/lib/BrowserSession.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { normalizeEmail } from "app/common/emails";
22
import { UserProfile } from "app/common/LoginSessionAPI";
33
import { SessionStore } from "app/server/lib/gristSessions";
4+
import { makeId } from "app/server/lib/idUtils";
45
import log from "app/server/lib/log";
56
import { fromCallback } from "app/server/lib/serverUtils";
67

@@ -87,7 +88,23 @@ export interface SessionOIDCInfo {
8788
idToken?: string;
8889
}
8990

90-
// Make an artificial change to a session to encourage express-session to set a cookie.
91+
/**
92+
* Create a default alternative session id for use in documents.
93+
*
94+
* This sessionID is also used client side on formulas (`user.SessionID`) when the user is anonymous
95+
*
96+
* Also force express-session to persist the sessionin the store.
97+
*/
98+
export function generateAltSessionID(session: SessionObj) {
99+
session.altSessionId = makeId();
100+
forceSessionChange(session);
101+
}
102+
103+
/**
104+
* Make an artificial change to a session to encourage express-session to set a cookie.
105+
*
106+
* This way, express-session will persist the session in the store.
107+
*/
91108
export function forceSessionChange(session: SessionObj) {
92109
session.alive = Number(session.alive || 0) + 1;
93110
}

app/server/lib/gristSessions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface SessionStore {
2828
setAsync(sid: string, session: any): Promise<void>;
2929
clearAsync(): Promise<void>;
3030
close(): Promise<void>;
31+
lengthAsync(): Promise<number>;
3132
}
3233

3334
/**

test/server/lib/Authorizer.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ async function activateServer(home: FlexServer, docManager: DocManager) {
4949
serverUrl = home.getOwnUrl();
5050
}
5151

52+
/**
53+
* Count the number of sessions in the session store (whenever we use SQLite or Redis).
54+
*/
55+
function countSessions(flexServer: FlexServer = server): Promise<number> {
56+
const store = flexServer["_sessionStore"];
57+
return store.lengthAsync();
58+
}
59+
60+
const anon = configForUser("Anonymous");
5261
const chimpy = configForUser("Chimpy");
5362
const charon = configForUser("Charon");
5463

@@ -151,6 +160,26 @@ describe("Authorizer", function() {
151160
assert.equal(resp.status, 404);
152161
});
153162

163+
it("does not generate sessions when using an API key to avoid polluting " +
164+
"the store session needlessly", async function() {
165+
const nbSessionsBefore = await countSessions();
166+
const resp = await axios.get(`${serverUrl}/api/orgs`, chimpy);
167+
const nbSessionsAfter = await countSessions();
168+
assert.equal(resp.status, 200);
169+
assert.equal(nbSessionsAfter, nbSessionsBefore, "No new session should have been created during the API call");
170+
});
171+
172+
// This checks we create an altSessionID for Anonymous users. Useful in particular to give them
173+
// rights through Access Rules.
174+
it("does generate a session when calling the API as anonymous", async function() {
175+
const nbSessionsBefore = await countSessions(server);
176+
const resp = await axios.get(`${serverUrl}/api/orgs`, anon);
177+
178+
const nbSessionsAfter = await countSessions(server);
179+
assert.equal(resp.status, 200);
180+
assert.equal(nbSessionsAfter, nbSessionsBefore + 1, "A new session should have been created during the API call");
181+
});
182+
154183
it("websocket allows openDoc for viewer", async function() {
155184
const cli = await openClient(server, "chimpy@getgrist.com", "pr");
156185
cli.ignoreTrivialActions();

0 commit comments

Comments
 (0)