From e9db8ab8f0b46ab5ee094a59d0a056604251d8bc Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Mon, 20 Jan 2025 11:51:50 +0100 Subject: [PATCH] feat: max parallel sessions config (#9109) --- .../__snapshots__/create-config.test.ts.snap | 1 + src/lib/create-config.ts | 24 ++++++++---- src/lib/services/user-service.ts | 27 ++++++------- src/lib/types/experimental.ts | 1 - src/lib/types/option.ts | 1 + .../e2e/services/user-service.e2e.test.ts | 38 ++++++------------- .../deploy/configuring-unleash.mdx | 1 + 7 files changed, 43 insertions(+), 50 deletions(-) diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index 6248cd1cfb..96e349b91e 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -149,6 +149,7 @@ exports[`should create default config 1`] = ` "clearSiteDataOnLogout": true, "cookieName": "unleash-session", "db": true, + "maxParallelSessions": 5, "ttlHours": 48, }, "strategySegmentsLimit": 5, diff --git a/src/lib/create-config.ts b/src/lib/create-config.ts index 25194c4721..ecd1d4eb69 100644 --- a/src/lib/create-config.ts +++ b/src/lib/create-config.ts @@ -266,7 +266,7 @@ const defaultDbOptions: WithOptional = applicationName: process.env.DATABASE_APPLICATION_NAME || 'unleash', }; -const defaultSessionOption: ISessionOption = { +const defaultSessionOption = (isEnterprise: boolean): ISessionOption => ({ ttlHours: parseEnvVarNumber(process.env.SESSION_TTL_HOURS, 48), clearSiteDataOnLogout: parseEnvVarBoolean( process.env.SESSION_CLEAR_SITE_DATA_ON_LOGOUT, @@ -274,7 +274,16 @@ const defaultSessionOption: ISessionOption = { ), cookieName: 'unleash-session', db: true, -}; + // default limit of 100 for enterprise, 5 for pro and oss + // at least 1 session should be allowed + maxParallelSessions: Math.max( + parseEnvVarNumber( + process.env.MAX_PARALLEL_SESSIONS, + isEnterprise ? 100 : 5, + ), + 1, + ), +}); const defaultServerOption: IServerOption = { pipe: undefined, @@ -533,11 +542,6 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig { options.db || {}, ]); - const session: ISessionOption = mergeAll([ - defaultSessionOption, - options.session || {}, - ]); - const logLevel = options.logLevel || LogLevel[process.env.LOG_LEVEL ?? LogLevel.error]; const getLogger = options.getLogger || getDefaultLogProvider(logLevel); @@ -638,6 +642,12 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig { ui.environment, isTest, ); + + const session: ISessionOption = mergeAll([ + defaultSessionOption(isEnterprise), + options.session || {}, + ]); + const metricsRateLimiting = loadMetricsRateLimitingConfig(options); const rateLimiting = loadRateLimitingConfig(options); diff --git a/src/lib/services/user-service.ts b/src/lib/services/user-service.ts index 1b2539f58d..e38f52d1b8 100644 --- a/src/lib/services/user-service.ts +++ b/src/lib/services/user-service.ts @@ -98,6 +98,8 @@ class UserService { readonly unleashUrl: string; + readonly maxParallelSessions: number; + constructor( stores: Pick, { @@ -106,6 +108,7 @@ class UserService { authentication, eventBus, flagResolver, + session, }: Pick< IUnleashConfig, | 'getLogger' @@ -113,6 +116,7 @@ class UserService { | 'server' | 'eventBus' | 'flagResolver' + | 'session' >, services: { accessService: AccessService; @@ -133,6 +137,7 @@ class UserService { this.sessionService = services.sessionService; this.settingService = services.settingService; this.flagResolver = flagResolver; + this.maxParallelSessions = session.maxParallelSessions; process.nextTick(() => this.initAdminUser(authentication)); @@ -431,22 +436,14 @@ class UserService { ); } - const deleteStaleUserSessions = this.flagResolver.getVariant( - 'deleteStaleUserSessions', - ); - if (deleteStaleUserSessions.feature_enabled) { - const allowedSessions = Number( - deleteStaleUserSessions.payload?.value || 5, + // subtract current user session that will be created + const deletedSessionsCount = + await this.sessionService.deleteStaleSessionsForUser( + user.id, + Math.max(this.maxParallelSessions - 1, 0), ); - // subtract current user session that will be created - const deletedSessionsCount = - await this.sessionService.deleteStaleSessionsForUser( - user.id, - Math.max(allowedSessions - 1, 0), - ); - user.deletedSessions = deletedSessionsCount; - user.activeSessions = allowedSessions; - } + user.deletedSessions = deletedSessionsCount; + user.activeSessions = this.maxParallelSessions; this.eventBus.emit(USER_LOGIN, { loginOrder }); return user; diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index ae4620749f..780666dd59 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -54,7 +54,6 @@ export type IFlagKey = | 'enterprise-payg' | 'flagOverviewRedesign' | 'showUserDeviceCount' - | 'deleteStaleUserSessions' | 'memorizeStats' | 'granularAdminPermissions' | 'streaming' diff --git a/src/lib/types/option.ts b/src/lib/types/option.ts index 98cd76f8cc..28471ca62f 100644 --- a/src/lib/types/option.ts +++ b/src/lib/types/option.ts @@ -45,6 +45,7 @@ export interface ISessionOption { db: boolean; clearSiteDataOnLogout: boolean; cookieName: string; + maxParallelSessions: number; } export interface IVersionOption { diff --git a/src/test/e2e/services/user-service.e2e.test.ts b/src/test/e2e/services/user-service.e2e.test.ts index ec10ef31d1..a3d935b8fb 100644 --- a/src/test/e2e/services/user-service.e2e.test.ts +++ b/src/test/e2e/services/user-service.e2e.test.ts @@ -18,7 +18,6 @@ import PasswordMismatch from '../../../lib/error/password-mismatch'; import type { EventService } from '../../../lib/services'; import { CREATE_ADDON, - type IFlagResolver, type IUnleashStores, type IUserStore, SYSTEM_USER_AUDIT, @@ -51,7 +50,9 @@ const allowedSessions = 2; beforeAll(async () => { db = await dbInit('user_service_serial', getLogger); stores = db.stores; - const config = createTestConfig(); + const config = createTestConfig({ + session: { maxParallelSessions: allowedSessions }, + }); eventBus = config.eventBus; eventService = createEventsService(db.rawDatabase, config); const groupService = new GroupService(stores, config, eventService); @@ -66,31 +67,14 @@ beforeAll(async () => { sessionService = new SessionService(stores, config); settingService = new SettingService(stores, config, eventService); - const flagResolver = { - isEnabled() { - return true; - }, - getVariant() { - return { - feature_enabled: true, - payload: { - value: String(allowedSessions), - }, - }; - }, - } as unknown as IFlagResolver; - userService = new UserService( - stores, - { ...config, flagResolver }, - { - accessService, - resetTokenService, - emailService, - eventService, - sessionService, - settingService, - }, - ); + userService = new UserService(stores, config, { + accessService, + resetTokenService, + emailService, + eventService, + sessionService, + settingService, + }); userStore = stores.userStore; const rootRoles = await accessService.getRootRoles(); adminRole = rootRoles.find((r) => r.name === RoleName.ADMIN)!; diff --git a/website/docs/using-unleash/deploy/configuring-unleash.mdx b/website/docs/using-unleash/deploy/configuring-unleash.mdx index aff97d0a62..c50a240089 100644 --- a/website/docs/using-unleash/deploy/configuring-unleash.mdx +++ b/website/docs/using-unleash/deploy/configuring-unleash.mdx @@ -201,6 +201,7 @@ unleash.start(unleashOptions); instructing the browser to clear all cookies on the same domain Unleash is running on. If disabled unleash will only destroy and clear the session cookie. Defaults to _true_. `SESSION_CLEAR_SITE_DATA_ON_LOGOUT` - _cookieName_ - Name of the cookies used to hold the session id. Defaults to 'unleash-session'. + - _maxParallelSessions_ - The maximum number of parallel user sessions with password based login. `MAX_PARALLEL_SESSIONS` - **ui** (object) - Set of UI specific overrides. You may set the following keys: `environment`, `slogan`. - **versionCheck** - the object deciding where to check for latest version - `url` - The url to check version (Defaults to `https://version.unleash.run`) - Overridable