diff --git a/crates/handlers/src/graphql/mod.rs b/crates/handlers/src/graphql/mod.rs index 11c461bcc..0fc5209c2 100644 --- a/crates/handlers/src/graphql/mod.rs +++ b/crates/handlers/src/graphql/mod.rs @@ -238,6 +238,7 @@ async fn get_requester( activity_tracker: &BoundActivityTracker, mut repo: BoxRepository, session_info: SessionInfo, + user_agent: Option, token: Option<&str>, ) -> Result { let entity = if let Some(token) = token { @@ -301,6 +302,7 @@ async fn get_requester( let requester = Requester { entity, ip_address: activity_tracker.ip(), + user_agent, }; repo.cancel().await?; @@ -318,12 +320,14 @@ pub async fn post( cookie_jar: CookieJar, content_type: Option>, authorization: Option>>, + user_agent: Option>, body: Body, ) -> Result { let body = body.into_data_stream(); let token = authorization .as_ref() .map(|TypedHeader(Authorization(bearer))| bearer.token()); + let user_agent = user_agent.map(|TypedHeader(h)| h.to_string()); let (session_info, _cookie_jar) = cookie_jar.session_info(); let requester = get_requester( undocumented_oauth2_access, @@ -331,6 +335,7 @@ pub async fn post( &activity_tracker, repo, session_info, + user_agent, token, ) .await?; @@ -370,11 +375,13 @@ pub async fn get( activity_tracker: BoundActivityTracker, cookie_jar: CookieJar, authorization: Option>>, + user_agent: Option>, RawQuery(query): RawQuery, ) -> Result { let token = authorization .as_ref() .map(|TypedHeader(Authorization(bearer))| bearer.token()); + let user_agent = user_agent.map(|TypedHeader(h)| h.to_string()); let (session_info, _cookie_jar) = cookie_jar.session_info(); let requester = get_requester( undocumented_oauth2_access, @@ -382,6 +389,7 @@ pub async fn get( &activity_tracker, repo, session_info, + user_agent, token, ) .await?; @@ -422,6 +430,7 @@ pub fn schema_builder() -> SchemaBuilder { pub struct Requester { entity: RequestingEntity, ip_address: Option, + user_agent: Option, } impl Requester { @@ -432,6 +441,13 @@ impl Requester { RequesterFingerprint::EMPTY } } + + pub fn for_policy(&self) -> mas_policy::Requester { + mas_policy::Requester { + ip_address: self.ip_address, + user_agent: self.user_agent.clone(), + } + } } impl Deref for Requester { diff --git a/crates/handlers/src/graphql/mutations/user_email.rs b/crates/handlers/src/graphql/mutations/user_email.rs index 371864b10..ba7aef776 100644 --- a/crates/handlers/src/graphql/mutations/user_email.rs +++ b/crates/handlers/src/graphql/mutations/user_email.rs @@ -427,7 +427,7 @@ impl UserEmailMutations { let res = policy .evaluate_email(mas_policy::EmailInput { email: &input.email, - requester: requester.fingerprint().into(), + requester: requester.for_policy(), }) .await?; if !res.valid() { @@ -618,7 +618,7 @@ impl UserEmailMutations { let res = policy .evaluate_email(mas_policy::EmailInput { email: &input.email, - requester: requester.fingerprint().into(), + requester: requester.for_policy(), }) .await?; if !res.valid() { diff --git a/crates/handlers/src/oauth2/authorization/complete.rs b/crates/handlers/src/oauth2/authorization/complete.rs index 205809c19..8c3faf5b7 100644 --- a/crates/handlers/src/oauth2/authorization/complete.rs +++ b/crates/handlers/src/oauth2/authorization/complete.rs @@ -8,6 +8,7 @@ use axum::{ extract::{Path, State}, response::{Html, IntoResponse, Response}, }; +use axum_extra::TypedHeader; use hyper::StatusCode; use mas_axum_utils::{cookies::CookieJar, csrf::CsrfExt, sentry::SentryEventID, SessionInfoExt}; use mas_data_model::{AuthorizationGrant, BrowserSession, Client, Device}; @@ -89,6 +90,7 @@ pub(crate) async fn get( State(key_store): State, policy: Policy, activity_tracker: BoundActivityTracker, + user_agent: Option>, mut repo: BoxRepository, cookie_jar: CookieJar, Path(grant_id): Path, @@ -97,6 +99,8 @@ pub(crate) async fn get( let maybe_session = session_info.load_session(&mut repo).await?; + let user_agent = user_agent.map(|TypedHeader(ua)| ua.to_string()); + let grant = repo .oauth2_authorization_grant() .lookup(grant_id) @@ -130,6 +134,7 @@ pub(crate) async fn get( &mut rng, &clock, &activity_tracker, + user_agent, repo, key_store, policy, @@ -199,6 +204,7 @@ pub(crate) async fn complete( rng: &mut (impl rand::RngCore + rand::CryptoRng + Send), clock: &impl Clock, activity_tracker: &BoundActivityTracker, + user_agent: Option, mut repo: BoxRepository, key_store: Keystore, mut policy: Policy, @@ -233,6 +239,7 @@ pub(crate) async fn complete( grant_type: mas_policy::GrantType::AuthorizationCode, requester: mas_policy::Requester { ip_address: activity_tracker.ip(), + user_agent, }, }) .await?; diff --git a/crates/handlers/src/oauth2/authorization/mod.rs b/crates/handlers/src/oauth2/authorization/mod.rs index f56f06133..27b121e95 100644 --- a/crates/handlers/src/oauth2/authorization/mod.rs +++ b/crates/handlers/src/oauth2/authorization/mod.rs @@ -8,6 +8,7 @@ use axum::{ extract::{Form, State}, response::{Html, IntoResponse, Response}, }; +use axum_extra::TypedHeader; use hyper::StatusCode; use mas_axum_utils::{cookies::CookieJar, csrf::CsrfExt, sentry::SentryEventID, SessionInfoExt}; use mas_data_model::{AuthorizationCode, Pkce}; @@ -136,6 +137,7 @@ pub(crate) async fn get( State(key_store): State, State(url_builder): State, policy: Policy, + user_agent: Option>, activity_tracker: BoundActivityTracker, mut repo: BoxRepository, cookie_jar: CookieJar, @@ -166,6 +168,8 @@ pub(crate) async fn get( let (session_info, cookie_jar) = cookie_jar.session_info(); let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng); + let user_agent = user_agent.map(|TypedHeader(ua)| ua.to_string()); + // One day, we will have try blocks let res: Result = ({ let templates = templates.clone(); @@ -349,6 +353,7 @@ pub(crate) async fn get( &mut rng, &clock, &activity_tracker, + user_agent, repo, key_store, policy, @@ -401,6 +406,7 @@ pub(crate) async fn get( &mut rng, &clock, &activity_tracker, + user_agent, repo, key_store, policy, diff --git a/crates/handlers/src/oauth2/consent.rs b/crates/handlers/src/oauth2/consent.rs index b549eaff5..f7b4b72fc 100644 --- a/crates/handlers/src/oauth2/consent.rs +++ b/crates/handlers/src/oauth2/consent.rs @@ -8,6 +8,7 @@ use axum::{ extract::{Form, Path, State}, response::{Html, IntoResponse, Response}, }; +use axum_extra::TypedHeader; use hyper::StatusCode; use mas_axum_utils::{ cookies::CookieJar, @@ -80,6 +81,7 @@ pub(crate) async fn get( mut policy: Policy, mut repo: BoxRepository, activity_tracker: BoundActivityTracker, + user_agent: Option>, cookie_jar: CookieJar, Path(grant_id): Path, ) -> Result { @@ -87,6 +89,8 @@ pub(crate) async fn get( let maybe_session = session_info.load_session(&mut repo).await?; + let user_agent = user_agent.map(|ua| ua.to_string()); + let grant = repo .oauth2_authorization_grant() .lookup(grant_id) @@ -118,6 +122,7 @@ pub(crate) async fn get( grant_type: mas_policy::GrantType::AuthorizationCode, requester: mas_policy::Requester { ip_address: activity_tracker.ip(), + user_agent, }, }) .await?; @@ -159,6 +164,7 @@ pub(crate) async fn post( mut policy: Policy, mut repo: BoxRepository, activity_tracker: BoundActivityTracker, + user_agent: Option>, cookie_jar: CookieJar, State(url_builder): State, Path(grant_id): Path, @@ -170,6 +176,8 @@ pub(crate) async fn post( let maybe_session = session_info.load_session(&mut repo).await?; + let user_agent = user_agent.map(|ua| ua.to_string()); + let grant = repo .oauth2_authorization_grant() .lookup(grant_id) @@ -200,6 +208,7 @@ pub(crate) async fn post( grant_type: mas_policy::GrantType::AuthorizationCode, requester: mas_policy::Requester { ip_address: activity_tracker.ip(), + user_agent, }, }) .await?; diff --git a/crates/handlers/src/oauth2/device/consent.rs b/crates/handlers/src/oauth2/device/consent.rs index 7674e7a4e..ac8bdd63b 100644 --- a/crates/handlers/src/oauth2/device/consent.rs +++ b/crates/handlers/src/oauth2/device/consent.rs @@ -10,6 +10,7 @@ use axum::{ response::{Html, IntoResponse, Response}, Form, }; +use axum_extra::TypedHeader; use mas_axum_utils::{ cookies::CookieJar, csrf::{CsrfExt, ProtectedForm}, @@ -46,6 +47,7 @@ pub(crate) async fn get( mut repo: BoxRepository, mut policy: Policy, activity_tracker: BoundActivityTracker, + user_agent: Option>, cookie_jar: CookieJar, Path(grant_id): Path, ) -> Result { @@ -54,6 +56,8 @@ pub(crate) async fn get( let maybe_session = session_info.load_session(&mut repo).await?; + let user_agent = user_agent.map(|ua| ua.to_string()); + let Some(session) = maybe_session else { let login = mas_router::Login::and_continue_device_code_grant(grant_id); return Ok((cookie_jar, url_builder.redirect(&login)).into_response()); @@ -89,6 +93,7 @@ pub(crate) async fn get( user: Some(&session.user), requester: mas_policy::Requester { ip_address: activity_tracker.ip(), + user_agent, }, }) .await?; @@ -127,6 +132,7 @@ pub(crate) async fn post( mut repo: BoxRepository, mut policy: Policy, activity_tracker: BoundActivityTracker, + user_agent: Option>, cookie_jar: CookieJar, Path(grant_id): Path, Form(form): Form>, @@ -137,6 +143,8 @@ pub(crate) async fn post( let maybe_session = session_info.load_session(&mut repo).await?; + let user_agent = user_agent.map(|TypedHeader(ua)| ua.to_string()); + let Some(session) = maybe_session else { let login = mas_router::Login::and_continue_device_code_grant(grant_id); return Ok((cookie_jar, url_builder.redirect(&login)).into_response()); @@ -172,6 +180,7 @@ pub(crate) async fn post( user: Some(&session.user), requester: mas_policy::Requester { ip_address: activity_tracker.ip(), + user_agent, }, }) .await?; diff --git a/crates/handlers/src/oauth2/registration.rs b/crates/handlers/src/oauth2/registration.rs index a32c77caf..fca11cf97 100644 --- a/crates/handlers/src/oauth2/registration.rs +++ b/crates/handlers/src/oauth2/registration.rs @@ -5,6 +5,7 @@ // Please see LICENSE in the repository root for full details. use axum::{extract::State, response::IntoResponse, Json}; +use axum_extra::TypedHeader; use hyper::StatusCode; use mas_axum_utils::sentry::SentryEventID; use mas_iana::oauth::OAuthClientAuthenticationMethod; @@ -196,6 +197,7 @@ pub(crate) async fn post( mut repo: BoxRepository, mut policy: Policy, activity_tracker: BoundActivityTracker, + user_agent: Option>, State(encrypter): State, body: Result, axum::extract::rejection::JsonRejection>, ) -> Result { @@ -204,6 +206,8 @@ pub(crate) async fn post( info!(?body, "Client registration"); + let user_agent = user_agent.map(|ua| ua.to_string()); + // Validate the body let metadata = body.validate()?; @@ -250,6 +254,7 @@ pub(crate) async fn post( client_metadata: &metadata, requester: mas_policy::Requester { ip_address: activity_tracker.ip(), + user_agent, }, }) .await?; diff --git a/crates/handlers/src/oauth2/token.rs b/crates/handlers/src/oauth2/token.rs index e89eb34d7..2c68cb782 100644 --- a/crates/handlers/src/oauth2/token.rs +++ b/crates/handlers/src/oauth2/token.rs @@ -683,6 +683,7 @@ async fn client_credentials_grant( grant_type: mas_policy::GrantType::ClientCredentials, requester: mas_policy::Requester { ip_address: activity_tracker.ip(), + user_agent: user_agent.clone().map(|ua| ua.raw), }, }) .await?; diff --git a/crates/handlers/src/rate_limit.rs b/crates/handlers/src/rate_limit.rs index 673cee0a6..eff30d86f 100644 --- a/crates/handlers/src/rate_limit.rs +++ b/crates/handlers/src/rate_limit.rs @@ -53,12 +53,6 @@ pub struct RequesterFingerprint { ip: Option, } -impl From for mas_policy::Requester { - fn from(val: RequesterFingerprint) -> Self { - mas_policy::Requester { ip_address: val.ip } - } -} - impl std::fmt::Display for RequesterFingerprint { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if let Some(ip) = self.ip { diff --git a/crates/handlers/src/upstream_oauth2/link.rs b/crates/handlers/src/upstream_oauth2/link.rs index cccb3d961..1ba8cfcc8 100644 --- a/crates/handlers/src/upstream_oauth2/link.rs +++ b/crates/handlers/src/upstream_oauth2/link.rs @@ -449,6 +449,7 @@ pub(crate) async fn get( email: None, requester: mas_policy::Requester { ip_address: activity_tracker.ip(), + user_agent: user_agent.clone().map(|ua| ua.raw), }, }) .await?; @@ -768,6 +769,7 @@ pub(crate) async fn post( email: email.as_deref(), requester: mas_policy::Requester { ip_address: activity_tracker.ip(), + user_agent: user_agent.clone().map(|ua| ua.raw), }, }) .await?; diff --git a/crates/handlers/src/views/register/password.rs b/crates/handlers/src/views/register/password.rs index 8ce2677ed..184b98c42 100644 --- a/crates/handlers/src/views/register/password.rs +++ b/crates/handlers/src/views/register/password.rs @@ -239,6 +239,7 @@ pub(crate) async fn post( email: Some(&form.email), requester: mas_policy::Requester { ip_address: activity_tracker.ip(), + user_agent: user_agent.clone().map(|ua| ua.raw), }, }) .await?; diff --git a/crates/policy/src/lib.rs b/crates/policy/src/lib.rs index 44da50d38..4bc319391 100644 --- a/crates/policy/src/lib.rs +++ b/crates/policy/src/lib.rs @@ -307,7 +307,10 @@ mod tests { registration_method: RegistrationMethod::Password, username: "hello", email: Some("hello@example.com"), - requester: Requester { ip_address: None }, + requester: Requester { + ip_address: None, + user_agent: None, + }, }) .await .unwrap(); @@ -318,7 +321,10 @@ mod tests { registration_method: RegistrationMethod::Password, username: "hello", email: Some("hello@foo.element.io"), - requester: Requester { ip_address: None }, + requester: Requester { + ip_address: None, + user_agent: None, + }, }) .await .unwrap(); @@ -329,7 +335,10 @@ mod tests { registration_method: RegistrationMethod::Password, username: "hello", email: Some("hello@staging.element.io"), - requester: Requester { ip_address: None }, + requester: Requester { + ip_address: None, + user_agent: None, + }, }) .await .unwrap(); diff --git a/crates/policy/src/model.rs b/crates/policy/src/model.rs index d89b16779..702f4aab3 100644 --- a/crates/policy/src/model.rs +++ b/crates/policy/src/model.rs @@ -101,6 +101,9 @@ impl EvaluationResult { pub struct Requester { /// IP address of the entity making the request pub ip_address: Option, + + /// User agent of the entity making the request + pub user_agent: Option, } #[derive(Serialize, Debug)] diff --git a/policies/schema/authorization_grant_input.json b/policies/schema/authorization_grant_input.json index 5c431b431..f23bf7a73 100644 --- a/policies/schema/authorization_grant_input.json +++ b/policies/schema/authorization_grant_input.json @@ -45,6 +45,10 @@ "description": "IP address of the entity making the request", "type": "string", "format": "ip" + }, + "user_agent": { + "description": "User agent of the entity making the request", + "type": "string" } } } diff --git a/policies/schema/client_registration_input.json b/policies/schema/client_registration_input.json index 096c56a0a..461645126 100644 --- a/policies/schema/client_registration_input.json +++ b/policies/schema/client_registration_input.json @@ -25,6 +25,10 @@ "description": "IP address of the entity making the request", "type": "string", "format": "ip" + }, + "user_agent": { + "description": "User agent of the entity making the request", + "type": "string" } } } diff --git a/policies/schema/email_input.json b/policies/schema/email_input.json index 384c359b6..d97f291be 100644 --- a/policies/schema/email_input.json +++ b/policies/schema/email_input.json @@ -24,6 +24,10 @@ "description": "IP address of the entity making the request", "type": "string", "format": "ip" + }, + "user_agent": { + "description": "User agent of the entity making the request", + "type": "string" } } } diff --git a/policies/schema/register_input.json b/policies/schema/register_input.json index 27a19b78c..cd8868cd4 100644 --- a/policies/schema/register_input.json +++ b/policies/schema/register_input.json @@ -38,6 +38,10 @@ "description": "IP address of the entity making the request", "type": "string", "format": "ip" + }, + "user_agent": { + "description": "User agent of the entity making the request", + "type": "string" } } }