mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-11-14 04:37:00 +00:00
741 lines
21 KiB
Rust
741 lines
21 KiB
Rust
use std::{
|
|
net::IpAddr,
|
|
time::{Duration, SystemTime},
|
|
};
|
|
|
|
use conduwuit::{
|
|
Err, Result, debug_error, debug_warn, err, error, info, trace,
|
|
utils::{self, stream::TryIgnore},
|
|
warn,
|
|
};
|
|
use database::{Deserialized, Json};
|
|
use futures::{FutureExt, Stream, StreamExt};
|
|
use lettre::Address;
|
|
use ruma::{
|
|
MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId, UserId,
|
|
api::client::profile::PropagateTo,
|
|
events::{
|
|
GlobalAccountDataEventType, ignored_user_list::IgnoredUserListEvent,
|
|
push_rules::PushRulesEvent, room::message::RoomMessageEventContent,
|
|
},
|
|
profile::ProfileFieldValue,
|
|
push::Ruleset,
|
|
};
|
|
use ruminuwuity::invite_permission_config::{FilterLevel, InvitePermissionConfigEvent};
|
|
|
|
use crate::{
|
|
appservice::RegistrationInfo,
|
|
users::{HashedPassword, UserSuspension, profile::ProfileFieldChange},
|
|
};
|
|
|
|
/// The status of an access token.
|
|
pub enum AccessTokenStatus {
|
|
Valid,
|
|
Expired,
|
|
}
|
|
|
|
/// The status of a user account.
|
|
#[derive(Clone, Copy)]
|
|
pub enum AccountStatus {
|
|
NotFound,
|
|
Active,
|
|
Deactivated,
|
|
}
|
|
|
|
impl AccountStatus {
|
|
#[must_use]
|
|
pub fn is_found(&self) -> bool { !matches!(self, Self::NotFound) }
|
|
|
|
#[must_use]
|
|
pub fn is_active(&self) -> bool { matches!(self, Self::Active) }
|
|
|
|
pub fn ensure_active(&self) -> Result<()> {
|
|
match self {
|
|
| Self::Active => Ok(()),
|
|
| Self::Deactivated => Err!(Request(UserDeactivated("This account is deactivated."))),
|
|
| Self::NotFound => Err!(Request(NotFound("This account does not exist."))),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl super::Service {
|
|
/// Returns true/false based on whether the recipient/receiving user has
|
|
/// ignored the sender.
|
|
pub async fn user_is_ignored(&self, sender_user: &UserId, recipient_user: &UserId) -> bool {
|
|
self.services
|
|
.account_data
|
|
.get_global(recipient_user, GlobalAccountDataEventType::IgnoredUserList)
|
|
.await
|
|
.is_ok_and(|ignored: IgnoredUserListEvent| {
|
|
ignored
|
|
.content
|
|
.ignored_users
|
|
.keys()
|
|
.any(|blocked_user| blocked_user == sender_user)
|
|
})
|
|
}
|
|
|
|
/// Returns the recipient's filter level for an invite from the sender.
|
|
///
|
|
/// If the sender is ignored by the recipient, `Ignore` is returned.
|
|
/// Otherwise, the resulting value depends on their invite blocking or
|
|
/// invite filtering configuration.
|
|
pub async fn invite_filter_level(
|
|
&self,
|
|
sender_user: &UserId,
|
|
recipient_user: &UserId,
|
|
) -> FilterLevel {
|
|
if self.user_is_ignored(sender_user, recipient_user).await {
|
|
FilterLevel::Ignore
|
|
} else {
|
|
let (stable, unstable) = tokio::join!(
|
|
self.services
|
|
.account_data
|
|
.get_global::<InvitePermissionConfigEvent>(
|
|
recipient_user,
|
|
GlobalAccountDataEventType::InvitePermissionConfig
|
|
),
|
|
self.services
|
|
.account_data
|
|
.get_global::<InvitePermissionConfigEvent>(
|
|
recipient_user,
|
|
"org.matrix.msc4155.invite_permission_config".into()
|
|
) // TODO: MSC4155 probably needs upstreaming to ruma at some point
|
|
);
|
|
if stable.is_err() && unstable.is_err() {
|
|
return FilterLevel::Allow;
|
|
}
|
|
stable
|
|
.unwrap_or_else(|_| unstable.unwrap())
|
|
.content
|
|
.user_filter_level(sender_user)
|
|
}
|
|
}
|
|
|
|
/// Check if a user is an admin
|
|
#[inline]
|
|
pub async fn is_admin(&self, user_id: &UserId) -> bool {
|
|
self.services.admin.user_is_admin(user_id).await
|
|
}
|
|
|
|
/// Create a new shadow user account on this server. Shadow accounts
|
|
/// have no password and cannot be logged into.
|
|
pub async fn create_shadow_account(&self, user_id: &UserId) -> Result<()> {
|
|
assert!(self.services.globals.user_is_local(user_id), "user id must be local");
|
|
|
|
if self.status(user_id).await.is_found() {
|
|
return Err!(Request(UserInUse("An account with this user ID already exists.")));
|
|
}
|
|
|
|
self.db.userid_password.insert(user_id, "");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Create a new account for a local human or bot user. If `password` is
|
|
/// None, the account will be a shadow account.
|
|
pub async fn create_local_account(
|
|
&self,
|
|
user_id: &UserId,
|
|
password: Option<HashedPassword>,
|
|
email: Option<Address>,
|
|
client: Option<&IpAddr>,
|
|
device_name: Option<&str>,
|
|
) -> Result<()> {
|
|
self.create_shadow_account(user_id).await?;
|
|
|
|
if let Some(password) = password {
|
|
self.convert_to_local_account(user_id, password).await?;
|
|
}
|
|
|
|
if let Some(client) = client {
|
|
let notice = if let Some(device_name) = device_name {
|
|
format!(
|
|
"New user \"{user_id}\" registered on this server from IP {client} and \
|
|
device display name \"{device_name}\".",
|
|
)
|
|
} else {
|
|
format!("New user \"{user_id}\" registered on this server from IP {client}.")
|
|
};
|
|
info!("{notice}");
|
|
|
|
if self.services.config.admin_room_notices {
|
|
self.services.admin.notice(¬ice).await;
|
|
}
|
|
}
|
|
|
|
// Set an initial display name
|
|
{
|
|
let mut displayname = user_id.localpart().to_owned();
|
|
|
|
let suffix = &self.services.config.new_user_displayname_suffix;
|
|
if !suffix.is_empty() {
|
|
displayname.push(' ');
|
|
displayname.push_str(suffix);
|
|
}
|
|
|
|
self.set_profile_field(
|
|
user_id,
|
|
ProfileFieldChange::Set(ProfileFieldValue::DisplayName(displayname)),
|
|
PropagateTo::None,
|
|
)
|
|
.await
|
|
.expect("should be able to set display name");
|
|
};
|
|
|
|
// Set default push rules
|
|
self.services
|
|
.account_data
|
|
.update(
|
|
None,
|
|
user_id,
|
|
GlobalAccountDataEventType::PushRules.to_string().into(),
|
|
&serde_json::to_value(PushRulesEvent::new(
|
|
Ruleset::server_default(user_id).into(),
|
|
))
|
|
.expect("should be able to serialize push rules"),
|
|
)
|
|
.await
|
|
.expect("should be able to update account data");
|
|
|
|
// If the user registered with an email, associate it with their
|
|
// account.
|
|
if let Some(email) = email {
|
|
// This may fail if the email is already in use, but we should have
|
|
// already checked that when we sent the validation email, so
|
|
// ignoring the error is acceptable here in the rare case that an
|
|
// email is sniped by another user between the validation email
|
|
// being sent and the account being created.
|
|
let _ = self
|
|
.services
|
|
.threepid
|
|
.associate_localpart_email(user_id.localpart(), &email)
|
|
.await;
|
|
}
|
|
|
|
// Attempt to empower the first user and disable first-run mode.
|
|
let was_first_user = self.services.firstrun.empower_first_user(user_id).await;
|
|
|
|
// If the registering user was not the first and we're suspending users
|
|
// on register, suspend them.
|
|
if !was_first_user && self.services.config.suspend_on_register {
|
|
// Note that we can still do auto joins for suspended users
|
|
self.suspend_account(user_id, &self.services.globals.server_user)
|
|
.await;
|
|
|
|
// And send an @room notice to the admin room, to prompt admins to
|
|
// review the new user and ideally unsuspend them if deemed
|
|
// appropriate.
|
|
if self.services.config.admin_room_notices {
|
|
self.services
|
|
.admin
|
|
.send_loud_message(RoomMessageEventContent::text_plain(format!(
|
|
"User {user_id} has been suspended as they are not the first user on \
|
|
this server. Please review and unsuspend them if appropriate."
|
|
)))
|
|
.await
|
|
.ok();
|
|
}
|
|
}
|
|
|
|
// Autojoin the user to the configured autojoin rooms
|
|
for room in &self.services.config.auto_join_rooms {
|
|
let Ok(room_id) = self.services.alias.resolve(room).await else {
|
|
error!(
|
|
"Failed to resolve room alias to room ID when attempting to auto join \
|
|
{room}, skipping"
|
|
);
|
|
continue;
|
|
};
|
|
|
|
if !self
|
|
.services
|
|
.state_cache
|
|
.server_in_room(self.services.globals.server_name(), &room_id)
|
|
.await
|
|
{
|
|
warn!(
|
|
"Skipping room {room} to automatically join as we have never joined before."
|
|
);
|
|
continue;
|
|
}
|
|
|
|
if let Some(room_server_name) = room.server_name() {
|
|
match self
|
|
.services
|
|
.membership
|
|
.join_room(
|
|
user_id,
|
|
&room_id,
|
|
Some("Automatically joining this room upon registration".to_owned()),
|
|
&[
|
|
self.services.globals.server_name().to_owned(),
|
|
room_server_name.to_owned(),
|
|
],
|
|
)
|
|
.boxed()
|
|
.await
|
|
{
|
|
| Err(e) => {
|
|
// don't return this error so we don't fail
|
|
// registrations
|
|
error!(
|
|
"Failed to automatically join room {room} for user {user_id}: {e}"
|
|
);
|
|
},
|
|
| _ => {
|
|
info!("Automatically joined room {room} for user {user_id}");
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
info!("Created new user account for {user_id}");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn determine_registration_user_id(
|
|
&self,
|
|
supplied_username: Option<String>,
|
|
email: Option<&Address>,
|
|
appservice_info: Option<&RegistrationInfo>,
|
|
) -> Result<OwnedUserId> {
|
|
const RANDOM_USER_ID_LENGTH: usize = 10;
|
|
|
|
let emergency_mode_enabled = self.services.config.emergency_password.is_some();
|
|
|
|
let supplied_username = supplied_username.or_else(|| {
|
|
// If the user didn't supply a username but did supply an email, use
|
|
// the email's user part to avoid falling back to a random username
|
|
email.map(|address| address.user().to_owned())
|
|
});
|
|
|
|
if let Some(supplied_username) = supplied_username {
|
|
// The user gets to pick their username. Do some validation to make
|
|
// sure it's acceptable.
|
|
|
|
// Don't allow registration with forbidden usernames.
|
|
if self
|
|
.services
|
|
.globals
|
|
.forbidden_usernames()
|
|
.is_match(&supplied_username)
|
|
&& !emergency_mode_enabled
|
|
{
|
|
return Err!(Request(Forbidden("Username is forbidden")));
|
|
}
|
|
|
|
// Create and validate the user ID
|
|
let user_id = match UserId::parse_with_server_name(
|
|
&supplied_username,
|
|
self.services.globals.server_name(),
|
|
) {
|
|
| Ok(user_id) => user_id,
|
|
| Err(e) => {
|
|
return Err!(Request(InvalidUsername(debug_warn!(
|
|
"Username {supplied_username} is not valid: {e}"
|
|
))));
|
|
},
|
|
};
|
|
|
|
if let Err(e) = user_id.validate_strict() {
|
|
// Unless we are in emergency mode, we should follow synapse's
|
|
// behaviour on not allowing things like spaces and UTF-8
|
|
// characters in usernames
|
|
if !emergency_mode_enabled {
|
|
return Err!(Request(InvalidUsername(debug_warn!(
|
|
"Username {supplied_username} contains disallowed characters or spaces: \
|
|
{e}"
|
|
))));
|
|
}
|
|
}
|
|
|
|
// Don't allow registration with user IDs that aren't local
|
|
if !self.services.globals.user_is_local(&user_id) {
|
|
return Err!(Request(InvalidUsername(
|
|
"Username {supplied_username} is not local to this server"
|
|
)));
|
|
}
|
|
|
|
if self.status(&user_id).await.is_found() {
|
|
return Err!(Request(UserInUse("Username is not available.")));
|
|
}
|
|
|
|
// Check that the user ID is/is not in an appservice's namespace
|
|
if let Some(appservice_info) = appservice_info {
|
|
if !appservice_info.is_user_match(&user_id) && !emergency_mode_enabled {
|
|
return Err!(Request(Exclusive(
|
|
"Username is not in this appservice's namespace."
|
|
)));
|
|
}
|
|
} else if self
|
|
.services
|
|
.appservice
|
|
.is_exclusive_user_id(&user_id)
|
|
.await
|
|
&& !emergency_mode_enabled
|
|
{
|
|
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
|
|
}
|
|
|
|
Ok(user_id)
|
|
} else {
|
|
// The user didn't specify a username. Generate a username for
|
|
// them.
|
|
|
|
loop {
|
|
let user_id = UserId::parse_with_server_name(
|
|
utils::random_string(RANDOM_USER_ID_LENGTH).to_lowercase(),
|
|
self.services.globals.server_name(),
|
|
)
|
|
.unwrap();
|
|
|
|
if !self.status(&user_id).await.is_found() {
|
|
break Ok(user_id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Deactivates an account, removing all of their device IDs.
|
|
pub async fn deactivate_account(&self, user_id: &UserId) -> Result<()> {
|
|
self.status(user_id).await.ensure_active()?;
|
|
|
|
// Remove all associated devices
|
|
self.all_device_ids(user_id)
|
|
.for_each(async |device_id| self.remove_device(user_id, &device_id).await)
|
|
.await;
|
|
|
|
// Mark user as deactivated
|
|
self.db.userid_deactivated.insert(user_id, "");
|
|
|
|
// TODO: Unhook 3PID
|
|
Ok(())
|
|
}
|
|
|
|
/// Suspend account, placing it in a read-only state
|
|
pub async fn suspend_account(&self, user_id: &UserId, suspending_user: &UserId) {
|
|
self.db.userid_suspension.raw_put(
|
|
user_id,
|
|
Json(UserSuspension {
|
|
suspended: true,
|
|
suspended_at: MilliSecondsSinceUnixEpoch::now().get().into(),
|
|
suspended_by: suspending_user.to_string(),
|
|
}),
|
|
);
|
|
}
|
|
|
|
/// Unsuspend account, placing it in a read-write state
|
|
pub async fn unsuspend_account(&self, user_id: &UserId) {
|
|
self.db.userid_suspension.remove(user_id);
|
|
}
|
|
|
|
/// Locks an account, preventing it being used until it is unlocked.
|
|
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));
|
|
}
|
|
|
|
/// Unlocks an account, allowing the user to log in and use it again.
|
|
pub async fn unlock_account(&self, user_id: &UserId) { self.db.userid_lock.remove(user_id); }
|
|
|
|
/// Check if account is suspended. Returns false if the user does not exist.
|
|
pub async fn is_suspended(&self, user_id: &UserId) -> Result<bool> {
|
|
match self
|
|
.db
|
|
.userid_suspension
|
|
.get(user_id)
|
|
.await
|
|
.deserialized::<UserSuspension>()
|
|
{
|
|
| Ok(s) => Ok(s.suspended),
|
|
| Err(e) =>
|
|
if e.is_not_found() {
|
|
Ok(false)
|
|
} else {
|
|
Err(e)
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Returns true if the user is locked. Returns false if the user does not
|
|
/// exist or is not locked.
|
|
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)
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Disables login for a user, preventing them from creating new devices,
|
|
/// but allows them to continue using their existing sessions unimpeded.
|
|
pub fn disable_login(&self, user_id: &UserId) {
|
|
self.db.userid_logindisabled.insert(user_id, "");
|
|
}
|
|
|
|
/// Re-enables login for a user, allowing them to create new devices again.
|
|
pub fn enable_login(&self, user_id: &UserId) { self.db.userid_logindisabled.remove(user_id); }
|
|
|
|
/// Returns true if the target user's login is disabled.
|
|
pub async fn is_login_disabled(&self, user_id: &UserId) -> bool {
|
|
self.db
|
|
.userid_logindisabled
|
|
.exists(user_id.as_str())
|
|
.await
|
|
.is_ok()
|
|
}
|
|
|
|
/// Checks the activation status of the provided user ID's account.
|
|
pub async fn status(&self, user_id: &UserId) -> AccountStatus {
|
|
if !self.services.globals.user_is_local(user_id) {
|
|
AccountStatus::NotFound
|
|
} else if self.db.userid_password.exists(user_id).await.is_ok() {
|
|
if self.db.userid_deactivated.exists(user_id).await.is_ok() {
|
|
AccountStatus::Deactivated
|
|
} else {
|
|
AccountStatus::Active
|
|
}
|
|
} else {
|
|
AccountStatus::NotFound
|
|
}
|
|
}
|
|
|
|
/// Checks if a user is a shadow.
|
|
pub async fn is_shadow(&self, user_id: &UserId) -> bool {
|
|
self.db
|
|
.userid_password
|
|
.get(user_id)
|
|
.await
|
|
.deserialized::<String>()
|
|
.is_ok_and(|hash| hash.is_empty())
|
|
}
|
|
|
|
/// Returns the number of users registered on this server, including
|
|
/// deactivated and shadow users.
|
|
#[inline]
|
|
pub async fn count(&self) -> usize { self.db.userid_password.count().await }
|
|
|
|
/// Find out which user an access token belongs to. Will panic if the access
|
|
/// token is empty.
|
|
pub async fn find_from_token(
|
|
&self,
|
|
token: &str,
|
|
) -> Option<(OwnedUserId, OwnedDeviceId, AccessTokenStatus)> {
|
|
assert!(!token.is_empty(), "Empty access token");
|
|
|
|
let (user_id, device_id) = self
|
|
.db
|
|
.token_userdeviceid
|
|
.get(token)
|
|
.await
|
|
.deserialized()
|
|
.ok()?;
|
|
|
|
// Check if the token has expired
|
|
if let Some(expires) = self
|
|
.db
|
|
.userdeviceid_tokenexpires
|
|
.qry(&(&user_id, &device_id))
|
|
.await
|
|
.deserialized::<u64>()
|
|
.ok()
|
|
.map(Duration::from_secs)
|
|
{
|
|
let expires_at = SystemTime::UNIX_EPOCH
|
|
.checked_add(expires)
|
|
.expect("expiry time should not overflow SystemTime");
|
|
|
|
if SystemTime::now() > expires_at {
|
|
return Some((user_id, device_id, AccessTokenStatus::Expired));
|
|
}
|
|
}
|
|
|
|
Some((user_id, device_id, AccessTokenStatus::Valid))
|
|
}
|
|
|
|
/// Returns an iterator over all local users on this homeserver.
|
|
pub fn stream_local_users(&self) -> impl Stream<Item = OwnedUserId> + Send {
|
|
self.db.userid_password.keys().ignore_err()
|
|
}
|
|
|
|
/// Set a user's password.
|
|
pub async fn set_password(&self, user_id: &UserId, password: HashedPassword) -> Result {
|
|
self.status(user_id).await.ensure_active()?;
|
|
|
|
self.db.userid_password.insert(user_id, password.0);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Convert an existing user to an active local account. This will turn
|
|
/// shadow accounts into normal accounts and will clear the deactivation
|
|
/// flag on the user if it is set.
|
|
pub async fn convert_to_local_account(
|
|
&self,
|
|
user_id: &UserId,
|
|
password: HashedPassword,
|
|
) -> Result {
|
|
if !self.status(user_id).await.is_found() {
|
|
return Err!(Request(NotFound("This account does not exist.")));
|
|
}
|
|
|
|
self.db.userid_deactivated.remove(user_id);
|
|
self.db.userid_password.insert(user_id, password.0);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Convert an existing user to a shadow account. This will clear their
|
|
/// password and reactivate them if they are deactivated.
|
|
pub async fn convert_to_shadow_account(&self, user_id: &UserId) -> Result {
|
|
if !self.status(user_id).await.is_found() {
|
|
return Err!(Request(NotFound("This account does not exist.")));
|
|
}
|
|
|
|
self.db.userid_deactivated.remove(user_id);
|
|
self.db.userid_password.insert(user_id, "");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Check a user's password.
|
|
pub async fn check_password(&self, user_id: &UserId, password: &str) -> Result<OwnedUserId> {
|
|
self.status(user_id).await.ensure_active()?;
|
|
|
|
let (hash, user_id): (String, OwnedUserId) =
|
|
if let Ok(hash) = self.db.userid_password.get(user_id).await.deserialized() {
|
|
(hash, user_id.to_owned())
|
|
} else {
|
|
// We also check the lowercased version of the user ID to handle
|
|
// legacy user IDs better
|
|
let lowercase_user_id = UserId::parse(user_id.as_str().to_lowercase()).unwrap();
|
|
|
|
let hash = self
|
|
.db
|
|
.userid_password
|
|
.get(lowercase_user_id.as_str())
|
|
.await
|
|
.deserialized()
|
|
.expect("user should exist");
|
|
|
|
(hash, lowercase_user_id)
|
|
};
|
|
|
|
if hash.is_empty() {
|
|
// Cannot log into shadow users
|
|
return Err!(Request(Forbidden("This user cannot log in with a password.")));
|
|
}
|
|
|
|
utils::hash::verify_password(password, &hash)
|
|
.inspect_err(|e| debug_error!("{e}"))
|
|
.map_err(|_| err!(Request(Forbidden("Invalid identifier or password."))))?;
|
|
|
|
Ok(user_id)
|
|
}
|
|
|
|
/// Creates an OpenID token, which can be used to prove that a user has
|
|
/// access to an account (primarily for integrations)
|
|
pub fn create_openid_token(&self, user_id: &UserId, token: &str) -> Result<u64> {
|
|
use std::num::Saturating as Sat;
|
|
|
|
let expires_in = self.services.config.openid_token_ttl;
|
|
let expires_at = Sat(utils::millis_since_unix_epoch()) + Sat(expires_in) * Sat(1000);
|
|
|
|
let mut value = expires_at.0.to_be_bytes().to_vec();
|
|
value.extend_from_slice(user_id.as_bytes());
|
|
|
|
self.db
|
|
.openidtoken_expiresatuserid
|
|
.insert(token.as_bytes(), value.as_slice());
|
|
|
|
Ok(expires_in)
|
|
}
|
|
|
|
/// Find out which user an OpenID access token belongs to.
|
|
pub async fn find_from_openid_token(&self, token: &str) -> Result<OwnedUserId> {
|
|
let Ok(value) = self.db.openidtoken_expiresatuserid.get(token).await else {
|
|
return Err!(Request(Unauthorized("OpenID token is unrecognised")));
|
|
};
|
|
|
|
let (expires_at_bytes, user_bytes) = value.split_at(0_u64.to_be_bytes().len());
|
|
let expires_at =
|
|
u64::from_be_bytes(expires_at_bytes.try_into().map_err(|e| {
|
|
err!(Database("expires_at in openid_userid is invalid u64. {e}"))
|
|
})?);
|
|
|
|
if expires_at < utils::millis_since_unix_epoch() {
|
|
debug_warn!("OpenID token is expired, removing");
|
|
self.db.openidtoken_expiresatuserid.remove(token.as_bytes());
|
|
|
|
return Err!(Request(Unauthorized("OpenID token is expired")));
|
|
}
|
|
|
|
let user_string = utils::string_from_bytes(user_bytes)
|
|
.map_err(|e| err!(Database("User ID in openid_userid is invalid unicode. {e}")))?;
|
|
|
|
OwnedUserId::try_from(user_string)
|
|
.map_err(|e| err!(Database("User ID in openid_userid is invalid. {e}")))
|
|
}
|
|
|
|
/// Creates a short-lived login token, which can be used to log in using the
|
|
/// `m.login.token` mechanism.
|
|
pub fn create_login_token(&self, user_id: &UserId, token: &str) -> u64 {
|
|
use std::num::Saturating as Sat;
|
|
|
|
let expires_in = self.services.config.login_token_ttl;
|
|
let expires_at = Sat(utils::millis_since_unix_epoch()) + Sat(expires_in);
|
|
|
|
let value = (expires_at.0, user_id);
|
|
self.db.logintoken_expiresatuserid.raw_put(token, value);
|
|
|
|
expires_in
|
|
}
|
|
|
|
/// Find out which user a login token belongs to.
|
|
/// Removes the token to prevent double-use attacks.
|
|
pub async fn find_from_login_token(&self, token: &str) -> Result<OwnedUserId> {
|
|
let Ok(value) = self.db.logintoken_expiresatuserid.get(token).await else {
|
|
return Err!(Request(Forbidden("Login token is unrecognised")));
|
|
};
|
|
let (expires_at, user_id): (u64, OwnedUserId) = value.deserialized()?;
|
|
|
|
if expires_at < utils::millis_since_unix_epoch() {
|
|
trace!(%user_id, ?token, "Removing expired login token");
|
|
|
|
self.db.logintoken_expiresatuserid.remove(token);
|
|
|
|
return Err!(Request(Forbidden("Login token is expired")));
|
|
}
|
|
|
|
self.db.logintoken_expiresatuserid.remove(token);
|
|
|
|
Ok(user_id)
|
|
}
|
|
}
|