From dc535d74514e785e721ec44221a7c669db15200b Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Thu, 30 Oct 2025 16:30:33 +0000 Subject: [PATCH] Add configuration for session limiting --- crates/cli/src/util.rs | 9 ++++++- crates/config/src/sections/experimental.rs | 15 ++++++++++++ crates/data-model/src/lib.rs | 4 +++- crates/data-model/src/site_config.rs | 10 ++++++++ crates/handlers/src/test_utils.rs | 1 + docs/config.schema.json | 28 ++++++++++++++++++++++ 6 files changed, 65 insertions(+), 2 deletions(-) diff --git a/crates/cli/src/util.rs b/crates/cli/src/util.rs index 4925d9866..eefbb5b0e 100644 --- a/crates/cli/src/util.rs +++ b/crates/cli/src/util.rs @@ -13,7 +13,7 @@ use mas_config::{ PolicyConfig, TemplatesConfig, }; use mas_context::LogContext; -use mas_data_model::{SessionExpirationConfig, SiteConfig}; +use mas_data_model::{SessionExpirationConfig, SessionLimitConfig, SiteConfig}; use mas_email::{MailTransport, Mailer}; use mas_handlers::passwords::PasswordManager; use mas_matrix::{HomeserverConnection, ReadOnlyHomeserverConnection}; @@ -225,6 +225,13 @@ pub fn site_config_from_config( session_expiration, login_with_email_allowed: account_config.login_with_email_allowed, plan_management_iframe_uri: experimental_config.plan_management_iframe_uri.clone(), + session_limit: experimental_config + .session_limit + .as_ref() + .map(|c| SessionLimitConfig { + soft_limit: c.soft_limit, + hard_limit: c.hard_limit, + }), }) } diff --git a/crates/config/src/sections/experimental.rs b/crates/config/src/sections/experimental.rs index c6c50e88d..7824aad33 100644 --- a/crates/config/src/sections/experimental.rs +++ b/crates/config/src/sections/experimental.rs @@ -81,6 +81,13 @@ pub struct ExperimentalConfig { /// validation. #[serde(skip_serializing_if = "Option::is_none")] pub plan_management_iframe_uri: Option, + + /// Experimental feature to limit the number of application sessions per + /// user. + /// + /// Disabled by default. + #[serde(skip_serializing_if = "Option::is_none")] + pub session_limit: Option, } impl Default for ExperimentalConfig { @@ -90,6 +97,7 @@ impl Default for ExperimentalConfig { compat_token_ttl: default_token_ttl(), inactive_session_expiration: None, plan_management_iframe_uri: None, + session_limit: None, } } } @@ -106,3 +114,10 @@ impl ExperimentalConfig { impl ConfigurationSection for ExperimentalConfig { const PATH: Option<&'static str> = Some("experimental"); } + +/// Configuration options for the inactive session expiration feature +#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)] +pub struct SessionLimitConfig { + pub soft_limit: u64, + pub hard_limit: u64, +} diff --git a/crates/data-model/src/lib.rs b/crates/data-model/src/lib.rs index 962c8be00..fd5c0e633 100644 --- a/crates/data-model/src/lib.rs +++ b/crates/data-model/src/lib.rs @@ -39,7 +39,9 @@ pub use self::{ DeviceCodeGrantState, InvalidRedirectUriError, JwksOrJwksUri, Pkce, Session, SessionState, }, policy_data::PolicyData, - site_config::{CaptchaConfig, CaptchaService, SessionExpirationConfig, SiteConfig}, + site_config::{ + CaptchaConfig, CaptchaService, SessionExpirationConfig, SessionLimitConfig, SiteConfig, + }, tokens::{ AccessToken, AccessTokenState, RefreshToken, RefreshTokenState, TokenFormatError, TokenType, }, diff --git a/crates/data-model/src/site_config.rs b/crates/data-model/src/site_config.rs index 9622203ad..96736b4f7 100644 --- a/crates/data-model/src/site_config.rs +++ b/crates/data-model/src/site_config.rs @@ -5,6 +5,7 @@ // Please see LICENSE files in the repository root for full details. use chrono::Duration; +use serde::Serialize; use url::Url; /// Which Captcha service is being used @@ -36,6 +37,12 @@ pub struct SessionExpirationConfig { pub compat_session_inactivity_ttl: Option, } +#[derive(Serialize, Debug, Clone)] +pub struct SessionLimitConfig { + pub soft_limit: u64, + pub hard_limit: u64, +} + /// Random site configuration we want accessible in various places. #[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] @@ -99,4 +106,7 @@ pub struct SiteConfig { /// The iframe URL to show in the plan tab of the UI pub plan_management_iframe_uri: Option, + + /// Limits on the number of application sessions that each user can have + pub session_limit: Option, } diff --git a/crates/handlers/src/test_utils.rs b/crates/handlers/src/test_utils.rs index f1859f352..df60c5c20 100644 --- a/crates/handlers/src/test_utils.rs +++ b/crates/handlers/src/test_utils.rs @@ -148,6 +148,7 @@ pub fn test_site_config() -> SiteConfig { session_expiration: None, login_with_email_allowed: true, plan_management_iframe_uri: None, + session_limit: None, } } diff --git a/docs/config.schema.json b/docs/config.schema.json index 524f02c93..41b40ad41 100644 --- a/docs/config.schema.json +++ b/docs/config.schema.json @@ -2659,6 +2659,14 @@ "plan_management_iframe_uri": { "description": "Experimental feature to show a plan management tab and iframe. This value is passed through \"as is\" to the client without any validation.", "type": "string" + }, + "session_limit": { + "description": "Experimental feature to limit the number of application sessions per user.\n\nDisabled by default.", + "allOf": [ + { + "$ref": "#/definitions/SessionLimitConfig" + } + ] } } }, @@ -2692,6 +2700,26 @@ "type": "boolean" } } + }, + "SessionLimitConfig": { + "description": "Configuration options for the inactive session expiration feature", + "type": "object", + "required": [ + "hard_limit", + "soft_limit" + ], + "properties": { + "soft_limit": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "hard_limit": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } } } } \ No newline at end of file