use crate::models::users::users; use crate::protos::auth::common::v1::{ AuthMethod, MfaStatus, MfaType, Principal, SshKeyAlgorithm, UserPublic, UserRef, UserStatus, user_ref, }; use chrono::{DateTime, Utc}; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; use uuid::Uuid; use crate::api::convert; use crate::errors::{AuthError, AuthResult}; use crate::share::user as user_store; pub const ALLOWED_APP_SCOPES: [&str; 12] = [ "user_name", "ssh_key", "access_key", "assets", "email", "profile", "theme", "guild", "event", "avatar", "repo_read", "repo_write", ]; pub const MAX_BATCH_USER_IDS: usize = 500; pub async fn find_user( db: &crate::db::DbConn, reference: &UserRef, ) -> AuthResult> { let Some(key) = reference.key.as_ref() else { return Err(AuthError::MalformedRequest); }; let query = match key { user_ref::Key::UserId(user_id) => users::Entity::find_by_id(*user_id), user_ref::Key::Username(username) => users::Entity::find() .filter(users::Column::Lowername.eq(username.trim().to_lowercase().as_str())), user_ref::Key::Email(email) => users::Entity::find() .filter(users::Column::Email.eq(email.trim().to_lowercase().as_str())), }; Ok(query.one(db.read()).await?) } pub async fn load_user(db: &crate::db::DbConn, user_id: i64) -> AuthResult { user_store::load_user(db, user_id).await } pub async fn load_active_user(db: &crate::db::DbConn, user_id: i64) -> AuthResult { user_store::load_active_user(db, user_id).await } pub fn is_active(user: &users::Model) -> bool { user_store::is_active(user) } pub fn ensure_active(user: &users::Model) -> AuthResult<()> { user_store::ensure_active(user) } pub fn ensure_security_version(user: &users::Model, expected: i32) -> AuthResult<()> { user_store::ensure_security_version(user, expected) } pub async fn active_mfa_count(db: &crate::db::DbConn, user_id: i64) -> AuthResult { user_store::active_mfa_count(db, user_id).await } pub async fn has_enabled_mfa(db: &crate::db::DbConn, user_id: i64) -> AuthResult { user_store::has_enabled_mfa(db, user_id).await } pub fn principal(user: &users::Model, method: AuthMethod) -> Principal { Principal { user_id: user.id, username: user.username.clone(), displayname: user .displayname .clone() .unwrap_or_else(|| user.username.clone()), is_admin: user.is_admin, bot: user.bot, system: user.system, security_version: user.security_version, method: method as i32, session_id: String::new(), access_key_id: String::new(), scopes: Vec::new(), scopes_bounded: false, mfa_verified: false, authenticated_at: Some(convert::now_timestamp()), service_id: String::new(), } } pub fn user_public(user: &users::Model, email: String, avatar_url: Option) -> UserPublic { UserPublic { user_id: user.id, username: user.username.clone(), displayname: user .displayname .clone() .unwrap_or_else(|| user.username.clone()), email, email_verified: user.email_verified_at.is_some(), avatar_url: avatar_url.unwrap_or_default(), is_admin: user.is_admin, bot: user.bot, system: user.system, } } pub fn user_status_of(user: &users::Model, active_mfa_factors: u32) -> UserStatus { UserStatus { exists: true, allow_use: user.allow_use, forbid_login: user.forbid_login, forbid_login_reason: user.forbid_login_reason.clone().unwrap_or_default(), forbid_login_until: convert::datetime_opt(user.endtime_forbid_login_at), deleted_at: convert::datetime_opt(user.deleted_at), deletion_requested_at: convert::datetime_opt(user.deletion_requested_at), email_verified_at: convert::datetime_opt(user.email_verified_at), security_version: user.security_version, active_mfa_factors, } } pub fn empty_status() -> UserStatus { UserStatus::default() } #[allow(clippy::too_many_arguments)] pub async fn audit( db: &crate::db::DbConn, user_id: i64, actor_user_id: Option, event_type: crate::models::users::user_events::UserEventType, details: Option, request_id: Option<&str>, client_ip: Option, user_agent: Option, ) -> AuthResult<()> { crate::share::audit::entry( db, user_id, actor_user_id, event_type, details, request_id, client_ip, user_agent, ) .await } pub fn page_token(offset: u64) -> String { offset.to_string() } pub fn parse_page_token(token: &str) -> Option { let trimmed = token.trim(); if trimmed.is_empty() { return Some(0); } trimmed.parse::().ok() } pub fn mfa_type_code(value: MfaType) -> i32 { value as i32 } pub fn mfa_status_code(value: MfaStatus) -> i32 { value as i32 } pub fn ssh_algorithm_code(value: crate::models::users::user_ssh_keys::PublicKeyType) -> i32 { let algorithm = match value { crate::models::users::user_ssh_keys::PublicKeyType::RSA => SshKeyAlgorithm::Rsa, crate::models::users::user_ssh_keys::PublicKeyType::ED25519 => SshKeyAlgorithm::Ed25519, crate::models::users::user_ssh_keys::PublicKeyType::ECDSA => SshKeyAlgorithm::Ecdsa, }; algorithm as i32 } pub fn new_id() -> Uuid { Uuid::now_v7() } pub async fn load_session( cache: &crate::cache::Cache, session_key: &str, ) -> AuthResult> { use crate::session::SessionStore; let key = crate::session::SessionKey::try_from(session_key.trim()) .map_err(|_| AuthError::Unauthenticated)?; let store = crate::session::RedisStore::new(cache.clone()); Ok(store.load(&key).await?.map(|(state, _)| state)) } pub fn now() -> DateTime { Utc::now() } pub fn session_index_key(cache: &crate::cache::Cache, user_id: i64) -> String { cache.key(&["user:session", &user_id.to_string()]) } pub async fn session_index(cache: &crate::cache::Cache, user_id: i64) -> AuthResult> { Ok(cache.smembers(&session_index_key(cache, user_id)).await?) } pub async fn drop_session_state( cache: &crate::cache::Cache, user_id: i64, session_key: &str, ) -> AuthResult<()> { use crate::session::{SessionKey, SessionStore}; let key = SessionKey::try_from(session_key.to_owned()).map_err(|_| AuthError::SessionNotFound)?; crate::session::RedisStore::new(cache.clone()) .delete(&key, Some(user_id)) .await?; Ok(()) } pub async fn refresh_session_version( cache: &crate::cache::Cache, user_id: i64, session_id: Uuid, version: i32, ttl_secs: u64, ) -> AuthResult<()> { use crate::session::{RedisStore, Session, SessionKey, SessionStore}; let ttl = actix_web::cookie::time::Duration::seconds(ttl_secs.max(1) as i64); let expected = session_id.to_string(); for key in session_index(cache, user_id).await? { let Some(state) = load_session(cache, &key).await? else { continue; }; let diff = { let session = Session::from_state(state); if session.session_id().as_deref() != Some(expected.as_str()) { continue; } session.set_security_version(version)?; session.diff() }; let key = SessionKey::try_from(key).map_err(|_| AuthError::SessionNotFound)?; RedisStore::new(cache.clone()) .update(key, diff, &ttl, Some(user_id), Some(user_id)) .await?; } Ok(()) } pub async fn drop_session_by_id( cache: &crate::cache::Cache, user_id: i64, session_id: Uuid, ) -> AuthResult<()> { use crate::session::Session; let expected = session_id.to_string(); for key in session_index(cache, user_id).await? { let Some(state) = load_session(cache, &key).await? else { continue; }; let matches = Session::from_state(state).session_id().as_deref() == Some(expected.as_str()); if !matches { continue; } if let Err(error) = drop_session_state(cache, user_id, &key).await { tracing::warn!(user_id, %error, "session state drop failed"); continue; } if let Err(error) = cache.srem(&session_index_key(cache, user_id), &key).await { tracing::warn!(user_id, %error, "session index cleanup failed"); } } Ok(()) } pub async fn drop_user_session_states( cache: &crate::cache::Cache, user_id: i64, keep: Option, ) -> AuthResult { use crate::session::Session; use futures_util::StreamExt; let index_key = session_index_key(cache, user_id); let dropped = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)); futures_util::stream::iter(session_index(cache, user_id).await?) .map(|key| { let cache = cache.clone(); let dropped = dropped.clone(); let index_key = index_key.clone(); async move { let bound = match load_session(&cache, &key).await { Ok(Some(state)) => Session::from_state(state) .get::("auth_session_id") .ok() .flatten() .and_then(|value| Uuid::parse_str(&value).ok()) .is_some_and(|id| Some(id) != keep), Ok(None) => true, Err(error) => { tracing::warn!(user_id, %error, "session state load failed during drop"); false } }; if !bound { return; } if let Err(error) = drop_session_state(&cache, user_id, &key).await { tracing::warn!(user_id, %error, "session state drop failed"); return; } if let Err(error) = cache.srem(&index_key, &key).await { tracing::warn!(user_id, %error, "session index cleanup failed"); return; } dropped.fetch_add(1, std::sync::atomic::Ordering::Relaxed); } }) .buffer_unordered(8) .for_each(|_| async {}) .await; Ok(dropped.load(std::sync::atomic::Ordering::Relaxed)) }