diff --git a/src/api/client_server/account.rs b/src/api/client_server/account.rs index 9c00c36f1b396a79877df6d0b2238494c41bda27..f74cdaa7de1773e474cbdc88b0d87f1c48ea8330 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<register::v3::Request>) -> Result<register::v3::Response> { - 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<register::v3::Request>) -> Result<registe 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; if is_guest @@ -160,6 +156,18 @@ pub async fn register_route(body: Ruma<register::v3::Request>) -> Result<registe }, }; + if body.body.login_type == Some(LoginType::ApplicationService) { + 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.")); + } + } else if services().appservice.is_exclusive_user_id(&user_id).await { + return Err(Error::BadRequest(ErrorKind::Exclusive, "User ID reserved by appservice.")); + } + // UIAA let mut uiaainfo; let skip_auth; @@ -174,7 +182,7 @@ pub async fn register_route(body: Ruma<register::v3::Request>) -> Result<registe session: None, auth_error: None, }; - skip_auth = body.from_appservice; + skip_auth = body.appservice_info.is_some(); } else { // No registration token necessary, but clients must still go through the flow uiaainfo = UiaaInfo { @@ -186,7 +194,7 @@ pub async fn register_route(body: Ruma<register::v3::Request>) -> Result<registe session: None, auth_error: None, }; - skip_auth = body.from_appservice || is_guest; + skip_auth = body.appservice_info.is_some() || is_guest; } if !skip_auth { @@ -281,7 +289,7 @@ pub async fn register_route(body: Ruma<register::v3::Request>) -> Result<registe info!("New user \"{}\" registered on this server.", user_id); // log in conduit admin channel if a non-guest user registered - if !body.from_appservice && !is_guest { + if body.appservice_info.is_none() && !is_guest { services() .admin .send_message(RoomMessageEventContent::notice_plain(format!( @@ -290,7 +298,7 @@ pub async fn register_route(body: Ruma<register::v3::Request>) -> Result<registe } // log in conduit admin channel if a guest registered - if !body.from_appservice && is_guest && services().globals.log_guest_registrations() { + if body.appservice_info.is_none() && is_guest && services().globals.log_guest_registrations() { if let Some(device_display_name) = &body.initial_device_display_name { if body .initial_device_display_name @@ -339,7 +347,7 @@ pub async fn register_route(body: Ruma<register::v3::Request>) -> Result<registe } } - if !body.from_appservice + if body.appservice_info.is_none() && !services().globals.config.auto_join_rooms.is_empty() && (services().globals.allow_guests_auto_join_rooms() || !is_guest) { @@ -468,7 +476,7 @@ pub async fn whoami_route(body: Ruma<whoami::v3::Request>) -> Result<whoami::v3: Ok(whoami::v3::Response { user_id: sender_user.clone(), device_id, - is_guest: services().users.is_deactivated(sender_user)? && !body.from_appservice, + is_guest: services().users.is_deactivated(sender_user)? && body.appservice_info.is_none(), }) } diff --git a/src/api/client_server/alias.rs b/src/api/client_server/alias.rs index fb57896d44395dd79445f7b7a2c209cb566b6c53..3fafee727247b2dffd47f3ad72f051cd5e274052 100644 --- a/src/api/client_server/alias.rs +++ b/src/api/client_server/alias.rs @@ -29,6 +29,18 @@ pub async fn create_alias_route(body: Ruma<create_alias::v3::Request>) -> 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<delete_alias::v3::Request>) -> 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 d2a62576feaf71c738476cdb9e27d72646d0d321..78ba5142b7a35026bf12494c7c86ec4a0fd90120 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<create_room::v3::Request>) -> Result<c let sender_user = body.sender_user.as_ref().expect("user is authenticated"); - if !services().globals.allow_room_creation() && !&body.from_appservice && !services().users.is_admin(sender_user)? { + if !services().globals.allow_room_creation() + && body.appservice_info.is_none() + && !services().users.is_admin(sender_user)? + { return Err(Error::BadRequest(ErrorKind::forbidden(), "Room creation has been disabled.")); } @@ -184,6 +187,16 @@ pub async fn create_room_route(body: Ruma<create_room::v3::Request>) -> Result<c } })?; + if let Some(ref alias) = alias { + if let Some(ref info) = body.appservice_info { + if !info.aliases.is_match(alias.as_str()) { + return Err(Error::BadRequest(ErrorKind::Exclusive, "Room alias is not in namespace.")); + } + } else if services().appservice.is_exclusive_alias(alias).await { + return Err(Error::BadRequest(ErrorKind::Exclusive, "Room alias reserved by appservice.")); + } + } + let room_version = match body.room_version.clone() { Some(room_version) => { if services() diff --git a/src/api/client_server/session.rs b/src/api/client_server/session.rs index 70ed113c264dcb57d28262aa2ae1107ab01acbf0..215436376b92f77ea78487a0b3b7778b702d8617 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<login::v3::Request>) -> Result<login::v3::Re .. }) => { 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<login::v3::Request>) -> Result<login::v3::Re let token = jsonwebtoken::decode::<Claims>(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<login::v3::Request>) -> Result<login::v3::Re user, }) => { 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 b7bdc03a35360cb4dd51da68dd564143032b58ce..3de3b8420d294cc56777ad402bd1b8b592331e9a 100644 --- a/src/api/ruma_wrapper/axum.rs +++ b/src/api/ruma_wrapper/axum.rs @@ -124,7 +124,7 @@ async fn from_request(req: Request<B>, _state: &S) -> Result<Self, Self::Rejecti let mut json_body = serde_json::from_slice::<CanonicalJsonValue>(&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 @@ async fn from_request(req: Request<B>, _state: &S) -> Result<Self, Self::Rejecti 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 @@ async fn from_request(req: Request<B>, _state: &S) -> Result<Self, Self::Rejecti 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 @@ async fn from_request(req: Request<B>, _state: &S) -> Result<Self, Self::Rejecti } }, (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 @@ async fn from_request(req: Request<B>, _state: &S) -> Result<Self, Self::Rejecti 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 2b0cd06ba0ff1ef6e3a694f727acac3fa9aa6b03..0dd3a617e3c2d842d8f393b5d4c8caca751feb4b 100644 --- a/src/api/ruma_wrapper/mod.rs +++ b/src/api/ruma_wrapper/mod.rs @@ -2,7 +2,7 @@ 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<T> { pub sender_servername: Option<OwnedServerName>, // This is None when body is not a valid string pub json_body: Option<CanonicalJsonValue>, - pub from_appservice: bool, + pub appservice_info: Option<RegistrationInfo>, } impl<T> Deref for Ruma<T> { diff --git a/src/service/appservice/mod.rs b/src/service/appservice/mod.rs index cf68db218aa69a1c2bce30b4d4273c2b89082d04..ea387881c4e80f02f0640d339d6efc87a9c0e9a9 100644 --- a/src/service/appservice/mod.rs +++ b/src/service/appservice/mod.rs @@ -5,7 +5,10 @@ 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 @@ pub fn is_exclusive_match(&self, heystack: &str) -> bool { } } +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<Vec<Namespace>> for NamespaceRegex { type Error = regex::Error; @@ -122,6 +135,7 @@ pub fn build(db: &'static dyn Data) -> Result<Self> { /// Registers an appservice and returns the ID to the caller pub async fn register_appservice(&self, yaml: Registration) -> Result<String> { + //TODO: Check for collisions between exclusive appservice namespaces services() .appservice .registration_info @@ -175,6 +189,30 @@ pub async fn find_from_token(&self, token: &str) -> Option<RegistrationInfo> { .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<Output = tokio::sync::RwLockReadGuard<'_, BTreeMap<String, RegistrationInfo>>> { self.registration_info.read() }