VFS/DataProvider/Config refactoring + SSH public key authentication
Phase 1-6 of refactoring plan: - VFS abstraction (VfsBackend trait + LocalFs + OpenFlags builder) - DataProvider trait (SqliteProvider + PgProvider, SFTPGo-compatible) - Config refactoring (AppConfig unified sections, env overrides) - SSH handlers (sftp/scp/rsync) migrated to VFS + DataProvider - SSH public key authentication (Ed25519 signature verification) - SSH stderr → CHANNEL_EXTENDED_DATA support - Web auth uses DataProvider instead of direct SQL - User home directory from provider (per-user isolation) - PostgreSQL auth provider for SFTPGo compatibility
This commit is contained in:
@@ -5,6 +5,8 @@ use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::provider::{DataProvider, ProviderError};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
pub user_id: String,
|
||||
@@ -66,13 +68,13 @@ pub struct AuthState {
|
||||
pub users: Arc<Mutex<HashMap<String, User>>>,
|
||||
pub auth_db: Option<crate::sync::AuthDb>,
|
||||
pub admin_sessions: Arc<Mutex<HashMap<String, AdminSession>>>,
|
||||
pub provider: Option<Arc<dyn DataProvider>>,
|
||||
}
|
||||
|
||||
impl AuthState {
|
||||
pub fn new() -> Self {
|
||||
let mut users = HashMap::new();
|
||||
|
||||
// Create default demo user
|
||||
let password_hash = hash("demo123", DEFAULT_COST).unwrap();
|
||||
users.insert(
|
||||
"demo".to_string(),
|
||||
@@ -89,6 +91,7 @@ impl AuthState {
|
||||
users: Arc::new(Mutex::new(users)),
|
||||
auth_db: None,
|
||||
admin_sessions: Arc::new(Mutex::new(HashMap::new())),
|
||||
provider: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +103,17 @@ impl AuthState {
|
||||
users: Arc::new(Mutex::new(HashMap::new())),
|
||||
auth_db,
|
||||
admin_sessions: Arc::new(Mutex::new(HashMap::new())),
|
||||
provider: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_provider(provider: Box<dyn DataProvider>) -> Self {
|
||||
AuthState {
|
||||
sessions: Arc::new(Mutex::new(HashMap::new())),
|
||||
users: Arc::new(Mutex::new(HashMap::new())),
|
||||
auth_db: None,
|
||||
admin_sessions: Arc::new(Mutex::new(HashMap::new())),
|
||||
provider: Some(Arc::from(provider)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,8 +222,12 @@ impl AuthState {
|
||||
}
|
||||
|
||||
pub fn login_with_sync(&self, username: &str, password: &str) -> Option<LoginResponse> {
|
||||
// Prefer provider over auth_db
|
||||
if let Some(provider) = &self.provider {
|
||||
return self.login_with_provider(&**provider, username, password);
|
||||
}
|
||||
|
||||
if let Some(auth_db) = &self.auth_db {
|
||||
// Get user from auth.sqlite
|
||||
let user = match auth_db.get_user(username) {
|
||||
Ok(Some(user)) => user,
|
||||
Ok(None) => {
|
||||
@@ -266,11 +284,70 @@ impl AuthState {
|
||||
}
|
||||
}
|
||||
|
||||
fn login_with_provider(&self, provider: &dyn DataProvider, username: &str, password: &str) -> Option<LoginResponse> {
|
||||
match provider.get_user(username) {
|
||||
Ok(Some(user)) => {
|
||||
if user.status != 1 {
|
||||
log::warn!("User {} is disabled or not found", username);
|
||||
return None;
|
||||
}
|
||||
|
||||
match provider.check_password(username, password) {
|
||||
Ok(true) => {
|
||||
let groups = provider.get_user_groups(username).unwrap_or_default();
|
||||
|
||||
let token = Uuid::new_v4().to_string();
|
||||
let now = Utc::now();
|
||||
let expires_at = now + Duration::hours(24);
|
||||
|
||||
let session = Session {
|
||||
token: token.clone(),
|
||||
user_id: username.to_string(),
|
||||
username: username.to_string(),
|
||||
created_at: now.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
|
||||
expires_at: expires_at.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
|
||||
groups: groups.clone(),
|
||||
permissions: user.permissions.clone(),
|
||||
};
|
||||
|
||||
let mut sessions = self.sessions.lock().unwrap();
|
||||
sessions.insert(token.clone(), session);
|
||||
|
||||
log::info!("User {} logged in via DataProvider", username);
|
||||
|
||||
Some(LoginResponse {
|
||||
token,
|
||||
expires_at: expires_at.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
|
||||
user_id: username.to_string(),
|
||||
groups,
|
||||
permissions: user.permissions,
|
||||
})
|
||||
}
|
||||
Ok(false) => {
|
||||
log::warn!("Invalid password for user {}", username);
|
||||
None
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Password check error for {}: {}", username, e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
log::warn!("User {} not found", username);
|
||||
None
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Provider error for {}: {}", username, e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_token(&self, token: &str) -> Option<Session> {
|
||||
let sessions = self.sessions.lock().unwrap();
|
||||
let session = sessions.get(token)?;
|
||||
|
||||
// Check expiration
|
||||
let expires_at = chrono::DateTime::parse_from_rfc3339(&session.expires_at)
|
||||
.ok()?
|
||||
.with_timezone(&Utc);
|
||||
|
||||
Reference in New Issue
Block a user