feat(appservice): ensure users/aliases outside of namespaces are not accessed
Signed-off-by: strawberry <strawberry@puppygock.gay>
This commit is contained in:
parent
b303a774d8
commit
19e4befcb8
7 changed files with 154 additions and 61 deletions
|
@ -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(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -124,7 +124,7 @@ where
|
|||
|
||||
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 @@ 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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> {
|
||||
|
|
|
@ -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<Vec<Namespace>> 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<String> {
|
||||
//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<Output = tokio::sync::RwLockReadGuard<'_, BTreeMap<String, RegistrationInfo>>> {
|
||||
self.registration_info.read()
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue