use crate::admin::permissions::{Groupe, Permission, pull_groupes_db};
use crate::auth::guard::{cache_permissions, evict_permissions, get_permissions};
use crate::auth::user_trait::RuniqueUser;
use crate::context::RequestExtensions;
use crate::middleware::session::session_db::RuniqueSessionStore;
use crate::utils::config::TraceResult;
use crate::utils::constante::{
admin_key::admin_context::permission::GROUPES,
session_key::session::{
SESSION_ACTIVE_KEY, SESSION_USER_ID_KEY, SESSION_USER_IS_STAFF_KEY,
SESSION_USER_IS_SUPERUSER_KEY, SESSION_USER_USERNAME_KEY,
},
};
use crate::utils::pk::Pk;
use axum::{extract::Request, middleware::Next, response::Response};
use sea_orm::DatabaseConnection;
use serde::{Deserialize, Serialize};
use std::marker::PhantomData;
use tower_sessions::Session;
#[derive(Debug, Clone)]
pub struct AdminLoginResult {
pub user_id: Pk,
pub username: String,
pub is_staff: bool,
pub is_superuser: bool,
}
#[async_trait::async_trait]
pub trait AdminAuth: Send + Sync + 'static {
async fn authenticate(
&self,
username: &str,
password: &str,
db: &DatabaseConnection,
) -> Option<AdminLoginResult>;
}
#[async_trait::async_trait]
pub trait UserEntity: Send + Sync + 'static {
type Model: RuniqueUser;
async fn find_by_id(db: &DatabaseConnection, id: crate::utils::pk::Pk) -> Option<Self::Model>;
async fn find_by_username(db: &DatabaseConnection, username: &str) -> Option<Self::Model>;
async fn find_by_email(db: &DatabaseConnection, email: &str) -> Option<Self::Model>;
async fn update_password(
db: &DatabaseConnection,
email: &str,
new_hash: &str,
) -> Result<(), sea_orm::DbErr>;
async fn update_password_by_id(
db: &DatabaseConnection,
id: crate::utils::pk::Pk,
new_hash: &str,
) -> Result<(), sea_orm::DbErr> {
let user = Self::find_by_id(db, id)
.await
.ok_or_else(|| sea_orm::DbErr::RecordNotFound("User not found".into()))?;
Self::update_password(db, user.email(), new_hash).await
}
}
pub struct DefaultAdminAuth<E: UserEntity>(PhantomData<E>);
impl<E: UserEntity> DefaultAdminAuth<E> {
pub fn new() -> Self {
Self(PhantomData)
}
}
impl<E: UserEntity> Default for DefaultAdminAuth<E> {
fn default() -> Self {
Self::new()
}
}
#[async_trait::async_trait]
impl<E: UserEntity> AdminAuth for DefaultAdminAuth<E> {
async fn authenticate(
&self,
username: &str,
password: &str,
db: &DatabaseConnection,
) -> Option<AdminLoginResult> {
let user_opt = E::find_by_username(db, username).await;
let hash = user_opt
.as_ref()
.map(|u| u.password_hash())
.unwrap_or(crate::utils::password::dummy_hash());
let password_ok = crate::utils::password::verify(password, hash);
let user = user_opt?;
if !user.can_access_admin() || !password_ok {
return None;
}
Some(AdminLoginResult {
user_id: user.user_id(),
username: user.username().to_string(),
is_staff: user.is_staff(),
is_superuser: user.is_superuser(),
})
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CurrentUser {
pub id: Pk,
pub username: String,
pub is_staff: bool,
pub is_superuser: bool,
pub groupes: Vec<Groupe>,
}
impl CurrentUser {
#[must_use]
pub fn permission_for(&self, resource_key: &str) -> Option<Permission> {
self.permissions_effectives()
.into_iter()
.find(|p| p.resource_key == resource_key)
}
pub fn permissions_effectives(&self) -> Vec<Permission> {
let mut agg: std::collections::HashMap<String, Permission> =
std::collections::HashMap::new();
for groupe in &self.groupes {
for perm in &groupe.permissions {
agg.entry(perm.resource_key.clone())
.or_insert_with(|| Permission::zeroed(perm.resource_key.clone()))
.merge_from(perm);
}
}
agg.into_values().collect()
}
#[must_use]
pub fn can_access_resource(&self, resource_key: &str) -> bool {
self.is_superuser
|| self
.permission_for(resource_key)
.is_some_and(|p| p.can_read)
}
#[must_use]
pub fn can_access_admin(&self) -> bool {
self.is_staff || self.is_superuser
}
}
async fn session_bool(session: &Session, key: &str) -> bool {
session
.get::<bool>(key)
.await
.ok()
.flatten()
.unwrap_or(false)
}
pub async fn is_authenticated(session: &Session) -> bool {
session
.get::<Pk>(SESSION_USER_ID_KEY)
.await
.ok()
.flatten()
.is_some()
}
pub async fn is_admin_authenticated(session: &Session) -> bool {
is_authenticated(session).await
&& (session_bool(session, SESSION_USER_IS_STAFF_KEY).await
|| session_bool(session, SESSION_USER_IS_SUPERUSER_KEY).await)
}
pub async fn get_user_id(session: &Session) -> Option<Pk> {
session.get::<Pk>(SESSION_USER_ID_KEY).await.ok().flatten()
}
pub async fn get_username(session: &Session) -> Option<String> {
session
.get::<String>(SESSION_USER_USERNAME_KEY)
.await
.ok()
.flatten()
}
#[allow(clippy::too_many_arguments)]
pub async fn login(
session: &Session,
db: &DatabaseConnection,
user_id: Pk,
username: &str,
is_staff: bool,
is_superuser: bool,
db_store: Option<&RuniqueSessionStore>,
exclusive: bool,
) -> Result<(), tower_sessions::session::Error> {
let existing_id: Option<_> = session.get::<Pk>(SESSION_USER_ID_KEY).await.ok().flatten();
let is_privilege_elevation = existing_id != Some(user_id);
if let Some(existing) = existing_id
&& existing != user_id
{
logout(session, db_store).await.trace(
crate::utils::runique_log::get_log()
.session
.as_ref()
.and_then(|s| s.store),
"pre-login logout of previous session",
);
}
if is_privilege_elevation {
session.cycle_id().await.trace_or(
crate::utils::runique_log::get_log()
.session
.as_ref()
.and_then(|s| s.store),
tracing::Level::WARN,
"cycle session id (session fixation protection)",
);
}
let groupes = pull_groupes_db(db, user_id).await;
cache_permissions(user_id, groupes.clone());
if let Some(level) = crate::utils::runique_log::get_log()
.auth
.as_ref()
.and_then(|a| a.login)
{
crate::runique_log!(
level,
user_id = %user_id,
username = %username,
is_superuser,
exclusive,
db_persist = db_store.is_some(),
"login"
);
}
session.insert(SESSION_USER_ID_KEY, user_id).await?;
session
.insert(SESSION_USER_USERNAME_KEY, username.to_string())
.await?;
session.insert(SESSION_USER_IS_STAFF_KEY, is_staff).await?;
session
.insert(SESSION_USER_IS_SUPERUSER_KEY, is_superuser)
.await?;
session.set_expiry(Some(tower_sessions::Expiry::OnInactivity(
tower_sessions::cookie::time::Duration::hours(24),
)));
if let Some(store) = db_store {
let cookie_id = session.id().map(|id| id.to_string()).unwrap_or_default();
let session_id = uuid::Uuid::new_v4().to_string();
let expires_at = chrono::Utc::now()
.naive_utc()
.checked_add_signed(chrono::Duration::hours(24))
.unwrap_or_else(|| chrono::Utc::now().naive_utc());
store
.create(&cookie_id, user_id, &session_id, expires_at)
.await
.trace_or(
crate::utils::runique_log::get_log()
.session
.as_ref()
.and_then(|s| s.store),
tracing::Level::WARN,
"persist session to DB",
);
if exclusive {
store
.invalidate_other_sessions(user_id, &cookie_id)
.await
.trace_or(
crate::utils::runique_log::get_log()
.session
.as_ref()
.and_then(|s| s.exclusive_login),
tracing::Level::WARN,
"invalidate other sessions (exclusive login)",
);
}
}
Ok(())
}
pub async fn auth_login(
session: &Session,
db: &DatabaseConnection,
user_id: Pk,
) -> Result<(), tower_sessions::session::Error> {
let Some(user) = crate::auth::user::BuiltinUserEntity::find_by_id(db, user_id).await else {
return Ok(());
};
if !user.is_active() {
return Ok(());
}
let store = RuniqueSessionStore::new(std::sync::Arc::new(db.clone()));
login(
session,
db,
user.user_id(),
user.username(),
user.is_staff(),
user.is_superuser(),
Some(&store),
false,
)
.await
}
pub async fn logout(
session: &Session,
db_store: Option<&RuniqueSessionStore>,
) -> Result<(), tower_sessions::session::Error> {
if let Some(store) = db_store {
let cookie_id = session.id().map(|id| id.to_string()).unwrap_or_default();
store.delete(&cookie_id).await.trace(
crate::utils::runique_log::get_log()
.session
.as_ref()
.and_then(|s| s.store),
"delete session from DB on logout",
);
}
if let Some(user_id) = session.get::<Pk>(SESSION_USER_ID_KEY).await.ok().flatten() {
evict_permissions(user_id);
}
session.remove::<i32>(SESSION_USER_ID_KEY).await?;
session.remove::<String>(SESSION_USER_USERNAME_KEY).await?;
session.remove::<bool>(SESSION_USER_IS_STAFF_KEY).await?;
session
.remove::<bool>(SESSION_USER_IS_SUPERUSER_KEY)
.await?;
session.remove::<Vec<Groupe>>(GROUPES).await?;
session.remove::<i64>(SESSION_ACTIVE_KEY).await?;
session.delete().await
}
pub async fn protect_session(
session: &Session,
duration_secs: i64,
) -> Result<(), tower_sessions::session::Error> {
let protect_until = chrono::Utc::now().timestamp().saturating_add(duration_secs);
session.insert(SESSION_ACTIVE_KEY, protect_until).await
}
pub async fn unprotect_session(session: &Session) -> Result<(), tower_sessions::session::Error> {
session.remove::<i64>(SESSION_ACTIVE_KEY).await?;
Ok(())
}
pub async fn load_user_middleware(
axum::extract::State(db): axum::extract::State<crate::utils::aliases::ADb>,
session: Session,
mut request: Request,
next: Next,
) -> Response {
if let (Some(user_id), Some(username)) =
(get_user_id(&session).await, get_username(&session).await)
{
let is_staff = session_bool(&session, SESSION_USER_IS_STAFF_KEY).await;
let is_superuser = session_bool(&session, SESSION_USER_IS_SUPERUSER_KEY).await;
let groupes = match get_permissions(user_id) {
Some(cached) => cached.groupes.clone(),
None => {
let groupes = pull_groupes_db(&*db, user_id).await;
cache_permissions(user_id, groupes.clone());
groupes
}
};
let current_user = CurrentUser {
id: user_id,
username,
is_staff,
is_superuser,
groupes,
};
let extensions = RequestExtensions::new().with_current_user(current_user);
extensions.inject_request(&mut request);
} else if session.id().is_some() {
session.delete().await.trace(
crate::utils::runique_log::get_log()
.session
.as_ref()
.and_then(|s| s.store),
"delete anonymous session",
);
}
next.run(request).await
}