Abstract and dedup the general UIAA pattern into api::router.

Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
Jason Volk
2025-09-10 10:08:11 +00:00
parent 8ed61aecb0
commit 54b347b855
5 changed files with 103 additions and 180 deletions

View File

@@ -1,18 +1,13 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use futures::{FutureExt, StreamExt};
use ruma::api::client::{
account::{
ThirdPartyIdRemovalStatus, change_password, deactivate, get_3pids,
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn,
whoami,
},
uiaa::{AuthData, AuthFlow, AuthType, Jwt, UiaaInfo},
use ruma::api::client::account::{
ThirdPartyIdRemovalStatus, change_password, deactivate, get_3pids,
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn, whoami,
};
use tuwunel_core::{Err, Error, Result, err, info, utils, utils::ReadyExt};
use tuwunel_core::{Err, Result, info, utils::ReadyExt};
use super::{SESSION_ID_LENGTH, session::jwt::validate_user};
use crate::Ruma;
use crate::{Ruma, router::auth_uiaa};
/// # `POST /_matrix/client/r0/account/password`
///
@@ -37,45 +32,7 @@ pub(crate) async fn change_password_route(
InsecureClientIp(client): InsecureClientIp,
body: Ruma<change_password::v3::Request>,
) -> Result<change_password::v3::Response> {
// Authentication for this endpoint was made optional, but we need
// authentication currently
let sender_user = body
.sender_user
.as_ref()
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
..Default::default()
};
match &body.auth {
| Some(auth) => {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, body.sender_device(), auth, &uiaainfo)
.await?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
// Success!
},
| _ => match body.json_body {
| Some(ref json) => {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, body.sender_device(), &uiaainfo, json);
return Err(Error::Uiaa(uiaainfo));
},
| _ => {
return Err!(Request(NotJson("JSON body is not valid")));
},
},
}
let ref sender_user = auth_uiaa(&services, &body).await?;
services
.users
@@ -87,7 +44,7 @@ pub(crate) async fn change_password_route(
services
.users
.all_device_ids(sender_user)
.ready_filter(|id| *id != body.sender_device())
.ready_filter(|&id| Some(id) != body.sender_device.as_deref())
.for_each(|id| services.users.remove_device(sender_user, id))
.await;
}
@@ -140,69 +97,15 @@ pub(crate) async fn deactivate_route(
InsecureClientIp(client): InsecureClientIp,
body: Ruma<deactivate::v3::Request>,
) -> Result<deactivate::v3::Response> {
let pass_flow = AuthFlow { stages: vec![AuthType::Password] };
let jwt_flow = AuthFlow { stages: vec![AuthType::Jwt] };
let mut uiaainfo = UiaaInfo {
flows: [pass_flow, jwt_flow].into(),
..Default::default()
};
let sender_user = match &body.auth {
| Some(AuthData::Jwt(Jwt { token, .. })) => {
let sender_user = validate_user(&services, token)?;
if !services.users.exists(&sender_user).await {
return Err!(Request(NotFound("User {sender_user} is not registered.")));
}
// Success!
sender_user
},
| Some(auth) => {
let sender_user = body
.sender_user
.as_deref()
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, body.sender_device(), auth, &uiaainfo)
.await?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
// Success!
sender_user.to_owned()
},
| _ => match body.json_body {
| Some(ref json) => {
let sender_user = body
.sender_user
.as_ref()
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, body.sender_device(), &uiaainfo, json);
return Err(Error::Uiaa(uiaainfo));
},
| _ => {
return Err!(Request(NotJson("JSON body is not valid")));
},
},
};
let ref sender_user = auth_uiaa(&services, &body).await?;
services
.deactivate
.full_deactivate(&sender_user)
.full_deactivate(sender_user)
.boxed()
.await?;
info!("User {sender_user} deactivated their account.");
if services.server.config.admin_room_notices {
services
.admin

View File

@@ -3,16 +3,13 @@ use axum_client_ip::InsecureClientIp;
use futures::StreamExt;
use ruma::{
MilliSecondsSinceUnixEpoch, OwnedDeviceId,
api::client::{
device::{self, delete_device, delete_devices, get_device, get_devices, update_device},
error::ErrorKind,
uiaa::{AuthFlow, AuthType, UiaaInfo},
api::client::device::{
self, delete_device, delete_devices, get_device, get_devices, update_device,
},
};
use tuwunel_core::{Err, Error, Result, debug, err, utils};
use tuwunel_core::{Err, Result, debug, err, utils};
use super::SESSION_ID_LENGTH;
use crate::{Ruma, client::DEVICE_ID_LENGTH};
use crate::{Ruma, client::DEVICE_ID_LENGTH, router::auth_uiaa};
/// # `GET /_matrix/client/r0/devices`
///
@@ -126,10 +123,10 @@ pub(crate) async fn delete_device_route(
State(services): State<crate::State>,
body: Ruma<delete_device::v3::Request>,
) -> Result<delete_device::v3::Response> {
let (sender_user, sender_device) = body.sender();
let appservice = body.appservice_info.as_ref();
if appservice.is_some_and(|appservice| appservice.registration.device_management) {
let sender_user = body.sender_user();
debug!(
"Skipping UIAA for {sender_user} as this is from an appservice and MSC4190 is \
enabled"
@@ -142,38 +139,7 @@ pub(crate) async fn delete_device_route(
return Ok(delete_device::v3::Response {});
}
// UIAA
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
..Default::default()
};
match &body.auth {
| Some(auth) => {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)
.await?;
if !worked {
return Err!(Uiaa(uiaainfo));
}
// Success!
},
| _ => match body.json_body {
| Some(ref json) => {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, sender_device, &uiaainfo, json);
return Err!(Uiaa(uiaainfo));
},
| _ => {
return Err!(Request(NotJson("Not json.")));
},
},
}
let ref sender_user = auth_uiaa(&services, &body).await?;
services
.users
@@ -200,10 +166,10 @@ pub(crate) async fn delete_devices_route(
State(services): State<crate::State>,
body: Ruma<delete_devices::v3::Request>,
) -> Result<delete_devices::v3::Response> {
let (sender_user, sender_device) = body.sender();
let appservice = body.appservice_info.as_ref();
if appservice.is_some_and(|appservice| appservice.registration.device_management) {
let sender_user = body.sender_user();
debug!(
"Skipping UIAA for {sender_user} as this is from an appservice and MSC4190 is \
enabled"
@@ -218,38 +184,7 @@ pub(crate) async fn delete_devices_route(
return Ok(delete_devices::v3::Response {});
}
// UIAA
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
..Default::default()
};
match &body.auth {
| Some(auth) => {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)
.await?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
// Success!
},
| _ => match body.json_body {
| Some(ref json) => {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, sender_device, &uiaainfo, json);
return Err(Error::Uiaa(uiaainfo));
},
| _ => {
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
},
},
}
let ref sender_user = auth_uiaa(&services, &body).await?;
for device_id in &body.devices {
services

View File

@@ -16,7 +16,9 @@ use http::{Uri, uri};
use tuwunel_core::{Server, err};
use self::handler::RouterExt;
pub(super) use self::{args::Args as Ruma, response::RumaResponse, state::State};
pub(super) use self::{
args::Args as Ruma, auth::auth_uiaa, response::RumaResponse, state::State,
};
use crate::{client, server};
pub fn build(router: Router<State>, server: &Server) -> Router<State> {

View File

@@ -1,5 +1,6 @@
mod appservice;
mod server;
mod uiaa;
use std::{fmt::Debug, time::SystemTime};
@@ -35,6 +36,7 @@ use ruma::{
use tuwunel_core::{Err, Error, Result, is_less_than, utils::result::LogDebugErr};
use tuwunel_service::{Services, appservice::RegistrationInfo};
pub(crate) use self::uiaa::auth_uiaa;
use self::{appservice::auth_appservice, server::auth_server};
use super::request::Request;

View File

@@ -0,0 +1,81 @@
use ruma::{
CanonicalJsonValue, OwnedUserId,
api::{
IncomingRequest,
client::uiaa::{AuthData, AuthFlow, AuthType, Jwt, UiaaInfo},
},
};
use tuwunel_core::{Err, Error, Result, err, utils};
use tuwunel_service::{Services, uiaa::SESSION_ID_LENGTH};
use crate::{Ruma, client::jwt};
pub(crate) async fn auth_uiaa<T>(services: &Services, body: &Ruma<T>) -> Result<OwnedUserId>
where
T: IncomingRequest + Send + Sync,
{
let flows = [
AuthFlow::new([AuthType::Password].into()),
AuthFlow::new([AuthType::Jwt].into()),
];
let mut uiaainfo = UiaaInfo {
flows: flows.into(),
..Default::default()
};
match body
.json_body
.as_ref()
.and_then(CanonicalJsonValue::as_object)
.and_then(|body| body.get("auth"))
.cloned()
.map(CanonicalJsonValue::into)
.map(serde_json::from_value)
.transpose()?
{
| Some(AuthData::Jwt(Jwt { ref token, .. })) => {
let sender_user = jwt::validate_user(services, token)?;
if !services.users.exists(&sender_user).await {
return Err!(Request(NotFound("User {sender_user} is not registered.")));
}
// Success!
Ok(sender_user)
},
| Some(ref auth) => {
let sender_user = body
.sender_user
.as_deref()
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, body.sender_device(), auth, &uiaainfo)
.await?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
// Success!
Ok(sender_user.to_owned())
},
| _ => match body.json_body {
| Some(ref json) => {
let sender_user = body
.sender_user
.as_ref()
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, body.sender_device(), &uiaainfo, json);
Err(Error::Uiaa(uiaainfo))
},
| _ => Err!(Request(NotJson("JSON body is not valid"))),
},
}
}