pub use types::*;
pub use session::SessionManager;
pub use permissions::PermissionChecker;
pub use password::PasswordUtils;
pub use jwt::JwtManager;
pub mod types;
pub mod session;
pub mod permissions;
pub mod password;
pub mod jwt;
pub mod certificate;
pub mod ldap;
pub mod oauth;
#[cfg(feature = "saml")]
pub mod saml;
use crate::auth::ldap::LdapService;
use crate::auth::oauth::OAuth2Service;
#[cfg(feature = "saml")]
use crate::auth::saml::{SamlConfig, SamlProvider};
use crate::config::{SecurityConfig, UserConfig};
use crate::error::{FusekiError, FusekiResult};
use axum::{
extract::{FromRequestParts, State},
http::{request::Parts, HeaderMap, StatusCode},
response::{IntoResponse, Response},
RequestPartsExt,
};
use axum_extra::headers::{authorization::Bearer, Authorization, HeaderMapExt};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{debug, error, info, warn};
#[derive(Clone)]
pub struct AuthService {
config: Arc<SecurityConfig>,
users: Arc<RwLock<HashMap<String, UserConfig>>>,
session_manager: SessionManager,
jwt_manager: Option<JwtManager>,
oauth2_service: Option<OAuth2Service>,
ldap_service: Option<LdapService>,
#[cfg(feature = "saml")]
saml_provider: Option<Arc<SamlProvider>>,
}
impl AuthService {
pub async fn new(config: SecurityConfig) -> FusekiResult<Self> {
let users = config.users.clone();
let session_manager = SessionManager::new(config.session.timeout_secs as i64);
let jwt_manager = config.jwt.as_ref()
.map(|jwt_config| JwtManager::new(jwt_config))
.transpose()?;
let oauth2_service = config.oauth.as_ref()
.map(|oauth_config| OAuth2Service::new(oauth_config.clone()));
let ldap_service = if let Some(ldap_config) = config.ldap.as_ref() {
Some(LdapService::new(ldap_config.clone()).await?)
} else {
None
};
#[cfg(feature = "saml")]
let saml_provider = None;
Ok(Self {
config: Arc::new(config),
users: Arc::new(RwLock::new(users)),
session_manager,
jwt_manager,
oauth2_service,
ldap_service,
#[cfg(feature = "saml")]
saml_provider,
})
}
pub async fn authenticate_user(&self, username: &str, password: &str) -> FusekiResult<AuthResult> {
let users = self.users.read().await;
if let Some(user_config) = users.get(username) {
if !user_config.enabled {
warn!("Login attempt for disabled user: {}", username);
return Ok(AuthResult::Forbidden);
}
if let Some(locked_until) = user_config.locked_until {
if locked_until > chrono::Utc::now() {
warn!("Login attempt for locked user: {} (locked until {})", username, locked_until);
return Ok(AuthResult::Locked);
}
}
if PasswordUtils::verify_password(password, &user_config.password_hash)? {
debug!("Successful local authentication for user: {}", username);
let permissions = PermissionChecker::compute_user_permissions(user_config);
let user = User {
username: username.to_string(),
roles: user_config.roles.clone(),
email: user_config.email.clone(),
full_name: user_config.full_name.clone(),
last_login: user_config.last_login,
permissions,
};
self.update_last_login(username).await?;
return Ok(AuthResult::Authenticated(user));
} else {
warn!("Failed local authentication attempt for user: {}", username);
self.increment_failed_attempts(username).await?;
}
}
if let Some(ldap_service) = &self.ldap_service {
debug!("Trying LDAP authentication for user: {}", username);
match ldap_service.authenticate(username, password).await {
Ok(ldap_user) => {
info!("Successful LDAP authentication for user: {}", username);
return Ok(AuthResult::Authenticated(ldap_user));
}
Err(e) => {
warn!("LDAP authentication failed for user {}: {}", username, e);
}
}
}
debug!("Authentication failed for user: {}", username);
Ok(AuthResult::Unauthenticated)
}
pub async fn create_session(&self, user: User) -> FusekiResult<String> {
self.session_manager.create_session(user).await
}
pub async fn validate_session(&self, session_id: &str) -> FusekiResult<AuthResult> {
self.session_manager.validate_session(session_id).await
}
pub async fn logout(&self, session_id: &str) -> FusekiResult<()> {
self.session_manager.invalidate_session(session_id).await
}
pub fn has_permission(&self, user: &User, permission: &Permission) -> bool {
PermissionChecker::has_permission(user, permission)
}
pub fn generate_token(&self, user: &User) -> FusekiResult<String> {
self.jwt_manager.as_ref()
.ok_or_else(|| FusekiError::configuration("JWT is not configured".to_string()))?
.generate_token(user)
}
pub fn validate_token(&self, token: &str) -> FusekiResult<TokenValidation> {
self.jwt_manager.as_ref()
.ok_or_else(|| FusekiError::configuration("JWT is not configured".to_string()))?
.validate_token(token)
}
pub fn hash_password(&self, password: &str) -> FusekiResult<String> {
PasswordUtils::hash_password(password)
}
pub async fn upsert_user(&self, username: String, user_config: UserConfig) -> FusekiResult<()> {
let mut users = self.users.write().await;
users.insert(username.clone(), user_config);
info!("User upserted: {}", username);
Ok(())
}
pub async fn remove_user(&self, username: &str) -> FusekiResult<bool> {
let mut users = self.users.write().await;
let removed = users.remove(username).is_some();
if removed {
info!("User removed: {}", username);
self.session_manager.invalidate_user_sessions(username).await?;
}
Ok(removed)
}
pub async fn get_user(&self, username: &str) -> Option<UserConfig> {
let users = self.users.read().await;
users.get(username).cloned()
}
pub async fn list_users(&self) -> HashMap<String, UserConfig> {
let users = self.users.read().await;
users.clone()
}
pub fn is_ldap_enabled(&self) -> bool {
self.ldap_service.is_some()
}
pub fn is_oauth2_enabled(&self) -> bool {
self.oauth2_service.is_some()
}
pub fn is_jwt_enabled(&self) -> bool {
self.jwt_manager.is_some()
}
async fn update_last_login(&self, username: &str) -> FusekiResult<()> {
let mut users = self.users.write().await;
if let Some(user) = users.get_mut(username) {
user.last_login = Some(chrono::Utc::now());
user.failed_login_attempts = 0;
}
Ok(())
}
async fn increment_failed_attempts(&self, username: &str) -> FusekiResult<()> {
let mut users = self.users.write().await;
if let Some(user) = users.get_mut(username) {
user.failed_login_attempts += 1;
if user.failed_login_attempts >= 5 {
user.locked_until = Some(chrono::Utc::now() + chrono::Duration::minutes(15));
warn!("User locked due to failed login attempts: {}", username);
}
}
Ok(())
}
}
impl<S> FromRequestParts<S> for AuthUser
where
S: Send + Sync,
Arc<AuthService>: axum::extract::FromRef<S>,
{
type Rejection = Response;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let auth_service = Arc::<AuthService>::from_ref(state);
if let Some(auth_header) = parts.headers.get("authorization") {
if let Ok(auth_str) = auth_header.to_str() {
if let Some(token) = JwtManager::extract_token_from_header(auth_str) {
match auth_service.validate_token(token) {
Ok(validation) => {
return Ok(AuthUser {
user: validation.user,
});
}
Err(e) => {
debug!("Token validation failed: {}", e);
}
}
}
}
}
if let Ok(cookies) = parts.extract::<axum_extra::extract::CookieJar>().await {
if let Some(session_cookie) = cookies.get("session_id") {
match auth_service.validate_session(session_cookie.value()).await {
Ok(AuthResult::Authenticated(user)) => {
return Ok(AuthUser { user });
}
Ok(_) => {
debug!("Session validation failed");
}
Err(e) => {
debug!("Session validation error: {}", e);
}
}
}
}
Err(StatusCode::UNAUTHORIZED.into_response())
}
}
#[derive(Debug, Clone)]
pub struct AuthUser {
pub user: User,
}
impl AuthUser {
pub fn has_permission(&self, permission: &Permission) -> bool {
PermissionChecker::has_permission(&self.user, permission)
}
pub fn username(&self) -> &str {
&self.user.username
}
pub fn roles(&self) -> &[String] {
&self.user.roles
}
pub fn has_role(&self, role: &str) -> bool {
self.user.roles.contains(&role.to_string())
}
}