quincy_server/
users_file.rs1use 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#[derive(Clone, Debug, Serialize, Deserialize)]
35pub struct UsersFilePayload {
36 username: String,
37 password: String,
38}
39
40pub struct UserDatabase {
42 users: DashMap<String, User>,
43 hasher: Argon2<'static>,
44}
45
46pub struct User {
48 pub username: String,
49 pub password_hash: String,
50}
51
52impl UsersFileServerAuthenticator {
53 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 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 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 pub fn new(users: DashMap<String, User>) -> Self {
152 Self {
153 users,
154 hasher: Argon2::default(),
155 }
156 }
157
158 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
182pub 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
207pub 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}