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:
65
markbase-core/src/provider/mod.rs
Normal file
65
markbase-core/src/provider/mod.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
pub mod sqlite;
|
||||
pub mod pg;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// 用户信息
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct User {
|
||||
pub username: String,
|
||||
pub password_hash: String,
|
||||
pub home_dir: PathBuf,
|
||||
pub uid: u32,
|
||||
pub gid: u32,
|
||||
pub permissions: String,
|
||||
pub status: i32,
|
||||
}
|
||||
|
||||
/// Provider 错误类型
|
||||
#[derive(Debug)]
|
||||
pub enum ProviderError {
|
||||
NotFound(String),
|
||||
AuthFailed(String),
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ProviderError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ProviderError::NotFound(msg) => write!(f, "Not found: {}", msg),
|
||||
ProviderError::AuthFailed(msg) => write!(f, "Authentication failed: {}", msg),
|
||||
ProviderError::Internal(msg) => write!(f, "Internal error: {}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ProviderError {}
|
||||
|
||||
/// 数据提供者 trait(用户认证和配置)
|
||||
pub trait DataProvider: Send + Sync {
|
||||
/// 获取用户信息
|
||||
fn get_user(&self, username: &str) -> Result<Option<User>, ProviderError>;
|
||||
|
||||
/// 验证用户密码
|
||||
fn check_password(&self, username: &str, password: &str) -> Result<bool, ProviderError>;
|
||||
|
||||
/// 获取用户主目录
|
||||
fn get_home_dir(&self, username: &str) -> Result<Option<String>, ProviderError>;
|
||||
|
||||
/// 获取用户组列表
|
||||
fn get_user_groups(&self, username: &str) -> Result<Vec<String>, ProviderError> {
|
||||
let _ = username;
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
/// 检查用户是否存在且启用
|
||||
fn user_exists(&self, username: &str) -> Result<bool, ProviderError> {
|
||||
Ok(self.get_user(username)?.map(|u| u.status == 1).unwrap_or(false))
|
||||
}
|
||||
|
||||
/// 获取用户的公开密钥列表(OpenSSH authorized_keys格式)
|
||||
fn get_public_keys(&self, username: &str) -> Result<Vec<String>, ProviderError> {
|
||||
let _ = username;
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
184
markbase-core/src/provider/pg.rs
Normal file
184
markbase-core/src/provider/pg.rs
Normal file
@@ -0,0 +1,184 @@
|
||||
use std::path::PathBuf;
|
||||
use postgres::{Client, NoTls};
|
||||
use bcrypt::verify;
|
||||
use super::{DataProvider, ProviderError, User};
|
||||
|
||||
/// PostgreSQL 数据提供者(兼容 SFTPGo 的 users 表)
|
||||
pub struct PgProvider {
|
||||
conn_str: String,
|
||||
}
|
||||
|
||||
impl PgProvider {
|
||||
/// 从连接字符串创建 PgProvider
|
||||
///
|
||||
/// 连接字符串格式:host=127.0.0.1 port=5432 dbname=sftpgo user=sftpgo password=sftpgo_pass_2026
|
||||
pub fn new(conn_str: &str) -> Result<Self, ProviderError> {
|
||||
Ok(Self { conn_str: conn_str.to_string() })
|
||||
}
|
||||
|
||||
pub fn from_params(
|
||||
host: &str,
|
||||
port: u16,
|
||||
dbname: &str,
|
||||
user: &str,
|
||||
password: &str,
|
||||
) -> Result<Self, ProviderError> {
|
||||
let conn_str = format!(
|
||||
"host={} port={} dbname={} user={} password={}",
|
||||
host, port, dbname, user, password
|
||||
);
|
||||
Ok(Self { conn_str })
|
||||
}
|
||||
|
||||
fn open_conn(&self) -> Result<Client, ProviderError> {
|
||||
Client::connect(&self.conn_str, NoTls)
|
||||
.map_err(|e| ProviderError::Internal(format!("PostgreSQL connect failed: {}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
impl DataProvider for PgProvider {
|
||||
fn get_user(&self, username: &str) -> Result<Option<User>, ProviderError> {
|
||||
let mut conn = self.open_conn()?;
|
||||
|
||||
let result = conn.query_opt(
|
||||
"SELECT username, password, home_dir, permissions, uid, gid, status
|
||||
FROM users WHERE username = $1 AND status = 1",
|
||||
&[&username],
|
||||
).map_err(|e| ProviderError::Internal(format!("Query error: {}", e)))?;
|
||||
|
||||
match result {
|
||||
Some(row) => Ok(Some(User {
|
||||
username: row.get(0),
|
||||
password_hash: row.get::<_, Option<String>>(1).unwrap_or_default(),
|
||||
home_dir: PathBuf::from(row.get::<_, String>(2)),
|
||||
permissions: row.get::<_, Option<String>>(3).unwrap_or_else(|| "*".to_string()),
|
||||
uid: row.get::<_, i64>(4) as u32,
|
||||
gid: row.get::<_, i64>(5) as u32,
|
||||
status: row.get(6),
|
||||
})),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn check_password(&self, username: &str, password: &str) -> Result<bool, ProviderError> {
|
||||
let hash = match self.get_user(username)? {
|
||||
Some(user) => user.password_hash,
|
||||
None => return Ok(false),
|
||||
};
|
||||
|
||||
if hash.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
verify(password, &hash)
|
||||
.map_err(|e| ProviderError::Internal(format!("bcrypt verify error: {}", e)))
|
||||
}
|
||||
|
||||
fn get_home_dir(&self, username: &str) -> Result<Option<String>, ProviderError> {
|
||||
Ok(self.get_user(username)?.map(|u| u.home_dir.to_string_lossy().to_string()))
|
||||
}
|
||||
|
||||
fn get_public_keys(&self, username: &str) -> Result<Vec<String>, ProviderError> {
|
||||
let mut conn = self.open_conn()?;
|
||||
let result = conn.query_opt(
|
||||
"SELECT public_keys FROM users WHERE username = $1 AND status = 1",
|
||||
&[&username],
|
||||
).map_err(|e| ProviderError::Internal(format!("Query error: {}", e)))?;
|
||||
|
||||
match result {
|
||||
Some(row) => {
|
||||
let json_str: Option<String> = row.get(0);
|
||||
match json_str {
|
||||
Some(s) if !s.is_empty() => {
|
||||
let keys: Vec<serde_json::Value> = serde_json::from_str(&s)
|
||||
.map_err(|e| ProviderError::Internal(format!("JSON parse error: {}", e)))?;
|
||||
Ok(keys.iter()
|
||||
.filter_map(|v| v.get("public_key")?.as_str().map(|s| s.to_string()))
|
||||
.collect())
|
||||
}
|
||||
_ => Ok(Vec::new()),
|
||||
}
|
||||
}
|
||||
None => Ok(Vec::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_pg_provider_connection() {
|
||||
// 仅当 SFTPGo PostgreSQL 可用时运行
|
||||
let provider = PgProvider::new(
|
||||
"host=127.0.0.1 port=5432 dbname=sftpgo user=sftpgo password=sftpgo_pass_2026"
|
||||
);
|
||||
assert!(provider.is_ok(), "Should connect to SFTPGo PostgreSQL");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pg_get_user_demo() {
|
||||
let provider = PgProvider::new(
|
||||
"host=127.0.0.1 port=5432 dbname=sftpgo user=sftpgo password=sftpgo_pass_2026"
|
||||
).unwrap();
|
||||
let user = provider.get_user("demo").unwrap();
|
||||
assert!(user.is_some(), "Demo user should exist");
|
||||
assert_eq!(user.unwrap().username, "demo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pg_get_user_momentry() {
|
||||
let provider = PgProvider::new(
|
||||
"host=127.0.0.1 port=5432 dbname=sftpgo user=sftpgo password=sftpgo_pass_2026"
|
||||
).unwrap();
|
||||
let user = provider.get_user("momentry").unwrap();
|
||||
assert!(user.is_some(), "Momentry user should exist");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pg_get_user_warren() {
|
||||
let provider = PgProvider::new(
|
||||
"host=127.0.0.1 port=5432 dbname=sftpgo user=sftpgo password=sftpgo_pass_2026"
|
||||
).unwrap();
|
||||
let user = provider.get_user("warren").unwrap();
|
||||
assert!(user.is_some(), "Warren user should exist");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pg_check_password_demo() {
|
||||
let provider = PgProvider::new(
|
||||
"host=127.0.0.1 port=5432 dbname=sftpgo user=sftpgo password=sftpgo_pass_2026"
|
||||
).unwrap();
|
||||
let valid = provider.check_password("demo", "demo123").unwrap();
|
||||
assert!(valid, "Password should be valid");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pg_check_password_invalid() {
|
||||
let provider = PgProvider::new(
|
||||
"host=127.0.0.1 port=5432 dbname=sftpgo user=sftpgo password=sftpgo_pass_2026"
|
||||
).unwrap();
|
||||
let valid = provider.check_password("demo", "wrong").unwrap();
|
||||
assert!(!valid, "Wrong password should fail");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pg_get_home_dir() {
|
||||
let provider = PgProvider::new(
|
||||
"host=127.0.0.1 port=5432 dbname=sftpgo user=sftpgo password=sftpgo_pass_2026"
|
||||
).unwrap();
|
||||
let dir = provider.get_home_dir("demo").unwrap();
|
||||
assert!(dir.is_some());
|
||||
assert!(dir.unwrap().contains("momentry"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pg_nonexistent_user() {
|
||||
let provider = PgProvider::new(
|
||||
"host=127.0.0.1 port=5432 dbname=sftpgo user=sftpgo password=sftpgo_pass_2026"
|
||||
).unwrap();
|
||||
let user = provider.get_user("__nonexistent__").unwrap();
|
||||
assert!(user.is_none());
|
||||
}
|
||||
}
|
||||
135
markbase-core/src/provider/sqlite.rs
Normal file
135
markbase-core/src/provider/sqlite.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
use std::path::PathBuf;
|
||||
use rusqlite::{Connection, params};
|
||||
use bcrypt::verify;
|
||||
use super::{DataProvider, ProviderError, User};
|
||||
|
||||
/// SQLite 数据提供者
|
||||
pub struct SqliteProvider {
|
||||
db_path: PathBuf,
|
||||
}
|
||||
|
||||
impl SqliteProvider {
|
||||
pub fn new(db_path: &str) -> Result<Self, ProviderError> {
|
||||
let path = PathBuf::from(db_path);
|
||||
if !path.exists() {
|
||||
return Err(ProviderError::NotFound(format!(
|
||||
"Database not found: {}", db_path
|
||||
)));
|
||||
}
|
||||
Ok(Self { db_path: path })
|
||||
}
|
||||
|
||||
fn open_conn(&self) -> Result<Connection, ProviderError> {
|
||||
Connection::open(&self.db_path)
|
||||
.map_err(|e| ProviderError::Internal(format!("Failed to open database: {}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
impl DataProvider for SqliteProvider {
|
||||
fn get_user(&self, username: &str) -> Result<Option<User>, ProviderError> {
|
||||
let conn = self.open_conn()?;
|
||||
|
||||
let result = conn.query_row(
|
||||
"SELECT username, password_hash, home_dir, permissions, uid, gid, status
|
||||
FROM sftpgo_users WHERE username = ?1 AND status = 1",
|
||||
params![username],
|
||||
|row| {
|
||||
Ok(User {
|
||||
username: row.get(0)?,
|
||||
password_hash: row.get(1)?,
|
||||
home_dir: PathBuf::from(row.get::<_, String>(2)?),
|
||||
permissions: row.get(3)?,
|
||||
uid: row.get::<_, i64>(4)? as u32,
|
||||
gid: row.get::<_, i64>(5)? as u32,
|
||||
status: row.get(6)?,
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
match result {
|
||||
Ok(user) => Ok(Some(user)),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(e) => Err(ProviderError::Internal(format!(
|
||||
"Database query error: {}", e
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn check_password(&self, username: &str, password: &str) -> Result<bool, ProviderError> {
|
||||
let hash = match self.get_user(username)? {
|
||||
Some(user) => user.password_hash,
|
||||
None => return Ok(false),
|
||||
};
|
||||
|
||||
verify(password, &hash)
|
||||
.map_err(|e| ProviderError::Internal(format!("bcrypt verify error: {}", e)))
|
||||
}
|
||||
|
||||
fn get_home_dir(&self, username: &str) -> Result<Option<String>, ProviderError> {
|
||||
Ok(self.get_user(username)?.map(|u| u.home_dir.to_string_lossy().to_string()))
|
||||
}
|
||||
|
||||
fn get_public_keys(&self, username: &str) -> Result<Vec<String>, ProviderError> {
|
||||
let _ = username;
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
fn get_user_groups(&self, username: &str) -> Result<Vec<String>, ProviderError> {
|
||||
let conn = self.open_conn()?;
|
||||
let groups: Vec<String> = conn
|
||||
.prepare("SELECT group_name FROM users_groups_mapping WHERE username = ?1")
|
||||
.map_err(|e| ProviderError::Internal(format!("Query prepare error: {}", e)))?
|
||||
.query_map(params![username], |row| row.get(0))
|
||||
.map_err(|e| ProviderError::Internal(format!("Query map error: {}", e)))?
|
||||
.filter_map(|r| r.ok())
|
||||
.collect();
|
||||
Ok(groups)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_provider_not_found() {
|
||||
let provider = SqliteProvider::new("/tmp/nonexistent.db");
|
||||
assert!(provider.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_provider_get_user() {
|
||||
let provider = SqliteProvider::new("data/auth.sqlite").unwrap();
|
||||
let user = provider.get_user("demo").unwrap();
|
||||
assert!(user.is_some());
|
||||
assert_eq!(user.unwrap().username, "demo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_provider_nonexistent_user() {
|
||||
let provider = SqliteProvider::new("data/auth.sqlite").unwrap();
|
||||
let user = provider.get_user("__nonexistent__").unwrap();
|
||||
assert!(user.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_password_valid() {
|
||||
let provider = SqliteProvider::new("data/auth.sqlite").unwrap();
|
||||
let valid = provider.check_password("demo", "demo123").unwrap();
|
||||
assert!(valid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_password_invalid() {
|
||||
let provider = SqliteProvider::new("data/auth.sqlite").unwrap();
|
||||
let valid = provider.check_password("demo", "wrong").unwrap();
|
||||
assert!(!valid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_home_dir() {
|
||||
let provider = SqliteProvider::new("data/auth.sqlite").unwrap();
|
||||
let dir = provider.get_home_dir("demo").unwrap();
|
||||
assert!(dir.is_some());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user