VFS/DataProvider/Config refactoring + SSH public key authentication
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled

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:
Warren
2026-06-18 23:35:18 +08:00
parent 83fb0de78a
commit f90e4f496c
25 changed files with 2039 additions and 612 deletions

View File

@@ -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);