diff --git a/src/api/client_server/account.rs b/src/api/client_server/account.rs index 9c00c36f..f74cdaa7 100644 --- a/src/api/client_server/account.rs +++ b/src/api/client_server/account.rs @@ -83,7 +83,7 @@ pub async fn get_register_available_route( /// access_token #[allow(clippy::doc_markdown)] 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() { info!( "Registration disabled and request not from known appservice, rejecting registration attempt for username \ {:?}", @@ -92,10 +92,6 @@ pub async fn register_route(body: Ruma) -> Result) -> Result) -> Result) -> Result) -> Result) -> Result) -> Result) -> Result) -> Result return Err(Error::BadRequest(ErrorKind::Unknown, "Room alias is forbidden.")); } + if let Some(ref info) = body.appservice_info { + if !info.aliases.is_match(body.room_alias.as_str()) { + return Err(Error::BadRequest(ErrorKind::Exclusive, "Room alias is not in namespace.")); + } + } else if services() + .appservice + .is_exclusive_alias(&body.room_alias) + .await + { + return Err(Error::BadRequest(ErrorKind::Exclusive, "Room alias reserved by appservice.")); + } + if services() .rooms .alias @@ -73,6 +85,18 @@ pub async fn delete_alias_route(body: Ruma) -> Result return Err(Error::BadRequest(ErrorKind::NotFound, "Alias does not exist.")); } + if let Some(ref info) = body.appservice_info { + if !info.aliases.is_match(body.room_alias.as_str()) { + return Err(Error::BadRequest(ErrorKind::Exclusive, "Room alias is not in namespace.")); + } + } else if services() + .appservice + .is_exclusive_alias(&body.room_alias) + .await + { + return Err(Error::BadRequest(ErrorKind::Exclusive, "Room alias reserved by appservice.")); + } + if services() .rooms .alias diff --git a/src/api/client_server/room.rs b/src/api/client_server/room.rs index d2a62576..78ba5142 100644 --- a/src/api/client_server/room.rs +++ b/src/api/client_server/room.rs @@ -50,7 +50,10 @@ pub async fn create_room_route(body: Ruma) -> Result) -> Result { if services() diff --git a/src/api/client_server/session.rs b/src/api/client_server/session.rs index 70ed113c..21543637 100644 --- a/src/api/client_server/session.rs +++ b/src/api/client_server/session.rs @@ -66,27 +66,23 @@ pub async fn login_route(body: Ruma) -> Result { debug!("Got password login type"); - let username = if let Some(UserIdentifier::UserIdOrLocalpart(user_id)) = identifier { - debug!("Using username from identifier field"); - user_id.to_lowercase() - } else if let Some(user_id) = user { - warn!( - "User \"{}\" is attempting to login with the deprecated \"user\" field at \ - \"/_matrix/client/v3/login\". conduwuit implements this deprecated behaviour, but this is \ - destined to be removed in a future Matrix release.", - user_id - ); - user_id.to_lowercase() + let user_id = if let Some(UserIdentifier::UserIdOrLocalpart(user_id)) = identifier { + UserId::parse_with_server_name(user_id.to_lowercase(), services().globals.server_name()) + } else if let Some(user) = user { + UserId::parse(user) } else { warn!("Bad login type: {:?}", &body.login_info); return Err(Error::BadRequest(ErrorKind::forbidden(), "Bad login type.")); - }; - - let user_id = UserId::parse_with_server_name(username, services().globals.server_name()).map_err(|e| { - warn!("Failed to parse username from user logging in: {}", e); + } + .map_err(|e| { + warn!("Failed to parse username from user logging in: {e}"); Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid.") })?; + if services().appservice.is_exclusive_user_id(&user_id).await { + return Err(Error::BadRequest(ErrorKind::Exclusive, "User ID reserved by appservice.")); + } + let hash = services() .users .password_hash(&user_id)? @@ -121,16 +117,23 @@ pub async fn login_route(body: Ruma) -> Result(token, jwt_decoding_key, &jsonwebtoken::Validation::default()) .map_err(|e| { - warn!("Failed to parse JWT token from user logging in: {}", e); + warn!("Failed to parse JWT token from user logging in: {e}"); Error::BadRequest(ErrorKind::InvalidUsername, "Token is invalid.") })?; let username = token.claims.sub.to_lowercase(); - UserId::parse_with_server_name(username, services().globals.server_name()).map_err(|e| { - warn!("Failed to parse username from user logging in: {}", e); - Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid.") - })? + let user_id = + UserId::parse_with_server_name(username, services().globals.server_name()).map_err(|e| { + warn!("Failed to parse username from user logging in: {e}"); + Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid.") + })?; + + if services().appservice.is_exclusive_user_id(&user_id).await { + return Err(Error::BadRequest(ErrorKind::Exclusive, "User ID reserved by appservice.")); + } + + user_id } else { return Err(Error::BadRequest( ErrorKind::Unknown, @@ -144,27 +147,28 @@ pub async fn login_route(body: Ruma) -> Result { debug!("Got appservice login type"); - if !body.from_appservice { - return Err(Error::BadRequest(ErrorKind::MissingToken, "Missing Appservice token.")); - }; - let username = if let Some(UserIdentifier::UserIdOrLocalpart(user_id)) = identifier { - user_id.to_lowercase() - } else if let Some(user_id) = user { - warn!( - "Appservice \"{}\" is attempting to login with the deprecated \"user\" field at \ - \"/_matrix/client/v3/login\". conduwuit implements this deprecated behaviour, but this is \ - destined to be removed in a future Matrix release.", - user_id - ); - user_id.to_lowercase() + let user_id = if let Some(UserIdentifier::UserIdOrLocalpart(user_id)) = identifier { + UserId::parse_with_server_name(user_id.to_lowercase(), services().globals.server_name()) + } else if let Some(user) = user { + UserId::parse(user) } else { + warn!("Bad login type: {:?}", &body.login_info); return Err(Error::BadRequest(ErrorKind::forbidden(), "Bad login type.")); - }; - - UserId::parse_with_server_name(username, services().globals.server_name()).map_err(|e| { - warn!("Failed to parse username from appservice logging in: {}", e); + } + .map_err(|e| { + warn!("Failed to parse username from appservice logging in: {e}"); Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid.") - })? + })?; + + if let Some(ref info) = body.appservice_info { + if !info.is_user_match(&user_id) { + return Err(Error::BadRequest(ErrorKind::Exclusive, "User is not in namespace.")); + } + } else { + return Err(Error::BadRequest(ErrorKind::MissingToken, "Missing appservice token.")); + } + + user_id }, _ => { warn!("Unsupported or unknown login type: {:?}", &body.login_info); diff --git a/src/api/ruma_wrapper/axum.rs b/src/api/ruma_wrapper/axum.rs index b7bdc03a..3de3b842 100644 --- a/src/api/ruma_wrapper/axum.rs +++ b/src/api/ruma_wrapper/axum.rs @@ -124,7 +124,7 @@ where let mut json_body = serde_json::from_slice::(&body).ok(); - let (sender_user, sender_device, sender_servername, from_appservice) = match (metadata.authentication, token) { + let (sender_user, sender_device, sender_servername, appservice_info) = match (metadata.authentication, token) { (_, Token::Invalid) => { return Err(Error::BadRequest( ErrorKind::UnknownToken { @@ -146,21 +146,27 @@ where UserId::parse, ) .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(), "User does not exist.")); } - // 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, Token::Appservice(info)) => { + (None, None, None, Some(*info)) }, - (AuthScheme::None | AuthScheme::AppserviceToken, Token::Appservice(_)) => (None, None, None, true), (AuthScheme::AccessToken, Token::None) => { return Err(Error::BadRequest(ErrorKind::MissingToken, "Missing access token.")); }, ( 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) => { if !services().globals.allow_federation() { return Err(Error::bad_config("Federation is disabled.")); @@ -234,7 +240,7 @@ where let pub_key_map = 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 {}: {e}\n{request_map:?}", x_matrix.origin); @@ -253,7 +259,7 @@ where } }, (AuthScheme::None | AuthScheme::AppserviceToken | AuthScheme::AccessTokenOptional, Token::None) => { - (None, None, None, false) + (None, None, None, None) }, (AuthScheme::ServerSignatures, Token::Appservice(_) | Token::User(_)) => { return Err(Error::BadRequest( @@ -322,7 +328,7 @@ where sender_device, sender_servername, json_body, - from_appservice, + appservice_info, }) } } diff --git a/src/api/ruma_wrapper/mod.rs b/src/api/ruma_wrapper/mod.rs index 2b0cd06b..0dd3a617 100644 --- a/src/api/ruma_wrapper/mod.rs +++ b/src/api/ruma_wrapper/mod.rs @@ -2,7 +2,7 @@ use std::ops::Deref; use ruma::{api::client::uiaa::UiaaResponse, CanonicalJsonValue, OwnedDeviceId, OwnedServerName, OwnedUserId}; -use crate::Error; +use crate::{service::appservice::RegistrationInfo, Error}; mod axum; @@ -14,7 +14,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 cf68db21..ea387881 100644 --- a/src/service/appservice/mod.rs +++ b/src/service/appservice/mod.rs @@ -5,7 +5,10 @@ use std::collections::BTreeMap; pub(crate) 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}; @@ -43,6 +46,16 @@ impl 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 NamespaceRegex { type Error = regex::Error; @@ -122,6 +135,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 +189,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>> { self.registration_info.read() }