From 3086271139826902d05637682ae6e1ce6404627b Mon Sep 17 00:00:00 2001 From: Matthias Ahouansou Date: Tue, 16 Apr 2024 15:53:38 +0100 Subject: [PATCH] feat(appservice): ensure users/aliases outside of namespaces are not accessed --- src/api/client_server/account.rs | 43 ++++++++++++------- src/api/client_server/alias.rs | 36 ++++++++++++++++ src/api/client_server/room.rs | 18 +++++++- src/api/client_server/session.rs | 73 +++++++++++++++++++++++++++----- src/api/ruma_wrapper/axum.rs | 25 +++++++---- src/api/ruma_wrapper/mod.rs | 4 +- src/service/appservice/mod.rs | 42 +++++++++++++++++- 7 files changed, 202 insertions(+), 39 deletions(-) diff --git a/src/api/client_server/account.rs b/src/api/client_server/account.rs index 9b6bb5f8..0226abc7 100644 --- a/src/api/client_server/account.rs +++ b/src/api/client_server/account.rs @@ -75,20 +75,13 @@ pub async fn get_register_available_route( /// - Creates a new account and populates it with default account data /// - If `inhibit_login` is false: Creates a device and returns device id and access_token pub async fn register_route(body: Ruma) -> Result { - if !services().globals.allow_registration() && !body.from_appservice { + if !services().globals.allow_registration() && body.appservice_info.is_none() { return Err(Error::BadRequest( ErrorKind::Forbidden, "Registration has been disabled.", )); } - if body.body.login_type == Some(LoginType::ApplicationService) && !body.from_appservice { - return Err(Error::BadRequest( - ErrorKind::MissingToken, - "Missing appservice token.", - )); - } - let is_guest = body.kind == RegistrationKind::Guest; let user_id = match (&body.username, is_guest) { @@ -126,10 +119,30 @@ pub async fn register_route(body: Ruma) -> Result) -> Result) -> Result) -> Result) -> Result { if services() diff --git a/src/api/client_server/session.rs b/src/api/client_server/session.rs index 54069c03..3e583fac 100644 --- a/src/api/client_server/session.rs +++ b/src/api/client_server/session.rs @@ -67,6 +67,13 @@ pub async fn login_route(body: Ruma) -> Result) -> Result) -> Result { - if !body.from_appservice { - return Err(Error::BadRequest( - ErrorKind::MissingToken, - "Missing appservice token.", - )); - }; - if let Some(UserIdentifier::UserIdOrLocalpart(user_id)) = identifier { + let user_id = if let Some(UserIdentifier::UserIdOrLocalpart(user_id)) = identifier { UserId::parse_with_server_name( user_id.to_lowercase(), services().globals.server_name(), @@ -133,7 +145,23 @@ pub async fn login_route(body: Ruma) -> Result { warn!("Unsupported or unknown login type: {:?}", &body.login_info); @@ -199,6 +227,15 @@ pub async fn logout_route(body: Ruma) -> Result Result { let sender_user = body.sender_user.as_ref().expect("user is authenticated"); + if let Some(ref info) = body.appservice_info { + if !info.is_user_match(sender_user) { + return Err(Error::BadRequest( + ErrorKind::Exclusive, + "User is not in namespace.", + )); + } + } else { + return Err(Error::BadRequest( + ErrorKind::MissingToken, + "Missing appservice token.", + )); + } + for device_id in services().users.all_device_ids(sender_user).flatten() { services().users.remove_device(sender_user, &device_id)?; } diff --git a/src/api/ruma_wrapper/axum.rs b/src/api/ruma_wrapper/axum.rs index af2dbeb6..649c1f54 100644 --- a/src/api/ruma_wrapper/axum.rs +++ b/src/api/ruma_wrapper/axum.rs @@ -99,7 +99,7 @@ where let mut json_body = serde_json::from_slice::(&body).ok(); - let (sender_user, sender_device, sender_servername, from_appservice) = + let (sender_user, sender_device, sender_servername, appservice_info) = match (metadata.authentication, token) { (_, Token::Invalid) => { return Err(Error::BadRequest( @@ -122,6 +122,14 @@ where .map_err(|_| { Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid.") })?; + + if !info.is_user_match(&user_id) { + return Err(Error::BadRequest( + ErrorKind::Exclusive, + "User is not in namespace.", + )); + } + if !services().users.exists(&user_id)? { return Err(Error::BadRequest( ErrorKind::Forbidden, @@ -129,15 +137,14 @@ where )); } - // TODO: Check if appservice is allowed to be that user - (Some(user_id), None, None, true) + (Some(user_id), None, None, Some(*info)) } ( AuthScheme::None | AuthScheme::AppserviceToken | AuthScheme::AccessTokenOptional, - Token::Appservice(_), - ) => (None, None, None, true), + Token::Appservice(info), + ) => (None, None, None, Some(*info)), (AuthScheme::AccessToken, Token::None) => { return Err(Error::BadRequest( ErrorKind::MissingToken, @@ -147,7 +154,7 @@ where ( AuthScheme::AccessToken | AuthScheme::AccessTokenOptional | AuthScheme::None, Token::User((user_id, device_id)), - ) => (Some(user_id), Some(device_id), None, false), + ) => (Some(user_id), Some(device_id), None, None), (AuthScheme::ServerSignatures, Token::None) => { let TypedHeader(Authorization(x_matrix)) = parts .extract::>>() @@ -228,7 +235,7 @@ where BTreeMap::from_iter([(x_matrix.origin.as_str().to_owned(), keys)]); match ruma::signatures::verify_json(&pub_key_map, &request_map) { - Ok(()) => (None, None, Some(x_matrix.origin), false), + Ok(()) => (None, None, Some(x_matrix.origin), None), Err(e) => { warn!( "Failed to verify json request from {}: {}\n{:?}", @@ -255,7 +262,7 @@ where | AuthScheme::AppserviceToken | AuthScheme::AccessTokenOptional, Token::None, - ) => (None, None, None, false), + ) => (None, None, None, None), (AuthScheme::ServerSignatures, Token::Appservice(_) | Token::User(_)) => { return Err(Error::BadRequest( ErrorKind::Unauthorized, @@ -318,7 +325,7 @@ where sender_user, sender_device, sender_servername, - from_appservice, + appservice_info, json_body, }) } diff --git a/src/api/ruma_wrapper/mod.rs b/src/api/ruma_wrapper/mod.rs index ac4c825a..862da1dc 100644 --- a/src/api/ruma_wrapper/mod.rs +++ b/src/api/ruma_wrapper/mod.rs @@ -1,4 +1,4 @@ -use crate::Error; +use crate::{service::appservice::RegistrationInfo, Error}; use ruma::{ api::client::uiaa::UiaaResponse, CanonicalJsonValue, OwnedDeviceId, OwnedServerName, OwnedUserId, @@ -16,7 +16,7 @@ pub struct Ruma { pub sender_servername: Option, // This is None when body is not a valid string pub json_body: Option, - pub from_appservice: bool, + pub appservice_info: Option, } impl Deref for Ruma { diff --git a/src/service/appservice/mod.rs b/src/service/appservice/mod.rs index 7d2d46bd..9db6609e 100644 --- a/src/service/appservice/mod.rs +++ b/src/service/appservice/mod.rs @@ -6,7 +6,10 @@ pub use data::Data; use futures_util::Future; use regex::RegexSet; -use ruma::api::appservice::{Namespace, Registration}; +use ruma::{ + api::appservice::{Namespace, Registration}, + RoomAliasId, RoomId, UserId, +}; use tokio::sync::RwLock; use crate::{services, Result}; @@ -83,6 +86,18 @@ pub struct RegistrationInfo { pub rooms: NamespaceRegex, } +impl RegistrationInfo { + pub fn is_user_match(&self, user_id: &UserId) -> bool { + self.users.is_match(user_id.as_str()) + || self.registration.sender_localpart == user_id.localpart() + } + + pub fn is_exclusive_user_match(&self, user_id: &UserId) -> bool { + self.users.is_exclusive_match(user_id.as_str()) + || self.registration.sender_localpart == user_id.localpart() + } +} + impl TryFrom for RegistrationInfo { fn try_from(value: Registration) -> Result { Ok(RegistrationInfo { @@ -122,6 +137,7 @@ impl Service { } /// Registers an appservice and returns the ID to the caller. pub async fn register_appservice(&self, yaml: Registration) -> Result { + //TODO: Check for collisions between exclusive appservice namespaces services() .appservice .registration_info @@ -175,6 +191,30 @@ impl Service { .cloned() } + // Checks if a given user id matches any exclusive appservice regex + pub async fn is_exclusive_user_id(&self, user_id: &UserId) -> bool { + self.read() + .await + .values() + .any(|info| info.is_exclusive_user_match(user_id)) + } + + // Checks if a given room alias matches any exclusive appservice regex + pub async fn is_exclusive_alias(&self, alias: &RoomAliasId) -> bool { + self.read() + .await + .values() + .any(|info| info.aliases.is_exclusive_match(alias.as_str())) + } + + // Checks if a given room id matches any exclusive appservice regex + pub async fn is_exclusive_room_id(&self, room_id: &RoomId) -> bool { + self.read() + .await + .values() + .any(|info| info.rooms.is_exclusive_match(room_id.as_str())) + } + pub fn read( &self, ) -> impl Future>>