1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-03-09 00:18:26 +01:00

feat: max parallel sessions config (#9109)

This commit is contained in:
Mateusz Kwasniewski 2025-01-20 11:51:50 +01:00 committed by GitHub
parent bbbc85245c
commit e9db8ab8f0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 43 additions and 50 deletions

View File

@ -149,6 +149,7 @@ exports[`should create default config 1`] = `
"clearSiteDataOnLogout": true, "clearSiteDataOnLogout": true,
"cookieName": "unleash-session", "cookieName": "unleash-session",
"db": true, "db": true,
"maxParallelSessions": 5,
"ttlHours": 48, "ttlHours": 48,
}, },
"strategySegmentsLimit": 5, "strategySegmentsLimit": 5,

View File

@ -266,7 +266,7 @@ const defaultDbOptions: WithOptional<IDBOption, 'user' | 'password' | 'host'> =
applicationName: process.env.DATABASE_APPLICATION_NAME || 'unleash', applicationName: process.env.DATABASE_APPLICATION_NAME || 'unleash',
}; };
const defaultSessionOption: ISessionOption = { const defaultSessionOption = (isEnterprise: boolean): ISessionOption => ({
ttlHours: parseEnvVarNumber(process.env.SESSION_TTL_HOURS, 48), ttlHours: parseEnvVarNumber(process.env.SESSION_TTL_HOURS, 48),
clearSiteDataOnLogout: parseEnvVarBoolean( clearSiteDataOnLogout: parseEnvVarBoolean(
process.env.SESSION_CLEAR_SITE_DATA_ON_LOGOUT, process.env.SESSION_CLEAR_SITE_DATA_ON_LOGOUT,
@ -274,7 +274,16 @@ const defaultSessionOption: ISessionOption = {
), ),
cookieName: 'unleash-session', cookieName: 'unleash-session',
db: true, 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 = { const defaultServerOption: IServerOption = {
pipe: undefined, pipe: undefined,
@ -533,11 +542,6 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
options.db || {}, options.db || {},
]); ]);
const session: ISessionOption = mergeAll([
defaultSessionOption,
options.session || {},
]);
const logLevel = const logLevel =
options.logLevel || LogLevel[process.env.LOG_LEVEL ?? LogLevel.error]; options.logLevel || LogLevel[process.env.LOG_LEVEL ?? LogLevel.error];
const getLogger = options.getLogger || getDefaultLogProvider(logLevel); const getLogger = options.getLogger || getDefaultLogProvider(logLevel);
@ -638,6 +642,12 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
ui.environment, ui.environment,
isTest, isTest,
); );
const session: ISessionOption = mergeAll([
defaultSessionOption(isEnterprise),
options.session || {},
]);
const metricsRateLimiting = loadMetricsRateLimitingConfig(options); const metricsRateLimiting = loadMetricsRateLimitingConfig(options);
const rateLimiting = loadRateLimitingConfig(options); const rateLimiting = loadRateLimitingConfig(options);

View File

@ -98,6 +98,8 @@ class UserService {
readonly unleashUrl: string; readonly unleashUrl: string;
readonly maxParallelSessions: number;
constructor( constructor(
stores: Pick<IUnleashStores, 'userStore'>, stores: Pick<IUnleashStores, 'userStore'>,
{ {
@ -106,6 +108,7 @@ class UserService {
authentication, authentication,
eventBus, eventBus,
flagResolver, flagResolver,
session,
}: Pick< }: Pick<
IUnleashConfig, IUnleashConfig,
| 'getLogger' | 'getLogger'
@ -113,6 +116,7 @@ class UserService {
| 'server' | 'server'
| 'eventBus' | 'eventBus'
| 'flagResolver' | 'flagResolver'
| 'session'
>, >,
services: { services: {
accessService: AccessService; accessService: AccessService;
@ -133,6 +137,7 @@ class UserService {
this.sessionService = services.sessionService; this.sessionService = services.sessionService;
this.settingService = services.settingService; this.settingService = services.settingService;
this.flagResolver = flagResolver; this.flagResolver = flagResolver;
this.maxParallelSessions = session.maxParallelSessions;
process.nextTick(() => this.initAdminUser(authentication)); process.nextTick(() => this.initAdminUser(authentication));
@ -431,22 +436,14 @@ class UserService {
); );
} }
const deleteStaleUserSessions = this.flagResolver.getVariant( // subtract current user session that will be created
'deleteStaleUserSessions', const deletedSessionsCount =
); await this.sessionService.deleteStaleSessionsForUser(
if (deleteStaleUserSessions.feature_enabled) { user.id,
const allowedSessions = Number( Math.max(this.maxParallelSessions - 1, 0),
deleteStaleUserSessions.payload?.value || 5,
); );
// subtract current user session that will be created user.deletedSessions = deletedSessionsCount;
const deletedSessionsCount = user.activeSessions = this.maxParallelSessions;
await this.sessionService.deleteStaleSessionsForUser(
user.id,
Math.max(allowedSessions - 1, 0),
);
user.deletedSessions = deletedSessionsCount;
user.activeSessions = allowedSessions;
}
this.eventBus.emit(USER_LOGIN, { loginOrder }); this.eventBus.emit(USER_LOGIN, { loginOrder });
return user; return user;

View File

@ -54,7 +54,6 @@ export type IFlagKey =
| 'enterprise-payg' | 'enterprise-payg'
| 'flagOverviewRedesign' | 'flagOverviewRedesign'
| 'showUserDeviceCount' | 'showUserDeviceCount'
| 'deleteStaleUserSessions'
| 'memorizeStats' | 'memorizeStats'
| 'granularAdminPermissions' | 'granularAdminPermissions'
| 'streaming' | 'streaming'

View File

@ -45,6 +45,7 @@ export interface ISessionOption {
db: boolean; db: boolean;
clearSiteDataOnLogout: boolean; clearSiteDataOnLogout: boolean;
cookieName: string; cookieName: string;
maxParallelSessions: number;
} }
export interface IVersionOption { export interface IVersionOption {

View File

@ -18,7 +18,6 @@ import PasswordMismatch from '../../../lib/error/password-mismatch';
import type { EventService } from '../../../lib/services'; import type { EventService } from '../../../lib/services';
import { import {
CREATE_ADDON, CREATE_ADDON,
type IFlagResolver,
type IUnleashStores, type IUnleashStores,
type IUserStore, type IUserStore,
SYSTEM_USER_AUDIT, SYSTEM_USER_AUDIT,
@ -51,7 +50,9 @@ const allowedSessions = 2;
beforeAll(async () => { beforeAll(async () => {
db = await dbInit('user_service_serial', getLogger); db = await dbInit('user_service_serial', getLogger);
stores = db.stores; stores = db.stores;
const config = createTestConfig(); const config = createTestConfig({
session: { maxParallelSessions: allowedSessions },
});
eventBus = config.eventBus; eventBus = config.eventBus;
eventService = createEventsService(db.rawDatabase, config); eventService = createEventsService(db.rawDatabase, config);
const groupService = new GroupService(stores, config, eventService); const groupService = new GroupService(stores, config, eventService);
@ -66,31 +67,14 @@ beforeAll(async () => {
sessionService = new SessionService(stores, config); sessionService = new SessionService(stores, config);
settingService = new SettingService(stores, config, eventService); settingService = new SettingService(stores, config, eventService);
const flagResolver = { userService = new UserService(stores, config, {
isEnabled() { accessService,
return true; resetTokenService,
}, emailService,
getVariant() { eventService,
return { sessionService,
feature_enabled: true, settingService,
payload: { });
value: String(allowedSessions),
},
};
},
} as unknown as IFlagResolver;
userService = new UserService(
stores,
{ ...config, flagResolver },
{
accessService,
resetTokenService,
emailService,
eventService,
sessionService,
settingService,
},
);
userStore = stores.userStore; userStore = stores.userStore;
const rootRoles = await accessService.getRootRoles(); const rootRoles = await accessService.getRootRoles();
adminRole = rootRoles.find((r) => r.name === RoleName.ADMIN)!; adminRole = rootRoles.find((r) => r.name === RoleName.ADMIN)!;

View File

@ -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 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` 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'. - _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`. - **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 - **versionCheck** - the object deciding where to check for latest version
- `url` - The url to check version (Defaults to `https://version.unleash.run`) - Overridable - `url` - The url to check version (Defaults to `https://version.unleash.run`) - Overridable