improvement: uiaa works like in synapse

This commit is contained in:
Timo Kösters 2021-05-04 19:03:18 +02:00
parent e1c4e5c73e
commit cf94b8e712
No known key found for this signature in database
GPG key ID: 24DA7517711A2BA4
10 changed files with 326 additions and 177 deletions

View file

@ -1,4 +1,7 @@
use std::{collections::BTreeMap, convert::TryInto};
use std::{
collections::BTreeMap,
convert::{TryFrom, TryInto},
};
use super::{State, DEVICE_ID_LENGTH, SESSION_ID_LENGTH, TOKEN_LENGTH};
use crate::{pdu::PduBuilder, utils, ConduitResult, Database, Error, Ruma};
@ -143,16 +146,28 @@ pub async fn register_route(
if !body.from_appservice {
if let Some(auth) = &body.auth {
let (worked, uiaainfo) =
db.uiaa
.try_auth(&user_id, "".into(), auth, &uiaainfo, &db.users, &db.globals)?;
let (worked, uiaainfo) = db.uiaa.try_auth(
&UserId::parse_with_server_name("", db.globals.server_name())
.expect("we know this is valid"),
"".into(),
auth,
&uiaainfo,
&db.users,
&db.globals,
)?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
// Success!
} else {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
db.uiaa.create(&user_id, "".into(), &uiaainfo)?;
db.uiaa.create(
&UserId::parse_with_server_name("", db.globals.server_name())
.expect("we know this is valid"),
"".into(),
&uiaainfo,
&body.json_body.expect("body is json"),
)?;
return Err(Error::Uiaa(uiaainfo));
}
}
@ -526,7 +541,12 @@ pub async fn change_password_route(
// Success!
} else {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
db.uiaa.create(&sender_user, &sender_device, &uiaainfo)?;
db.uiaa.create(
&sender_user,
&sender_device,
&uiaainfo,
&body.json_body.expect("body is json"),
)?;
return Err(Error::Uiaa(uiaainfo));
}
@ -612,7 +632,12 @@ pub async fn deactivate_route(
// Success!
} else {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
db.uiaa.create(&sender_user, &sender_device, &uiaainfo)?;
db.uiaa.create(
&sender_user,
&sender_device,
&uiaainfo,
&body.json_body.expect("body is json"),
)?;
return Err(Error::Uiaa(uiaainfo));
}

View file

@ -115,7 +115,12 @@ pub async fn delete_device_route(
// Success!
} else {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
db.uiaa.create(&sender_user, &sender_device, &uiaainfo)?;
db.uiaa.create(
&sender_user,
&sender_device,
&uiaainfo,
&body.json_body.expect("body is json"),
)?;
return Err(Error::Uiaa(uiaainfo));
}
@ -164,7 +169,12 @@ pub async fn delete_devices_route(
// Success!
} else {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
db.uiaa.create(&sender_user, &sender_device, &uiaainfo)?;
db.uiaa.create(
&sender_user,
&sender_device,
&uiaainfo,
&body.json_body.expect("body is json"),
)?;
return Err(Error::Uiaa(uiaainfo));
}

View file

@ -203,19 +203,20 @@ pub async fn get_public_rooms_filtered_helper(
}
}
let mut all_rooms =
db.rooms
.public_rooms()
.map(|room_id| {
let room_id = room_id?;
let mut all_rooms = db
.rooms
.public_rooms()
.map(|room_id| {
let room_id = room_id?;
let chunk = PublicRoomsChunk {
aliases: Vec::new(),
canonical_alias: db
.rooms
.room_state_get(&room_id, &EventType::RoomCanonicalAlias, "")?
.map_or(Ok::<_, Error>(None), |s| {
Ok(serde_json::from_value::<
let chunk = PublicRoomsChunk {
aliases: Vec::new(),
canonical_alias: db
.rooms
.room_state_get(&room_id, &EventType::RoomCanonicalAlias, "")?
.map_or(Ok::<_, Error>(None), |s| {
Ok(
serde_json::from_value::<
Raw<canonical_alias::CanonicalAliasEventContent>,
>(s.content)
.expect("from_value::<Raw<..>> can never fail")
@ -223,62 +224,61 @@ pub async fn get_public_rooms_filtered_helper(
.map_err(|_| {
Error::bad_database("Invalid canonical alias event in database.")
})?
.alias)
})?,
name: db
.rooms
.room_state_get(&room_id, &EventType::RoomName, "")?
.map_or(Ok::<_, Error>(None), |s| {
Ok(serde_json::from_value::<Raw<name::NameEventContent>>(
s.content,
)
.expect("from_value::<Raw<..>> can never fail")
.deserialize()
.map_err(|_| {
Error::bad_database("Invalid room name event in database.")
})?
.name()
.map(|n| n.to_owned()))
})?,
num_joined_members: (db.rooms.room_members(&room_id).count() as u32).into(),
topic: db
.rooms
.room_state_get(&room_id, &EventType::RoomTopic, "")?
.map_or(Ok::<_, Error>(None), |s| {
Ok(Some(
serde_json::from_value::<Raw<topic::TopicEventContent>>(
s.content,
)
.alias,
)
})?,
name: db
.rooms
.room_state_get(&room_id, &EventType::RoomName, "")?
.map_or(Ok::<_, Error>(None), |s| {
Ok(
serde_json::from_value::<Raw<name::NameEventContent>>(s.content)
.expect("from_value::<Raw<..>> can never fail")
.deserialize()
.map_err(|_| {
Error::bad_database("Invalid room name event in database.")
})?
.name()
.map(|n| n.to_owned()),
)
})?,
num_joined_members: (db.rooms.room_members(&room_id).count() as u32).into(),
topic: db
.rooms
.room_state_get(&room_id, &EventType::RoomTopic, "")?
.map_or(Ok::<_, Error>(None), |s| {
Ok(Some(
serde_json::from_value::<Raw<topic::TopicEventContent>>(s.content)
.expect("from_value::<Raw<..>> can never fail")
.deserialize()
.map_err(|_| {
Error::bad_database("Invalid room topic event in database.")
})?
.topic,
))
})?,
world_readable: db
.rooms
.room_state_get(&room_id, &EventType::RoomHistoryVisibility, "")?
.map_or(Ok::<_, Error>(false), |s| {
Ok(serde_json::from_value::<
Raw<history_visibility::HistoryVisibilityEventContent>,
>(s.content)
.expect("from_value::<Raw<..>> can never fail")
.deserialize()
.map_err(|_| {
Error::bad_database(
"Invalid room history visibility event in database.",
)
})?
.history_visibility
== history_visibility::HistoryVisibility::WorldReadable)
})?,
guest_can_join: db
.rooms
.room_state_get(&room_id, &EventType::RoomGuestAccess, "")?
.map_or(Ok::<_, Error>(false), |s| {
Ok(
))
})?,
world_readable: db
.rooms
.room_state_get(&room_id, &EventType::RoomHistoryVisibility, "")?
.map_or(Ok::<_, Error>(false), |s| {
Ok(serde_json::from_value::<
Raw<history_visibility::HistoryVisibilityEventContent>,
>(s.content)
.expect("from_value::<Raw<..>> can never fail")
.deserialize()
.map_err(|_| {
Error::bad_database(
"Invalid room history visibility event in database.",
)
})?
.history_visibility
== history_visibility::HistoryVisibility::WorldReadable)
})?,
guest_can_join: db
.rooms
.room_state_get(&room_id, &EventType::RoomGuestAccess, "")?
.map_or(Ok::<_, Error>(false), |s| {
Ok(
serde_json::from_value::<Raw<guest_access::GuestAccessEventContent>>(
s.content,
)
@ -290,33 +290,31 @@ pub async fn get_public_rooms_filtered_helper(
.guest_access
== guest_access::GuestAccess::CanJoin,
)
})?,
avatar_url: db
.rooms
.room_state_get(&room_id, &EventType::RoomAvatar, "")?
.map(|s| {
Ok::<_, Error>(
serde_json::from_value::<Raw<avatar::AvatarEventContent>>(
s.content,
)
})?,
avatar_url: db
.rooms
.room_state_get(&room_id, &EventType::RoomAvatar, "")?
.map(|s| {
Ok::<_, Error>(
serde_json::from_value::<Raw<avatar::AvatarEventContent>>(s.content)
.expect("from_value::<Raw<..>> can never fail")
.deserialize()
.map_err(|_| {
Error::bad_database("Invalid room avatar event in database.")
})?
.url,
)
})
.transpose()?
// url is now an Option<String> so we must flatten
.flatten(),
room_id,
};
Ok(chunk)
})
.filter_map(|r: Result<_>| r.ok()) // Filter out buggy rooms
// We need to collect all, so we can sort by member count
.collect::<Vec<_>>();
)
})
.transpose()?
// url is now an Option<String> so we must flatten
.flatten(),
room_id,
};
Ok(chunk)
})
.filter_map(|r: Result<_>| r.ok()) // Filter out buggy rooms
// We need to collect all, so we can sort by member count
.collect::<Vec<_>>();
all_rooms.sort_by(|l, r| r.num_joined_members.cmp(&l.num_joined_members));

View file

@ -220,7 +220,12 @@ pub async fn upload_signing_keys_route(
// Success!
} else {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
db.uiaa.create(&sender_user, &sender_device, &uiaainfo)?;
db.uiaa.create(
&sender_user,
&sender_device,
&uiaainfo,
&body.json_body.expect("body is json"),
)?;
return Err(Error::Uiaa(uiaainfo));
}

View file

@ -56,13 +56,8 @@ pub async fn send_message_event_route(
let event_id = db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::from(&body.event_type),
content: serde_json::from_str(
body.json_body
.as_ref()
.ok_or(Error::BadRequest(ErrorKind::BadJson, "Invalid JSON body."))?
.get(),
)
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Invalid JSON body."))?,
content: serde_json::from_str(body.body.body.json().get())
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Invalid JSON body."))?,
unsigned: Some(unsigned),
state_key: None,
redacts: None,

View file

@ -69,9 +69,9 @@ use {
ruma::api::client::r0::to_device::send_event_to_device,
};
const DEVICE_ID_LENGTH: usize = 10;
const TOKEN_LENGTH: usize = 256;
const SESSION_ID_LENGTH: usize = 256;
pub const DEVICE_ID_LENGTH: usize = 10;
pub const TOKEN_LENGTH: usize = 256;
pub const SESSION_ID_LENGTH: usize = 256;
#[cfg(feature = "conduit_bin")]
#[options("/<_..>")]

View file

@ -135,7 +135,8 @@ impl Database {
todeviceid_events: db.open_tree("todeviceid_events")?,
},
uiaa: uiaa::Uiaa {
userdeviceid_uiaainfo: db.open_tree("userdeviceid_uiaainfo")?,
userdevicesessionid_uiaainfo: db.open_tree("userdevicesessionid_uiaainfo")?,
userdevicesessionid_uiaarequest: db.open_tree("userdevicesessionid_uiaarequest")?,
},
rooms: rooms::Rooms {
edus: rooms::RoomEdus {

View file

@ -1,15 +1,17 @@
use crate::{Error, Result};
use crate::{client_server::SESSION_ID_LENGTH, utils, Error, Result};
use ruma::{
api::client::{
error::ErrorKind,
r0::uiaa::{IncomingAuthData, UiaaInfo},
},
signatures::CanonicalJsonValue,
DeviceId, UserId,
};
#[derive(Clone)]
pub struct Uiaa {
pub(super) userdeviceid_uiaainfo: sled::Tree, // User-interactive authentication
pub(super) userdevicesessionid_uiaainfo: sled::Tree, // User-interactive authentication
pub(super) userdevicesessionid_uiaarequest: sled::Tree, // UiaaRequest = canonical json value
}
impl Uiaa {
@ -19,8 +21,20 @@ impl Uiaa {
user_id: &UserId,
device_id: &DeviceId,
uiaainfo: &UiaaInfo,
json_body: &CanonicalJsonValue,
) -> Result<()> {
self.update_uiaa_session(user_id, device_id, Some(uiaainfo))
self.set_uiaa_request(
user_id,
device_id,
uiaainfo.session.as_ref().expect("session should be set"), // TODO: better session error handling (why is it optional in ruma?)
json_body,
)?;
self.update_uiaa_session(
user_id,
device_id,
uiaainfo.session.as_ref().expect("session should be set"),
Some(uiaainfo),
)
}
pub fn try_auth(
@ -45,6 +59,10 @@ impl Uiaa {
})
.unwrap_or_else(|| Ok(uiaainfo.clone()))?;
if uiaainfo.session.is_none() {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
}
// Find out what the user completed
match &**kind {
"m.login.password" => {
@ -130,35 +148,96 @@ impl Uiaa {
}
if !completed {
self.update_uiaa_session(user_id, device_id, Some(&uiaainfo))?;
self.update_uiaa_session(
user_id,
device_id,
uiaainfo.session.as_ref().expect("session is always set"),
Some(&uiaainfo),
)?;
return Ok((false, uiaainfo));
}
// UIAA was successful! Remove this session and return true
self.update_uiaa_session(user_id, device_id, None)?;
self.update_uiaa_session(
user_id,
device_id,
uiaainfo.session.as_ref().expect("session is always set"),
None,
)?;
Ok((true, uiaainfo))
} else {
panic!("FallbackAcknowledgement is not supported yet");
}
}
fn set_uiaa_request(
&self,
user_id: &UserId,
device_id: &DeviceId,
session: &str,
request: &CanonicalJsonValue,
) -> Result<()> {
let mut userdevicesessionid = user_id.as_bytes().to_vec();
userdevicesessionid.push(0xff);
userdevicesessionid.extend_from_slice(device_id.as_bytes());
userdevicesessionid.push(0xff);
userdevicesessionid.extend_from_slice(session.as_bytes());
self.userdevicesessionid_uiaarequest.insert(
&userdevicesessionid,
&*serde_json::to_string(request).expect("json value to string always works"),
)?;
Ok(())
}
pub fn get_uiaa_request(
&self,
user_id: &UserId,
device_id: &DeviceId,
session: &str,
) -> Result<Option<CanonicalJsonValue>> {
let mut userdevicesessionid = user_id.as_bytes().to_vec();
userdevicesessionid.push(0xff);
userdevicesessionid.extend_from_slice(device_id.as_bytes());
userdevicesessionid.push(0xff);
userdevicesessionid.extend_from_slice(session.as_bytes());
self.userdevicesessionid_uiaarequest
.get(&userdevicesessionid)?
.map_or(Ok(None), |bytes| {
Ok::<_, Error>(Some(
serde_json::from_str::<CanonicalJsonValue>(
&utils::string_from_bytes(&bytes).map_err(|_| {
Error::bad_database("Invalid uiaa request bytes in db.")
})?,
)
.map_err(|_| Error::bad_database("Invalid uiaa request in db."))?,
))
})
}
fn update_uiaa_session(
&self,
user_id: &UserId,
device_id: &DeviceId,
session: &str,
uiaainfo: Option<&UiaaInfo>,
) -> Result<()> {
let mut userdeviceid = user_id.as_bytes().to_vec();
userdeviceid.push(0xff);
userdeviceid.extend_from_slice(device_id.as_bytes());
let mut userdevicesessionid = user_id.as_bytes().to_vec();
userdevicesessionid.push(0xff);
userdevicesessionid.extend_from_slice(device_id.as_bytes());
userdevicesessionid.push(0xff);
userdevicesessionid.extend_from_slice(session.as_bytes());
if let Some(uiaainfo) = uiaainfo {
self.userdeviceid_uiaainfo.insert(
&userdeviceid,
self.userdevicesessionid_uiaainfo.insert(
&userdevicesessionid,
&*serde_json::to_string(&uiaainfo).expect("UiaaInfo::to_string always works"),
)?;
} else {
self.userdeviceid_uiaainfo.remove(&userdeviceid)?;
self.userdevicesessionid_uiaainfo
.remove(&userdevicesessionid)?;
}
Ok(())
@ -170,14 +249,16 @@ impl Uiaa {
device_id: &DeviceId,
session: &str,
) -> Result<UiaaInfo> {
let mut userdeviceid = user_id.as_bytes().to_vec();
userdeviceid.push(0xff);
userdeviceid.extend_from_slice(device_id.as_bytes());
let mut userdevicesessionid = user_id.as_bytes().to_vec();
userdevicesessionid.push(0xff);
userdevicesessionid.extend_from_slice(device_id.as_bytes());
userdevicesessionid.push(0xff);
userdevicesessionid.extend_from_slice(session.as_bytes());
let uiaainfo = serde_json::from_slice::<UiaaInfo>(
&self
.userdeviceid_uiaainfo
.get(&userdeviceid)?
.userdevicesessionid_uiaainfo
.get(&userdevicesessionid)?
.ok_or(Error::BadRequest(
ErrorKind::Forbidden,
"UIAA session does not exist.",
@ -185,18 +266,6 @@ impl Uiaa {
)
.map_err(|_| Error::bad_database("UiaaInfo in userdeviceid_uiaainfo is invalid."))?;
if uiaainfo
.session
.as_ref()
.filter(|&s| s == session)
.is_none()
{
return Err(Error::BadRequest(
ErrorKind::Forbidden,
"UIAA session token invalid.",
));
}
Ok(uiaainfo)
}
}

View file

@ -8,7 +8,7 @@ use std::ops::Deref;
#[cfg(feature = "conduit_bin")]
use {
crate::{server_server, utils},
crate::server_server,
log::{debug, warn},
rocket::{
data::{self, ByteUnit, Data, FromData},
@ -35,7 +35,7 @@ pub struct Ruma<T: Outgoing> {
pub sender_user: Option<UserId>,
pub sender_device: Option<Box<DeviceId>>,
// This is None when body is not a valid string
pub json_body: Option<Box<serde_json::value::RawValue>>,
pub json_body: Option<CanonicalJsonValue>,
pub from_appservice: bool,
}
@ -66,6 +66,8 @@ where
let mut body = Vec::new();
handle.read_to_end(&mut body).await.unwrap();
let mut json_body = serde_json::from_slice::<CanonicalJsonValue>(&body).ok();
let (sender_user, sender_device, from_appservice) = if let Some((_id, registration)) = db
.appservice
.iter_all()
@ -115,7 +117,7 @@ where
// Unknown Token
None => return Failure((Status::raw(581), ())),
Some((user_id, device_id)) => {
(Some(user_id), Some(device_id.into()), false)
(Some(user_id), Some(Box::<DeviceId>::from(device_id)), false)
}
}
} else {
@ -187,12 +189,10 @@ where
}
};
let json_body = serde_json::from_slice::<CanonicalJsonValue>(&body);
let mut request_map = BTreeMap::<String, CanonicalJsonValue>::new();
if let Ok(json_body) = json_body {
request_map.insert("content".to_owned(), json_body);
if let Some(json_body) = &json_body {
request_map.insert("content".to_owned(), json_body.clone());
};
request_map.insert(
@ -271,6 +271,43 @@ where
http_request = http_request.header(header.name.as_str(), &*header.value);
}
match &mut json_body {
Some(CanonicalJsonValue::Object(json_body)) => {
let user_id = sender_user.clone().unwrap_or_else(|| {
UserId::parse_with_server_name("", db.globals.server_name())
.expect("we know this is valid")
});
if let Some(initial_request) = json_body
.get("auth")
.and_then(|auth| auth.as_object())
.and_then(|auth| auth.get("session"))
.and_then(|session| session.as_str())
.and_then(|session| {
db.uiaa
.get_uiaa_request(
&user_id,
&sender_device.clone().unwrap_or_else(|| "".into()),
session,
)
.ok()
.flatten()
})
{
match initial_request {
CanonicalJsonValue::Object(initial_request) => {
for (key, value) in initial_request.into_iter() {
json_body.entry(key).or_insert(value);
}
}
_ => {}
}
}
body = serde_json::to_vec(json_body).expect("value to bytes can't fail");
}
_ => {}
}
let http_request = http_request.body(&*body).unwrap();
debug!("{:?}", http_request);
match <T::Incoming as IncomingRequest>::try_from_http_request(http_request) {
@ -278,11 +315,8 @@ where
body: t,
sender_user,
sender_device,
// TODO: Can we avoid parsing it again? (We only need this for append_pdu)
json_body: utils::string_from_bytes(&body)
.ok()
.and_then(|s| serde_json::value::RawValue::from_string(s).ok()),
from_appservice,
json_body,
}),
Err(e) => {
warn!("{:?}", e);

View file

@ -1018,29 +1018,6 @@ pub fn handle_incoming_pdu<'a>(
}
debug!("Auth check succeeded.");
// 13. Check if the event passes auth based on the "current state" of the room, if not "soft fail" it
let current_state = db
.rooms
.room_state_full(&room_id)
.map_err(|_| "Failed to load room state.".to_owned())?
.into_iter()
.map(|(k, v)| (k, Arc::new(v)))
.collect();
if !state_res::event_auth::auth_check(
&room_version,
&incoming_pdu,
previous_create,
&current_state,
None,
)
.map_err(|_e| "Auth check failed.".to_owned())?
{
// Soft fail, we leave the event as an outlier but don't add it to the timeline
return Err("Event has been soft failed".into());
};
debug!("Auth check with current state succeeded.");
// Now we calculate the set of extremities this room has after the incoming event has been
// applied. We start with the previous extremities (aka leaves)
let mut extremities = db
@ -1103,6 +1080,14 @@ pub fn handle_incoming_pdu<'a>(
// don't just trust a set of state we got from a remote).
// We do this by adding the current state to the list of fork states
let current_state = db
.rooms
.room_state_full(&room_id)
.map_err(|_| "Failed to load room state.".to_owned())?
.into_iter()
.map(|(k, v)| (k, Arc::new(v)))
.collect();
fork_states.insert(current_state);
// We also add state after incoming event to the fork states
@ -1199,18 +1184,40 @@ pub fn handle_incoming_pdu<'a>(
}
};
// Now that the event has passed all auth it is added into the timeline.
// We use the `state_at_event` instead of `state_after` so we accurately
// represent the state for this event.
let pdu_id = append_incoming_pdu(
&db,
// 13. Check if the event passes auth based on the "current state" of the room, if not "soft fail" it
let soft_fail = !state_res::event_auth::auth_check(
&room_version,
&incoming_pdu,
val,
extremities,
&state_at_incoming_event,
previous_create,
&new_room_state
.iter()
.filter_map(|(k, v)| {
Some((k.clone(), Arc::new(db.rooms.get_pdu(&v).ok().flatten()?)))
})
.collect(),
None,
)
.map_err(|_| "Failed to add pdu to db.".to_owned())?;
debug!("Appended incoming pdu.");
.map_err(|_e| "Auth check failed.".to_owned())?;
let mut pdu_id = None;
if !soft_fail {
// Now that the event has passed all auth it is added into the timeline.
// We use the `state_at_event` instead of `state_after` so we accurately
// represent the state for this event.
pdu_id = Some(
append_incoming_pdu(
&db,
&incoming_pdu,
val,
extremities,
&state_at_incoming_event,
)
.map_err(|_| "Failed to add pdu to db.".to_owned())?,
);
debug!("Appended incoming pdu.");
} else {
warn!("Event was soft failed: {:?}", incoming_pdu);
}
// Set the new room state to the resolved state
if update_state {
@ -1220,8 +1227,13 @@ pub fn handle_incoming_pdu<'a>(
}
debug!("Updated resolved state");
if soft_fail {
// Soft fail, we leave the event as an outlier but don't add it to the timeline
return Err("Event has been soft failed".into());
}
// Event has passed all auth/stateres checks
Ok(Some(pdu_id))
Ok(pdu_id)
})
}