mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a83c1f1513 | |||
| 8b5e4d8fe1 | |||
| 7502a944d7 |
@@ -0,0 +1 @@
|
|||||||
|
Implemented account locking functionality, to complement user suspension. Contributed by @nex.
|
||||||
@@ -238,6 +238,7 @@ pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) ->
|
|||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
pub(super) async fn suspend(&self, user_id: String) -> Result {
|
pub(super) async fn suspend(&self, user_id: String) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||||
|
|
||||||
if user_id == self.services.globals.server_user {
|
if user_id == self.services.globals.server_user {
|
||||||
@@ -262,6 +263,7 @@ pub(super) async fn suspend(&self, user_id: String) -> Result {
|
|||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
pub(super) async fn unsuspend(&self, user_id: String) -> Result {
|
pub(super) async fn unsuspend(&self, user_id: String) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||||
|
|
||||||
if user_id == self.services.globals.server_user {
|
if user_id == self.services.globals.server_user {
|
||||||
@@ -974,3 +976,44 @@ pub(super) async fn force_leave_remote_room(
|
|||||||
self.write_str(&format!("{user_id} successfully left {room_id} via remote server."))
|
self.write_str(&format!("{user_id} successfully left {room_id} via remote server."))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
pub(super) async fn lock(&self, user_id: String) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||||
|
assert!(
|
||||||
|
self.services.globals.user_is_local(&user_id),
|
||||||
|
"Parsed user_id must be a local user"
|
||||||
|
);
|
||||||
|
if user_id == self.services.globals.server_user {
|
||||||
|
return Err!("Not allowed to lock the server service account.",);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.services.users.exists(&user_id).await {
|
||||||
|
return Err!("User {user_id} does not exist.");
|
||||||
|
}
|
||||||
|
if self.services.users.is_admin(&user_id).await {
|
||||||
|
return Err!("Admin users cannot be locked.");
|
||||||
|
}
|
||||||
|
self.services
|
||||||
|
.users
|
||||||
|
.lock_account(&user_id, self.sender_or_service_user())
|
||||||
|
.await;
|
||||||
|
|
||||||
|
self.write_str(&format!("User {user_id} has been locked."))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
pub(super) async fn unlock(&self, user_id: String) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||||
|
assert!(
|
||||||
|
self.services.globals.user_is_local(&user_id),
|
||||||
|
"Parsed user_id must be a local user"
|
||||||
|
);
|
||||||
|
self.services.users.unlock_account(&user_id).await;
|
||||||
|
|
||||||
|
self.write_str(&format!("User {user_id} has been unlocked."))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|||||||
@@ -81,6 +81,26 @@ pub enum UserCommand {
|
|||||||
user_id: String,
|
user_id: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// - Lock a user
|
||||||
|
///
|
||||||
|
/// Locked users are unable to use their accounts beyond logging out. This
|
||||||
|
/// is akin to a temporary deactivation that does not change the user's
|
||||||
|
/// password. This can be used to quickly prevent a user from accessing
|
||||||
|
/// their account.
|
||||||
|
Lock {
|
||||||
|
/// Username of the user to lock
|
||||||
|
user_id: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// - Unlock a user
|
||||||
|
///
|
||||||
|
/// Reverses the effects of the `lock` command, allowing the user to use
|
||||||
|
/// their account again.
|
||||||
|
Unlock {
|
||||||
|
/// Username of the user to unlock
|
||||||
|
user_id: String,
|
||||||
|
},
|
||||||
|
|
||||||
/// - List local users in the database
|
/// - List local users in the database
|
||||||
#[clap(alias = "list")]
|
#[clap(alias = "list")]
|
||||||
ListUsers,
|
ListUsers,
|
||||||
|
|||||||
+24
-6
@@ -137,12 +137,30 @@ pub(super) async fn auth(
|
|||||||
| (
|
| (
|
||||||
AuthScheme::AccessToken | AuthScheme::AccessTokenOptional | AuthScheme::None,
|
AuthScheme::AccessToken | AuthScheme::AccessTokenOptional | AuthScheme::None,
|
||||||
Token::User((user_id, device_id)),
|
Token::User((user_id, device_id)),
|
||||||
) => Ok(Auth {
|
) => {
|
||||||
origin: None,
|
let is_locked = services.users.is_locked(&user_id).await.map_err(|e| {
|
||||||
sender_user: Some(user_id),
|
err!(Request(Forbidden(warn!("Failed to check user lock status: {e}"))))
|
||||||
sender_device: Some(device_id),
|
})?;
|
||||||
appservice_info: None,
|
if is_locked {
|
||||||
}),
|
// Only /logout and /logout/all are allowed for locked users
|
||||||
|
if !matches!(
|
||||||
|
metadata,
|
||||||
|
&ruma::api::client::session::logout::v3::Request::METADATA
|
||||||
|
| &ruma::api::client::session::logout_all::v3::Request::METADATA
|
||||||
|
) {
|
||||||
|
return Err(Error::BadRequest(
|
||||||
|
ErrorKind::UserLocked,
|
||||||
|
"This account has been locked.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Auth {
|
||||||
|
origin: None,
|
||||||
|
sender_user: Some(user_id),
|
||||||
|
sender_device: Some(device_id),
|
||||||
|
appservice_info: None,
|
||||||
|
})
|
||||||
|
},
|
||||||
| (AuthScheme::ServerSignatures, Token::None) =>
|
| (AuthScheme::ServerSignatures, Token::None) =>
|
||||||
Ok(auth_server(services, request, json_body).await?),
|
Ok(auth_server(services, request, json_body).await?),
|
||||||
| (
|
| (
|
||||||
|
|||||||
@@ -75,10 +75,12 @@ pub(super) fn bad_request_code(kind: &ErrorKind) -> StatusCode {
|
|||||||
| ThreepidDenied
|
| ThreepidDenied
|
||||||
| InviteBlocked
|
| InviteBlocked
|
||||||
| WrongRoomKeysVersion { .. }
|
| WrongRoomKeysVersion { .. }
|
||||||
|
| UserSuspended
|
||||||
| Forbidden { .. } => StatusCode::FORBIDDEN,
|
| Forbidden { .. } => StatusCode::FORBIDDEN,
|
||||||
|
|
||||||
// 401
|
// 401
|
||||||
| UnknownToken { .. } | MissingToken | Unauthorized => StatusCode::UNAUTHORIZED,
|
| UnknownToken { .. } | MissingToken | Unauthorized | UserLocked =>
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
|
||||||
// 400
|
// 400
|
||||||
| _ => StatusCode::BAD_REQUEST,
|
| _ => StatusCode::BAD_REQUEST,
|
||||||
|
|||||||
@@ -386,6 +386,10 @@ pub(super) static MAPS: &[Descriptor] = &[
|
|||||||
name: "userid_suspension",
|
name: "userid_suspension",
|
||||||
..descriptor::RANDOM_SMALL
|
..descriptor::RANDOM_SMALL
|
||||||
},
|
},
|
||||||
|
Descriptor {
|
||||||
|
name: "userid_lock",
|
||||||
|
..descriptor::RANDOM_SMALL
|
||||||
|
},
|
||||||
Descriptor {
|
Descriptor {
|
||||||
name: "userid_presenceid",
|
name: "userid_presenceid",
|
||||||
..descriptor::RANDOM_SMALL
|
..descriptor::RANDOM_SMALL
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ struct Data {
|
|||||||
userid_origin: Arc<Map>,
|
userid_origin: Arc<Map>,
|
||||||
userid_password: Arc<Map>,
|
userid_password: Arc<Map>,
|
||||||
userid_suspension: Arc<Map>,
|
userid_suspension: Arc<Map>,
|
||||||
|
userid_lock: Arc<Map>,
|
||||||
userid_selfsigningkeyid: Arc<Map>,
|
userid_selfsigningkeyid: Arc<Map>,
|
||||||
userid_usersigningkeyid: Arc<Map>,
|
userid_usersigningkeyid: Arc<Map>,
|
||||||
useridprofilekey_value: Arc<Map>,
|
useridprofilekey_value: Arc<Map>,
|
||||||
@@ -115,6 +116,7 @@ impl crate::Service for Service {
|
|||||||
userid_origin: args.db["userid_origin"].clone(),
|
userid_origin: args.db["userid_origin"].clone(),
|
||||||
userid_password: args.db["userid_password"].clone(),
|
userid_password: args.db["userid_password"].clone(),
|
||||||
userid_suspension: args.db["userid_suspension"].clone(),
|
userid_suspension: args.db["userid_suspension"].clone(),
|
||||||
|
userid_lock: args.db["userid_lock"].clone(),
|
||||||
userid_selfsigningkeyid: args.db["userid_selfsigningkeyid"].clone(),
|
userid_selfsigningkeyid: args.db["userid_selfsigningkeyid"].clone(),
|
||||||
userid_usersigningkeyid: args.db["userid_usersigningkeyid"].clone(),
|
userid_usersigningkeyid: args.db["userid_usersigningkeyid"].clone(),
|
||||||
useridprofilekey_value: args.db["useridprofilekey_value"].clone(),
|
useridprofilekey_value: args.db["useridprofilekey_value"].clone(),
|
||||||
@@ -220,6 +222,26 @@ impl Service {
|
|||||||
self.db.userid_suspension.remove(user_id);
|
self.db.userid_suspension.remove(user_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn lock_account(&self, user_id: &UserId, locking_user: &UserId) {
|
||||||
|
// NOTE: Locking is basically just suspension with a more severe effect,
|
||||||
|
// so we'll just re-use the suspension data structure to store the lock state.
|
||||||
|
let suspension = self
|
||||||
|
.db
|
||||||
|
.userid_lock
|
||||||
|
.get(user_id)
|
||||||
|
.await
|
||||||
|
.deserialized::<UserSuspension>()
|
||||||
|
.unwrap_or_else(|_| UserSuspension {
|
||||||
|
suspended: true,
|
||||||
|
suspended_at: MilliSecondsSinceUnixEpoch::now().get().into(),
|
||||||
|
suspended_by: locking_user.to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
self.db.userid_lock.raw_put(user_id, Json(suspension));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn unlock_account(&self, user_id: &UserId) { self.db.userid_lock.remove(user_id); }
|
||||||
|
|
||||||
/// Check if a user has an account on this homeserver.
|
/// Check if a user has an account on this homeserver.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub async fn exists(&self, user_id: &UserId) -> bool {
|
pub async fn exists(&self, user_id: &UserId) -> bool {
|
||||||
@@ -255,6 +277,24 @@ impl Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn is_locked(&self, user_id: &UserId) -> Result<bool> {
|
||||||
|
match self
|
||||||
|
.db
|
||||||
|
.userid_lock
|
||||||
|
.get(user_id)
|
||||||
|
.await
|
||||||
|
.deserialized::<UserSuspension>()
|
||||||
|
{
|
||||||
|
| Ok(s) => Ok(s.suspended),
|
||||||
|
| Err(e) =>
|
||||||
|
if e.is_not_found() {
|
||||||
|
Ok(false)
|
||||||
|
} else {
|
||||||
|
Err(e)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if account is active, infallible
|
/// Check if account is active, infallible
|
||||||
pub async fn is_active(&self, user_id: &UserId) -> bool {
|
pub async fn is_active(&self, user_id: &UserId) -> bool {
|
||||||
!self.is_deactivated(user_id).await.unwrap_or(true)
|
!self.is_deactivated(user_id).await.unwrap_or(true)
|
||||||
|
|||||||
Reference in New Issue
Block a user