quincy_server/
users_file.rs

1use std::{
2    fs::{self, File},
3    io::{BufRead, BufReader, BufWriter, Write},
4    path::Path,
5    sync::Arc,
6};
7
8use argon2::{Argon2, PasswordHash, PasswordVerifier};
9use async_trait::async_trait;
10use dashmap::DashMap;
11use ipnet::IpNet;
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14
15use crate::server::address_pool::AddressPool;
16use quincy::{
17    auth::{ClientAuthenticator, ServerAuthenticator},
18    config::{ClientAuthenticationConfig, ServerAuthenticationConfig},
19    error::AuthError,
20    Result,
21};
22
23pub struct UsersFileServerAuthenticator {
24    user_database: UserDatabase,
25    address_pool: Arc<AddressPool>,
26}
27
28pub struct UsersFileClientAuthenticator {
29    username: String,
30    password: String,
31}
32
33/// Authentication payload for users file authentication method
34#[derive(Clone, Debug, Serialize, Deserialize)]
35pub struct UsersFilePayload {
36    username: String,
37    password: String,
38}
39
40/// Represents a user database for authentication
41pub struct UserDatabase {
42    users: DashMap<String, User>,
43    hasher: Argon2<'static>,
44}
45
46/// Represents a user with authentication information
47pub struct User {
48    pub username: String,
49    pub password_hash: String,
50}
51
52impl UsersFileServerAuthenticator {
53    /// Creates a new users file server authenticator
54    ///
55    /// # Arguments
56    /// * `config` - Server authentication configuration
57    /// * `address_pool` - Pool of available client IP addresses
58    ///
59    /// # Errors
60    /// Returns `AuthError::StoreUnavailable` if the users file cannot be loaded
61    pub fn new(
62        config: &ServerAuthenticationConfig,
63        address_pool: Arc<AddressPool>,
64    ) -> Result<Self> {
65        let users_file =
66            load_users_file(&config.users_file).map_err(|_| AuthError::StoreUnavailable)?;
67        let user_database = UserDatabase::new(users_file);
68
69        Ok(Self {
70            user_database,
71            address_pool,
72        })
73    }
74}
75
76impl UsersFileClientAuthenticator {
77    /// Creates a new users file client authenticator
78    ///
79    /// # Arguments
80    /// * `config` - Client authentication configuration containing credentials
81    pub fn new(config: &ClientAuthenticationConfig) -> Self {
82        Self {
83            username: config.username.clone(),
84            password: config.password.clone(),
85        }
86    }
87}
88
89#[async_trait]
90impl ServerAuthenticator for UsersFileServerAuthenticator {
91    async fn authenticate_user(&self, authentication_payload: Value) -> Result<(String, IpNet)> {
92        let payload: UsersFilePayload = serde_json::from_value(authentication_payload)
93            .map_err(|_| AuthError::InvalidPayload)?;
94
95        self.user_database
96            .authenticate(&payload.username, payload.password)
97            .await?;
98
99        let client_address = self
100            .address_pool
101            .next_available_address()
102            .ok_or(AuthError::StoreUnavailable)?;
103
104        Ok((payload.username, client_address))
105    }
106}
107
108#[async_trait]
109impl ClientAuthenticator for UsersFileClientAuthenticator {
110    async fn generate_payload(&self) -> Result<Value> {
111        let payload = UsersFilePayload {
112            username: self.username.clone(),
113            password: self.password.clone(),
114        };
115
116        Ok(serde_json::to_value(payload).map_err(|_| AuthError::InvalidPayload)?)
117    }
118}
119
120impl User {
121    /// Creates a new `User` instance given the username and password hash.
122    ///
123    /// ### Arguments
124    /// - `username` - the username
125    /// - `password_hash` - a password hash representing the user's password
126    pub fn new(username: String, password_hash: String) -> Self {
127        Self {
128            username,
129            password_hash,
130        }
131    }
132}
133
134impl TryFrom<String> for User {
135    type Error = quincy::QuincyError;
136
137    fn try_from(user_string: String) -> Result<Self> {
138        let split: Vec<String> = user_string.split(':').map(|str| str.to_owned()).collect();
139        let name = split.first().ok_or(AuthError::InvalidPayload)?.clone();
140        let password_hash_string = split.get(1).ok_or(AuthError::InvalidPayload)?.clone();
141
142        Ok(User::new(name, password_hash_string))
143    }
144}
145
146impl UserDatabase {
147    /// Creates a new instance of the authentication module.
148    ///
149    /// ### Arguments
150    /// - `users` - a map of users (username -> `User`)
151    pub fn new(users: DashMap<String, User>) -> Self {
152        Self {
153            users,
154            hasher: Argon2::default(),
155        }
156    }
157
158    /// Authenticates the given user credentials.
159    ///
160    /// # Arguments
161    /// * `username` - The username to authenticate
162    /// * `password` - The password to verify
163    ///
164    /// # Errors
165    /// Returns `AuthError::UserNotFound` if the user doesn't exist
166    /// Returns `AuthError::InvalidCredentials` if the password is incorrect
167    /// Returns `AuthError::PasswordHashingFailed` if password verification fails
168    pub async fn authenticate(&self, username: &str, password: String) -> Result<()> {
169        let user = self.users.get(username).ok_or(AuthError::UserNotFound)?;
170
171        let password_hash =
172            PasswordHash::new(&user.password_hash).map_err(|_| AuthError::PasswordHashingFailed)?;
173
174        self.hasher
175            .verify_password(password.as_bytes(), &password_hash)
176            .map_err(|_| AuthError::InvalidCredentials)?;
177
178        Ok(())
179    }
180}
181
182/// Loads the contents of a file with users and their password hashes into a map.
183///
184/// # Arguments
185/// * `users_file` - Path to the users file
186///
187/// # Returns
188/// A `DashMap` containing all loaded users
189///
190/// # Errors
191/// Returns `AuthError::StoreUnavailable` if the file cannot be read or parsed
192pub fn load_users_file(users_file: &Path) -> Result<DashMap<String, User>> {
193    let file = File::open(users_file).map_err(|_| AuthError::StoreUnavailable)?;
194    let lines = BufReader::new(file).lines();
195
196    let result: DashMap<String, User> = DashMap::new();
197
198    for line in lines {
199        let line_content = line.map_err(|_| AuthError::StoreUnavailable)?;
200        let user: User = line_content.try_into()?;
201        result.insert(user.username.clone(), user);
202    }
203
204    Ok(result)
205}
206
207/// Writes the users and their password hashes into the specified file
208///
209/// # Arguments
210/// * `users_file` - Path to the users file
211/// * `users` - A map of users (username -> `User`)
212///
213/// # Errors
214/// Returns `AuthError::StoreUnavailable` if the file cannot be written
215pub fn save_users_file(users_file: &Path, users: DashMap<String, User>) -> Result<()> {
216    if users_file.exists() {
217        fs::remove_file(users_file).map_err(|_| AuthError::StoreUnavailable)?;
218    }
219
220    let file = File::create(users_file).map_err(|_| AuthError::StoreUnavailable)?;
221    let mut writer = BufWriter::new(file);
222
223    for (username, user) in users {
224        writer
225            .write_all(format!("{username}:{}\n", user.password_hash).as_bytes())
226            .map_err(|_| AuthError::StoreUnavailable)?;
227    }
228
229    Ok(())
230}
231
232#[cfg(test)]
233mod tests {
234    use crate::users_file::{User, UserDatabase};
235    use argon2::password_hash::SaltString;
236    use argon2::{Argon2, PasswordHasher};
237    use dashmap::DashMap;
238    use rand_core::OsRng;
239
240    #[tokio::test]
241    async fn test_authentication() {
242        let users: DashMap<String, User> = DashMap::new();
243
244        let argon = Argon2::default();
245        let username = "test".to_owned();
246        let password = "password".to_owned();
247        let salt = SaltString::generate(&mut OsRng);
248
249        let password_hash = argon.hash_password(password.as_bytes(), &salt).unwrap();
250
251        let test_user = User::new(username.clone(), password_hash.to_string());
252        users.insert(username.clone(), test_user);
253
254        let user_db = UserDatabase::new(users);
255        user_db
256            .authenticate(&username, password)
257            .await
258            .expect("Credentials are valid");
259    }
260}