diff --git a/crates/cli/src/commands/debug.rs b/crates/cli/src/commands/debug.rs index bb87c5e81..6da64f95b 100644 --- a/crates/cli/src/commands/debug.rs +++ b/crates/cli/src/commands/debug.rs @@ -9,7 +9,8 @@ use std::process::ExitCode; use clap::Parser; use figment::Figment; use mas_config::{ - ConfigurationSection, ConfigurationSectionExt, DatabaseConfig, MatrixConfig, PolicyConfig, + ConfigurationSection, ConfigurationSectionExt, DatabaseConfig, ExperimentalConfig, + MatrixConfig, PolicyConfig, }; use mas_storage_pg::PgRepositoryFactory; use tracing::{info, info_span}; @@ -45,8 +46,12 @@ impl Options { PolicyConfig::extract_or_default(figment).map_err(anyhow::Error::from_boxed)?; let matrix_config = MatrixConfig::extract(figment).map_err(anyhow::Error::from_boxed)?; + let experimental_config = + ExperimentalConfig::extract(figment).map_err(anyhow::Error::from_boxed)?; info!("Loading and compiling the policy module"); - let policy_factory = policy_factory_from_config(&config, &matrix_config).await?; + let policy_factory = + policy_factory_from_config(&config, &matrix_config, &experimental_config) + .await?; if with_dynamic_data { let database_config = diff --git a/crates/cli/src/commands/server.rs b/crates/cli/src/commands/server.rs index 12f9e8a14..b72d48111 100644 --- a/crates/cli/src/commands/server.rs +++ b/crates/cli/src/commands/server.rs @@ -127,7 +127,9 @@ impl Options { // Load and compile the WASM policies (and fallback to the default embedded one) info!("Loading and compiling the policy module"); - let policy_factory = policy_factory_from_config(&config.policy, &config.matrix).await?; + let policy_factory = + policy_factory_from_config(&config.policy, &config.matrix, &config.experimental) + .await?; let policy_factory = Arc::new(policy_factory); load_policy_factory_dynamic_data_continuously( diff --git a/crates/cli/src/util.rs b/crates/cli/src/util.rs index 8f5989305..caa76664d 100644 --- a/crates/cli/src/util.rs +++ b/crates/cli/src/util.rs @@ -135,6 +135,7 @@ pub fn test_mailer_in_background(mailer: &Mailer, timeout: Duration) { pub async fn policy_factory_from_config( config: &PolicyConfig, matrix_config: &MatrixConfig, + experimental_config: &ExperimentalConfig, ) -> Result { let policy_file = tokio::fs::File::open(&config.wasm_module) .await @@ -148,8 +149,18 @@ pub async fn policy_factory_from_config( email: config.email_entrypoint.clone(), }; - let data = - mas_policy::Data::new(matrix_config.homeserver.clone()).with_rest(config.data.clone()); + let session_limit_config = + experimental_config + .session_limit + .as_ref() + .map(|c| SessionLimitConfig { + soft_limit: c.soft_limit, + hard_limit: c.hard_limit, + hard_limit_eviction: c.hard_limit_eviction, + }); + + let data = mas_policy::Data::new(matrix_config.homeserver.clone(), session_limit_config) + .with_rest(config.data.clone()); PolicyFactory::load(policy_file, data, entrypoints) .await diff --git a/crates/handlers/src/compat/login.rs b/crates/handlers/src/compat/login.rs index 5ce833732..86ffd7641 100644 --- a/crates/handlers/src/compat/login.rs +++ b/crates/handlers/src/compat/login.rs @@ -685,7 +685,6 @@ async fn token_login( let res = policy .evaluate_compat_login(mas_policy::CompatLoginInput { user: &browser_session.user, - session_limit: session_limit_config, login: CompatLogin::Token, session_replaced, session_counts, @@ -813,7 +812,6 @@ async fn user_password_login( let res = policy .evaluate_compat_login(mas_policy::CompatLoginInput { user: &user, - session_limit: session_limit_config, login: CompatLogin::Password, session_replaced, session_counts, diff --git a/crates/handlers/src/compat/login_sso_complete.rs b/crates/handlers/src/compat/login_sso_complete.rs index 1a1dbcdc5..df059cd36 100644 --- a/crates/handlers/src/compat/login_sso_complete.rs +++ b/crates/handlers/src/compat/login_sso_complete.rs @@ -19,7 +19,7 @@ use mas_axum_utils::{ cookies::CookieJar, csrf::{CsrfExt, ProtectedForm}, }; -use mas_data_model::{BoxClock, BoxRng, Clock, MatrixUser, SiteConfig}; +use mas_data_model::{BoxClock, BoxRng, Clock, MatrixUser}; use mas_matrix::HomeserverConnection; use mas_policy::{Policy, model::CompatLogin}; use mas_router::{CompatLoginSsoAction, UrlBuilder}; @@ -53,7 +53,6 @@ pub async fn get( State(templates): State, State(url_builder): State, State(homeserver): State>, - State(site_config): State, mut policy: Policy, activity_tracker: BoundActivityTracker, user_agent: Option>, @@ -115,12 +114,9 @@ pub async fn get( // We can close the repository early, we don't need it at this point repo.save().await?; - let session_limit_config = site_config.session_limit.as_ref(); - let res = policy .evaluate_compat_login(mas_policy::CompatLoginInput { user: &session.user, - session_limit: session_limit_config, login: CompatLogin::Sso { redirect_uri: login.redirect_uri.to_string(), }, @@ -197,7 +193,6 @@ pub async fn post( PreferredLanguage(locale): PreferredLanguage, State(templates): State, State(url_builder): State, - State(site_config): State, mut policy: Policy, activity_tracker: BoundActivityTracker, user_agent: Option>, @@ -267,12 +262,9 @@ pub async fn post( let session_counts = count_user_sessions_for_limiting(&mut repo, &session.user).await?; - let session_limit_config = site_config.session_limit.as_ref(); - let res = policy .evaluate_compat_login(mas_policy::CompatLoginInput { user: &session.user, - session_limit: session_limit_config, login: CompatLogin::Sso { redirect_uri: login.redirect_uri.to_string(), }, diff --git a/crates/handlers/src/oauth2/authorization/consent.rs b/crates/handlers/src/oauth2/authorization/consent.rs index 31fdb8b3e..ab51bef1c 100644 --- a/crates/handlers/src/oauth2/authorization/consent.rs +++ b/crates/handlers/src/oauth2/authorization/consent.rs @@ -17,7 +17,7 @@ use mas_axum_utils::{ cookies::CookieJar, csrf::{CsrfExt, ProtectedForm}, }; -use mas_data_model::{AuthorizationGrantStage, BoxClock, BoxRng, MatrixUser, SiteConfig}; +use mas_data_model::{AuthorizationGrantStage, BoxClock, BoxRng, MatrixUser}; use mas_keystore::Keystore; use mas_matrix::HomeserverConnection; use mas_policy::Policy; @@ -91,7 +91,6 @@ pub(crate) async fn get( State(templates): State, State(url_builder): State, State(homeserver): State>, - State(site_config): State, mut policy: Policy, mut repo: BoxRepository, activity_tracker: BoundActivityTracker, @@ -146,12 +145,9 @@ pub(crate) async fn get( // We can close the repository early, we don't need it at this point repo.save().await?; - let session_limit_config = site_config.session_limit.as_ref(); - let res = policy .evaluate_authorization_grant(mas_policy::AuthorizationGrantInput { user: Some(&session.user), - session_limit: session_limit_config, client: &client, session_counts: Some(session_counts), scope: &grant.scope, @@ -224,7 +220,6 @@ pub(crate) async fn post( PreferredLanguage(locale): PreferredLanguage, State(templates): State, State(key_store): State, - State(site_config): State, mut policy: Policy, mut repo: BoxRepository, activity_tracker: BoundActivityTracker, @@ -280,13 +275,11 @@ pub(crate) async fn post( return Err(RouteError::GrantNotPending(grant.id)); } - let session_limit_config = site_config.session_limit.as_ref(); let session_counts = count_user_sessions_for_limiting(&mut repo, &browser_session.user).await?; let res = policy .evaluate_authorization_grant(mas_policy::AuthorizationGrantInput { user: Some(&browser_session.user), - session_limit: session_limit_config, client: &client, session_counts: Some(session_counts), scope: &grant.scope, diff --git a/crates/handlers/src/oauth2/device/consent.rs b/crates/handlers/src/oauth2/device/consent.rs index b39a0e159..3912d2dc1 100644 --- a/crates/handlers/src/oauth2/device/consent.rs +++ b/crates/handlers/src/oauth2/device/consent.rs @@ -18,7 +18,7 @@ use mas_axum_utils::{ cookies::CookieJar, csrf::{CsrfExt, ProtectedForm}, }; -use mas_data_model::{BoxClock, BoxRng, MatrixUser, SiteConfig}; +use mas_data_model::{BoxClock, BoxRng, MatrixUser}; use mas_matrix::HomeserverConnection; use mas_policy::Policy; use mas_router::UrlBuilder; @@ -53,7 +53,6 @@ pub(crate) async fn get( State(templates): State, State(url_builder): State, State(homeserver): State>, - State(site_config): State, mut repo: BoxRepository, mut policy: Policy, activity_tracker: BoundActivityTracker, @@ -108,7 +107,6 @@ pub(crate) async fn get( .context("Client not found") .map_err(InternalError::from_anyhow)?; - let session_limit_config = site_config.session_limit.as_ref(); let session_counts = count_user_sessions_for_limiting(&mut repo, &session.user).await?; // We can close the repository early, we don't need it at this point @@ -119,7 +117,6 @@ pub(crate) async fn get( .evaluate_authorization_grant(mas_policy::AuthorizationGrantInput { grant_type: mas_policy::GrantType::DeviceCode, client: &client, - session_limit: session_limit_config, session_counts: Some(session_counts), scope: &grant.scope, user: Some(&session.user), @@ -194,7 +191,6 @@ pub(crate) async fn post( State(templates): State, State(url_builder): State, State(homeserver): State>, - State(site_config): State, mut repo: BoxRepository, mut policy: Policy, activity_tracker: BoundActivityTracker, @@ -250,7 +246,6 @@ pub(crate) async fn post( .context("Client not found") .map_err(InternalError::from_anyhow)?; - let session_limit_config = site_config.session_limit.as_ref(); let session_counts = count_user_sessions_for_limiting(&mut repo, &session.user).await?; // Evaluate the policy @@ -258,7 +253,6 @@ pub(crate) async fn post( .evaluate_authorization_grant(mas_policy::AuthorizationGrantInput { grant_type: mas_policy::GrantType::DeviceCode, client: &client, - session_limit: session_limit_config, session_counts: Some(session_counts), scope: &grant.scope, user: Some(&session.user), diff --git a/crates/handlers/src/oauth2/token.rs b/crates/handlers/src/oauth2/token.rs index d19ca9c5e..696c7d426 100644 --- a/crates/handlers/src/oauth2/token.rs +++ b/crates/handlers/src/oauth2/token.rs @@ -792,13 +792,10 @@ async fn client_credentials_grant( .clone() .unwrap_or_else(|| std::iter::empty::().collect()); - let session_limit_config = site_config.session_limit.as_ref(); - // Make the request go through the policy engine let res = policy .evaluate_authorization_grant(mas_policy::AuthorizationGrantInput { user: None, - session_limit: session_limit_config, client, session_counts: None, scope: &scope, diff --git a/crates/handlers/src/test_utils.rs b/crates/handlers/src/test_utils.rs index a2bbf7d4a..521a4848d 100644 --- a/crates/handlers/src/test_utils.rs +++ b/crates/handlers/src/test_utils.rs @@ -86,7 +86,7 @@ pub(crate) async fn policy_factory( email: "email/violation".to_owned(), }; - let data = mas_policy::Data::new(server_name.to_owned()).with_rest(data); + let data = mas_policy::Data::new(server_name.to_owned(), None).with_rest(data); let policy_factory = PolicyFactory::load(file, data, entrypoints).await?; let policy_factory = Arc::new(policy_factory); diff --git a/crates/policy/src/lib.rs b/crates/policy/src/lib.rs index af715909b..a5d4805ad 100644 --- a/crates/policy/src/lib.rs +++ b/crates/policy/src/lib.rs @@ -9,7 +9,7 @@ pub mod model; use std::sync::Arc; use arc_swap::ArcSwap; -use mas_data_model::Ulid; +use mas_data_model::{SessionLimitConfig, Ulid}; use opa_wasm::{ Runtime, wasmtime::{Config, Engine, Module, OptLevel, Store}, @@ -100,13 +100,19 @@ pub struct Data { #[derive(Serialize, Debug)] struct BaseData { server_name: String, + + /// Limits on the number of application sessions that each user can have + session_limit: Option, } impl Data { #[must_use] - pub fn new(server_name: String) -> Self { + pub fn new(server_name: String, session_limit: Option) -> Self { Self { - base: BaseData { server_name }, + base: BaseData { + server_name, + session_limit, + }, rest: None, } @@ -501,7 +507,7 @@ mod tests { #[tokio::test] async fn test_register() { - let data = Data::new("example.com".to_owned()).with_rest(serde_json::json!({ + let data = Data::new("example.com".to_owned(), None).with_rest(serde_json::json!({ "allowed_domains": ["element.io", "*.element.io"], "banned_domains": ["staging.element.io"], })); @@ -566,7 +572,7 @@ mod tests { #[tokio::test] async fn test_dynamic_data() { - let data = Data::new("example.com".to_owned()); + let data = Data::new("example.com".to_owned(), None); #[allow(clippy::disallowed_types)] let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) @@ -630,7 +636,7 @@ mod tests { #[tokio::test] async fn test_big_dynamic_data() { - let data = Data::new("example.com".to_owned()); + let data = Data::new("example.com".to_owned(), None); #[allow(clippy::disallowed_types)] let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) diff --git a/crates/policy/src/model.rs b/crates/policy/src/model.rs index b8be99bae..a3bf24b5f 100644 --- a/crates/policy/src/model.rs +++ b/crates/policy/src/model.rs @@ -11,7 +11,7 @@ use std::net::IpAddr; -use mas_data_model::{Client, SessionLimitConfig, User}; +use mas_data_model::{Client, User}; use oauth2_types::{registration::VerifiedClientMetadata, scope::Scope}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -179,10 +179,6 @@ pub struct AuthorizationGrantInput<'a> { #[schemars(with = "Option>")] pub user: Option<&'a User>, - /// Limits on the number of application sessions that each user can have - #[schemars(with = "std::collections::HashMap")] - pub session_limit: Option<&'a SessionLimitConfig>, - /// How many sessions the user has. /// Not populated if it's not a user logging in. pub session_counts: Option, @@ -205,10 +201,6 @@ pub struct CompatLoginInput<'a> { #[schemars(with = "std::collections::HashMap")] pub user: &'a User, - /// Limits on the number of application sessions that each user can have - #[schemars(with = "std::collections::HashMap")] - pub session_limit: Option<&'a SessionLimitConfig>, - /// How many sessions the user has. pub session_counts: SessionCounts, diff --git a/policies/authorization_grant/authorization_grant.rego b/policies/authorization_grant/authorization_grant.rego index d7631a538..e7d1e68e5 100644 --- a/policies/authorization_grant/authorization_grant.rego +++ b/policies/authorization_grant/authorization_grant.rego @@ -159,7 +159,7 @@ violation contains { "msg": "user has too many active sessions", } if { # Only apply if session limits are enabled in the config - input.session_limit != null + data.session_limit != null # Only apply if it's a user logging in (who therefore has countable sessions) input.session_counts != null @@ -168,5 +168,5 @@ violation contains { # reached or exceeded. # We use the soft limit because the user will be able to interactively remove # sessions to return under the limit. - input.session_limit.soft_limit <= input.session_counts.total + data.session_limit.soft_limit <= input.session_counts.total } diff --git a/policies/authorization_grant/authorization_grant_test.rego b/policies/authorization_grant/authorization_grant_test.rego index a0752367e..e2ca74086 100644 --- a/policies/authorization_grant/authorization_grant_test.rego +++ b/policies/authorization_grant/authorization_grant_test.rego @@ -226,31 +226,31 @@ test_mas_scopes if { test_session_limiting if { authorization_grant.allow with input.user as user with input.session_counts as {"total": 1} - with input.session_limit as {"soft_limit": 32, "hard_limit": 64} + with data.session_limit as {"soft_limit": 32, "hard_limit": 64} authorization_grant.allow with input.user as user with input.session_counts as {"total": 31} - with input.session_limit as {"soft_limit": 32, "hard_limit": 64} + with data.session_limit as {"soft_limit": 32, "hard_limit": 64} not authorization_grant.allow with input.user as user with input.session_counts as {"total": 32} - with input.session_limit as {"soft_limit": 32, "hard_limit": 64} + with data.session_limit as {"soft_limit": 32, "hard_limit": 64} not authorization_grant.allow with input.user as user with input.session_counts as {"total": 42} - with input.session_limit as {"soft_limit": 32, "hard_limit": 64} + with data.session_limit as {"soft_limit": 32, "hard_limit": 64} not authorization_grant.allow with input.user as user with input.session_counts as {"total": 65} - with input.session_limit as {"soft_limit": 32, "hard_limit": 64} + with data.session_limit as {"soft_limit": 32, "hard_limit": 64} # No limit configured authorization_grant.allow with input.user as user with input.session_counts as {"total": 1} - with input.session_limit as null + with data.session_limit as null # Client credentials grant authorization_grant.allow with input.user as user with input.session_counts as null - with input.session_limit as {"soft_limit": 32, "hard_limit": 64} + with data.session_limit as {"soft_limit": 32, "hard_limit": 64} } diff --git a/policies/compat_login/compat_login.rego b/policies/compat_login/compat_login.rego index 05b0f4ebf..4f76842cd 100644 --- a/policies/compat_login/compat_login.rego +++ b/policies/compat_login/compat_login.rego @@ -30,7 +30,7 @@ violation contains { "msg": "user has too many active sessions (soft limit)", } if { # Only apply if session limits are enabled in the config - input.session_limit != null + data.session_limit != null # This is a web-based interactive login is_interactive @@ -43,7 +43,7 @@ violation contains { # reached or exceeded. # We use the soft limit because the user will be able to interactively remove # sessions to return under the limit. - input.session_limit.soft_limit <= input.session_counts.total + data.session_limit.soft_limit <= input.session_counts.total } violation contains { @@ -51,7 +51,7 @@ violation contains { "msg": "user has too many active sessions (hard limit)", } if { # Only apply if session limits are enabled in the config - input.session_limit != null + data.session_limit != null # This is not a web-based interactive login not is_interactive @@ -64,7 +64,7 @@ violation contains { # reached or exceeded. # We don't use the soft limit because the user won't be able to interactively remove # sessions to return under the limit. - input.session_limit.hard_limit <= input.session_counts.total + data.session_limit.hard_limit <= input.session_counts.total } is_interactive if { diff --git a/policies/compat_login/compat_login_test.rego b/policies/compat_login/compat_login_test.rego index 1b5874758..1b8049844 100644 --- a/policies/compat_login/compat_login_test.rego +++ b/policies/compat_login/compat_login_test.rego @@ -16,38 +16,38 @@ test_session_limiting_sso if { with input.session_counts as {"total": 1} with input.login as {"type": "m.login.sso"} with input.session_replaced as false - with input.session_limit as {"soft_limit": 32, "hard_limit": 64} + with data.session_limit as {"soft_limit": 32, "hard_limit": 64} compat_login.allow with input.user as user with input.session_counts as {"total": 31} with input.login as {"type": "m.login.sso"} with input.session_replaced as false - with input.session_limit as {"soft_limit": 32, "hard_limit": 64} + with data.session_limit as {"soft_limit": 32, "hard_limit": 64} not compat_login.allow with input.user as user with input.session_counts as {"total": 32} with input.login as {"type": "m.login.sso"} with input.session_replaced as false - with input.session_limit as {"soft_limit": 32, "hard_limit": 64} + with data.session_limit as {"soft_limit": 32, "hard_limit": 64} not compat_login.allow with input.user as user with input.session_counts as {"total": 42} with input.login as {"type": "m.login.sso"} with input.session_replaced as false - with input.session_limit as {"soft_limit": 32, "hard_limit": 64} + with data.session_limit as {"soft_limit": 32, "hard_limit": 64} not compat_login.allow with input.user as user with input.session_counts as {"total": 65} with input.login as {"type": "m.login.sso"} with input.session_replaced as false - with input.session_limit as {"soft_limit": 32, "hard_limit": 64} + with data.session_limit as {"soft_limit": 32, "hard_limit": 64} # No limit configured compat_login.allow with input.user as user with input.session_counts as {"total": 1} with input.login as {"type": "m.login.sso"} with input.session_replaced as false - with input.session_limit as null + with data.session_limit as null } # Test session limiting when using `m.login.password` @@ -56,32 +56,32 @@ test_session_limiting_password if { with input.session_counts as {"total": 1} with input.login as {"type": "m.login.password"} with input.session_replaced as false - with input.session_limit as {"soft_limit": 32, "hard_limit": 64} + with data.session_limit as {"soft_limit": 32, "hard_limit": 64} compat_login.allow with input.user as user with input.session_counts as {"total": 63} with input.login as {"type": "m.login.password"} with input.session_replaced as false - with input.session_limit as {"soft_limit": 32, "hard_limit": 64} + with data.session_limit as {"soft_limit": 32, "hard_limit": 64} not compat_login.allow with input.user as user with input.session_counts as {"total": 64} with input.login as {"type": "m.login.password"} with input.session_replaced as false - with input.session_limit as {"soft_limit": 32, "hard_limit": 64} + with data.session_limit as {"soft_limit": 32, "hard_limit": 64} not compat_login.allow with input.user as user with input.session_counts as {"total": 65} with input.login as {"type": "m.login.password"} with input.session_replaced as false - with input.session_limit as {"soft_limit": 32, "hard_limit": 64} + with data.session_limit as {"soft_limit": 32, "hard_limit": 64} # No limit configured compat_login.allow with input.user as user with input.session_counts as {"total": 1} with input.login as {"type": "m.login.password"} with input.session_replaced as false - with input.session_limit as null + with data.session_limit as null } test_no_session_limiting_upon_replacement if { @@ -89,11 +89,11 @@ test_no_session_limiting_upon_replacement if { with input.session_counts as {"total": 65} with input.login as {"type": "m.login.password"} with input.session_replaced as false - with input.session_limit as {"soft_limit": 32, "hard_limit": 64} + with data.session_limit as {"soft_limit": 32, "hard_limit": 64} not compat_login.allow with input.user as user with input.session_counts as {"total": 65} with input.login as {"type": "m.login.sso"} with input.session_replaced as false - with input.session_limit as {"soft_limit": 32, "hard_limit": 64} + with data.session_limit as {"soft_limit": 32, "hard_limit": 64} }