feat: account deactivation (#137)

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 <theo@localhost>
Reviewed-on: https://git.koesters.xyz/timo/conduit/pulls/137
Reviewed-by: Timo Kösters <timo@koesters.xyz>
next
the0 2020-07-05 07:48:19 +02:00 committed by Timo Kösters
parent 67a1f21f5d
commit b81939841b
3 changed files with 132 additions and 22 deletions

View File

@ -12,7 +12,10 @@ use ruma::{
api::client::{ api::client::{
error::ErrorKind, error::ErrorKind,
r0::{ r0::{
account::{change_password, get_username_availability, register}, account::{
change_password, deactivate, get_username_availability, register,
ThirdPartyIdRemovalStatus,
},
alias::{create_alias, delete_alias, get_alias}, alias::{create_alias, delete_alias, get_alias},
backup::{ backup::{
add_backup_keys, create_backup, get_backup, get_backup_keys, get_latest_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(); let password = body.password.clone().unwrap_or_default();
if let Ok(hash) = utils::calculate_hash(&password) {
// Create user // Create user
db.users.create(&user_id, &hash)?; db.users.create(&user_id, &password)?;
} else {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Password does not meet the requirements.",
));
}
// Generate new device id if the user didn't specify one // Generate new device id if the user didn't specify one
let device_id = body 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 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."))?; 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 = let hash_matches =
argon2::verify_encoded(&hash, password.as_bytes()).unwrap_or(false); argon2::verify_encoded(&hash, password.as_bytes()).unwrap_or(false);
@ -312,6 +312,7 @@ pub fn change_password_route(
) -> ConduitResult<change_password::Response> { ) -> ConduitResult<change_password::Response> {
let user_id = body.user_id.as_ref().expect("user is authenticated"); 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 device_id = body.device_id.as_ref().expect("user is authenticated");
let mut uiaainfo = UiaaInfo { let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { flows: vec![AuthFlow {
stages: vec!["m.login.password".to_owned()], stages: vec!["m.login.password".to_owned()],
@ -334,6 +335,7 @@ pub fn change_password_route(
if !worked { if !worked {
return Err(Error::Uiaa(uiaainfo)); return Err(Error::Uiaa(uiaainfo));
} }
// Success!
} else { } else {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH)); uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
db.uiaa.create(&user_id, &device_id, &uiaainfo)?; db.uiaa.create(&user_id, &device_id, &uiaainfo)?;
@ -357,6 +359,79 @@ pub fn change_password_route(
Ok(change_password::Response.into()) Ok(change_password::Response.into())
} }
#[post("/_matrix/client/r0/account/deactivate", data = "<body>")]
pub fn deactivate_route(
db: State<'_, Database>,
body: Ruma<deactivate::Request>,
) -> ConduitResult<deactivate::Response> {
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")] #[get("/_matrix/client/r0/capabilities")]
pub fn get_capabilities_route() -> ConduitResult<get_capabilities::Response> { pub fn get_capabilities_route() -> ConduitResult<get_capabilities::Response> {
let mut available = BTreeMap::new(); let mut available = BTreeMap::new();
@ -1905,19 +1980,27 @@ pub fn search_users_route(
.filter_map(|user_id| { .filter_map(|user_id| {
// Filter out buggy users (they should not exist, but you never know...) // Filter out buggy users (they should not exist, but you never know...)
let user_id = user_id.ok()?; 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(), user_id: user_id.clone(),
display_name: db.users.displayname(&user_id).ok()?, display_name: db.users.displayname(&user_id).ok()?,
avatar_url: db.users.avatar_url(&user_id).ok()?, avatar_url: db.users.avatar_url(&user_id).ok()?,
}) };
})
.filter(|user| { if !user.user_id.to_string().contains(&body.search_term)
user.user_id.to_string().contains(&body.search_term) && user
|| user
.display_name .display_name
.as_ref() .as_ref()
.filter(|name| name.contains(&body.search_term)) .filter(|name| name.contains(&body.search_term))
.is_some() .is_none()
{
return None;
}
Some(user)
}) })
.collect(), .collect(),
limited: false, limited: false,

View File

@ -37,9 +37,21 @@ impl Users {
Ok(self.userid_password.contains_key(user_id.to_string())?) Ok(self.userid_password.contains_key(user_id.to_string())?)
} }
/// Check if account is deactivated
pub fn is_deactivated(&self, user_id: &UserId) -> Result<bool> {
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. /// Create a new user account on this homeserver.
pub fn create(&self, user_id: &UserId, hash: &str) -> Result<()> { pub fn create(&self, user_id: &UserId, password: &str) -> Result<()> {
self.userid_password.insert(user_id.to_string(), hash)?; self.set_password(user_id, password)?;
Ok(()) Ok(())
} }
@ -97,13 +109,13 @@ impl Users {
pub fn set_password(&self, user_id: &UserId, password: &str) -> Result<()> { pub fn set_password(&self, user_id: &UserId, password: &str) -> Result<()> {
if let Ok(hash) = utils::calculate_hash(&password) { if let Ok(hash) = utils::calculate_hash(&password) {
self.userid_password.insert(user_id.to_string(), &*hash)?; self.userid_password.insert(user_id.to_string(), &*hash)?;
Ok(())
} else { } else {
return Err(Error::BadRequest( Err(Error::BadRequest(
ErrorKind::InvalidParam, ErrorKind::InvalidParam,
"Password does not meet the requirements.", "Password does not meet the requirements.",
)); ))
} }
Ok(())
} }
/// Returns the displayname of a user on this homeserver. /// 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(())
}
} }

View File

@ -30,6 +30,7 @@ fn setup_rocket() -> rocket::Rocket {
client_server::login_route, client_server::login_route,
client_server::logout_route, client_server::logout_route,
client_server::change_password_route, client_server::change_password_route,
client_server::deactivate_route,
client_server::get_capabilities_route, client_server::get_capabilities_route,
client_server::get_pushrules_all_route, client_server::get_pushrules_all_route,
client_server::set_pushrule_route, client_server::set_pushrule_route,