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
parent
67a1f21f5d
commit
b81939841b
|
@ -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, &password)?;
|
||||||
db.users.create(&user_id, &hash)?;
|
|
||||||
} 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,
|
||||||
|
|
|
@ -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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue