From b81939841b272206991041ce7643a1c572590db3 Mon Sep 17 00:00:00 2001 From: the0 Date: Sun, 5 Jul 2020 07:48:19 +0200 Subject: [PATCH] feat: account deactivation (#137) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deactivation: swap unwrap_or(false) to .ok()? feat: implement deactivate account route Implement error code on login to deactivated account Deactivation: Changes requested Add missing .clone() Deactivation: Requested changes Remove unneeded .filter() Deactivation: badly named signature leads to confusion Co-authored-by: the0 Reviewed-on: https://git.koesters.xyz/timo/conduit/pulls/137 Reviewed-by: Timo Kösters --- src/client_server.rs | 117 ++++++++++++++++++++++++++++++++++++------ src/database/users.rs | 36 +++++++++++-- src/main.rs | 1 + 3 files changed, 132 insertions(+), 22 deletions(-) diff --git a/src/client_server.rs b/src/client_server.rs index 65b2c869..cde8bf57 100644 --- a/src/client_server.rs +++ b/src/client_server.rs @@ -12,7 +12,10 @@ use ruma::{ api::client::{ error::ErrorKind, r0::{ - account::{change_password, get_username_availability, register}, + account::{ + change_password, deactivate, get_username_availability, register, + ThirdPartyIdRemovalStatus, + }, alias::{create_alias, delete_alias, get_alias}, backup::{ add_backup_keys, create_backup, get_backup, get_backup_keys, get_latest_backup, @@ -179,15 +182,8 @@ pub fn register_route( let password = body.password.clone().unwrap_or_default(); - if let Ok(hash) = utils::calculate_hash(&password) { - // Create user - db.users.create(&user_id, &hash)?; - } else { - return Err(Error::BadRequest( - ErrorKind::InvalidParam, - "Password does not meet the requirements.", - )); - } + // Create user + db.users.create(&user_id, &password)?; // Generate new device id if the user didn't specify one let device_id = body @@ -252,6 +248,10 @@ pub fn login_route( let user_id = UserId::parse_with_server_name(username, db.globals.server_name()).map_err(|_| Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid."))?; let hash = db.users.password_hash(&user_id)?.ok_or(Error::BadRequest(ErrorKind::Forbidden, "Wrong username or password."))?; + if hash.is_empty() { + return Err(Error::BadRequest(ErrorKind::UserDeactivated, "The user has been deactivated")); + } + let hash_matches = argon2::verify_encoded(&hash, password.as_bytes()).unwrap_or(false); @@ -312,6 +312,7 @@ pub fn change_password_route( ) -> ConduitResult { let user_id = body.user_id.as_ref().expect("user is authenticated"); let device_id = body.device_id.as_ref().expect("user is authenticated"); + let mut uiaainfo = UiaaInfo { flows: vec![AuthFlow { stages: vec!["m.login.password".to_owned()], @@ -334,6 +335,7 @@ pub fn change_password_route( if !worked { return Err(Error::Uiaa(uiaainfo)); } + // Success! } else { uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH)); db.uiaa.create(&user_id, &device_id, &uiaainfo)?; @@ -357,6 +359,79 @@ pub fn change_password_route( Ok(change_password::Response.into()) } +#[post("/_matrix/client/r0/account/deactivate", data = "")] +pub fn deactivate_route( + db: State<'_, Database>, + body: Ruma, +) -> ConduitResult { + let user_id = body.user_id.as_ref().expect("user is authenticated"); + let device_id = body.device_id.as_ref().expect("user is authenticated"); + + let mut uiaainfo = UiaaInfo { + flows: vec![AuthFlow { + stages: vec!["m.login.password".to_owned()], + }], + completed: Vec::new(), + params: Default::default(), + session: None, + auth_error: None, + }; + + if let Some(auth) = &body.auth { + let (worked, uiaainfo) = db.uiaa.try_auth( + &user_id, + &device_id, + 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, &device_id, &uiaainfo)?; + return Err(Error::Uiaa(uiaainfo)); + } + + // Leave all joined rooms and reject all invitations + for room_id in db + .rooms + .rooms_joined(&user_id) + .chain(db.rooms.rooms_invited(&user_id)) + { + let room_id = room_id?; + let event = member::MemberEventContent { + membership: member::MembershipState::Leave, + displayname: None, + avatar_url: None, + is_direct: None, + third_party_invite: None, + }; + + db.rooms.append_pdu( + room_id.clone(), + user_id.clone(), + EventType::RoomMember, + serde_json::to_value(event).expect("event is valid, we just created it"), + None, + Some(user_id.to_string()), + None, + &db.globals, + )?; + } + + // Remove devices and mark account as deactivated + db.users.deactivate_account(&user_id)?; + + Ok(deactivate::Response { + id_server_unbind_result: ThirdPartyIdRemovalStatus::NoSupport, + } + .into()) +} + #[get("/_matrix/client/r0/capabilities")] pub fn get_capabilities_route() -> ConduitResult { let mut available = BTreeMap::new(); @@ -1905,19 +1980,27 @@ pub fn search_users_route( .filter_map(|user_id| { // Filter out buggy users (they should not exist, but you never know...) let user_id = user_id.ok()?; - Some(search_users::User { + if db.users.is_deactivated(&user_id).ok()? { + return None; + } + + let user = search_users::User { user_id: user_id.clone(), display_name: db.users.displayname(&user_id).ok()?, avatar_url: db.users.avatar_url(&user_id).ok()?, - }) - }) - .filter(|user| { - user.user_id.to_string().contains(&body.search_term) - || user + }; + + if !user.user_id.to_string().contains(&body.search_term) + && user .display_name .as_ref() .filter(|name| name.contains(&body.search_term)) - .is_some() + .is_none() + { + return None; + } + + Some(user) }) .collect(), limited: false, diff --git a/src/database/users.rs b/src/database/users.rs index e05cf2e6..2ccf59ac 100644 --- a/src/database/users.rs +++ b/src/database/users.rs @@ -37,9 +37,21 @@ impl Users { Ok(self.userid_password.contains_key(user_id.to_string())?) } + /// Check if account is deactivated + pub fn is_deactivated(&self, user_id: &UserId) -> Result { + Ok(self + .userid_password + .get(user_id.to_string())? + .ok_or(Error::BadRequest( + ErrorKind::InvalidParam, + "User does not exist.", + ))? + .is_empty()) + } + /// Create a new user account on this homeserver. - pub fn create(&self, user_id: &UserId, hash: &str) -> Result<()> { - self.userid_password.insert(user_id.to_string(), hash)?; + pub fn create(&self, user_id: &UserId, password: &str) -> Result<()> { + self.set_password(user_id, password)?; Ok(()) } @@ -97,13 +109,13 @@ impl Users { pub fn set_password(&self, user_id: &UserId, password: &str) -> Result<()> { if let Ok(hash) = utils::calculate_hash(&password) { self.userid_password.insert(user_id.to_string(), &*hash)?; + Ok(()) } else { - return Err(Error::BadRequest( + Err(Error::BadRequest( ErrorKind::InvalidParam, "Password does not meet the requirements.", - )); + )) } - Ok(()) } /// Returns the displayname of a user on this homeserver. @@ -721,4 +733,18 @@ impl Users { })?) }) } + + /// Deactivate account + pub fn deactivate_account(&self, user_id: &UserId) -> Result<()> { + // Remove all associated devices + for device_id in self.all_device_ids(user_id) { + self.remove_device(&user_id, &device_id?)?; + } + + // Set the password to "" to indicate a deactivated account + self.userid_password.insert(user_id.to_string(), "")?; + + // TODO: Unhook 3PID + Ok(()) + } } diff --git a/src/main.rs b/src/main.rs index 11475723..f94df501 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,6 +30,7 @@ fn setup_rocket() -> rocket::Rocket { client_server::login_route, client_server::logout_route, client_server::change_password_route, + client_server::deactivate_route, client_server::get_capabilities_route, client_server::get_pushrules_all_route, client_server::set_pushrule_route,