1use std::collections::BTreeMap;
9use std::fs;
10use std::io;
11use std::path::Path;
12
13use serde::{Deserialize, Serialize};
14use zeroize::Zeroizing;
15
16use crate::error::AuthError;
17use crate::hash::{hash_password, verify_password};
18use crate::role::Role;
19
20const AUTH_FILE: &str = "auth.json";
21
22#[derive(Serialize, Deserialize, Clone, Debug)]
24pub struct User {
25 pub name: String,
27 pub password_hash: String,
29 pub role: String,
31}
32
33#[derive(Serialize, Deserialize, Clone, Debug, Default)]
35pub struct UserStore {
36 users: BTreeMap<String, User>,
37}
38
39impl UserStore {
40 pub fn new() -> Self {
42 UserStore {
43 users: BTreeMap::new(),
44 }
45 }
46
47 pub fn create_user(&mut self, name: &str, password: &str, role: &str) -> Result<(), AuthError> {
52 if self.users.contains_key(name) {
53 return Err(AuthError::UserExists(name.to_string()));
54 }
55 if Role::builtin(role).is_none() {
56 return Err(AuthError::UnknownRole(role.to_string()));
57 }
58 let secret = Zeroizing::new(password.to_string());
61 let password_hash = hash_password(&secret)?;
62 self.users.insert(
63 name.to_string(),
64 User {
65 name: name.to_string(),
66 password_hash,
67 role: role.to_string(),
68 },
69 );
70 Ok(())
71 }
72
73 pub fn authenticate(&self, name: &str, candidate: &str) -> Option<&User> {
78 let user = self.users.get(name)?;
79 if verify_password(&user.password_hash, candidate) {
80 Some(user)
81 } else {
82 None
83 }
84 }
85
86 pub fn set_role(&mut self, name: &str, role: &str) -> Result<(), AuthError> {
90 if Role::builtin(role).is_none() {
91 return Err(AuthError::UnknownRole(role.to_string()));
92 }
93 let user = self
94 .users
95 .get_mut(name)
96 .ok_or_else(|| AuthError::UnknownUser(name.to_string()))?;
97 user.role = role.to_string();
98 Ok(())
99 }
100
101 pub fn set_password(&mut self, name: &str, new_password: &str) -> Result<(), AuthError> {
106 let secret = Zeroizing::new(new_password.to_string());
107 let password_hash = hash_password(&secret)?;
108 let user = self
109 .users
110 .get_mut(name)
111 .ok_or_else(|| AuthError::UnknownUser(name.to_string()))?;
112 user.password_hash = password_hash;
113 Ok(())
114 }
115
116 pub fn delete_user(&mut self, name: &str) -> Result<(), AuthError> {
118 self.users
119 .remove(name)
120 .map(|_| ())
121 .ok_or_else(|| AuthError::UnknownUser(name.to_string()))
122 }
123
124 pub fn len(&self) -> usize {
126 self.users.len()
127 }
128
129 pub fn is_empty(&self) -> bool {
132 self.users.is_empty()
133 }
134
135 pub fn list_users(&self) -> Vec<(String, String)> {
137 self.users
138 .values()
139 .map(|u| (u.name.clone(), u.role.clone()))
140 .collect()
141 }
142
143 pub fn save(&self, dir: &Path) -> io::Result<()> {
147 let json = serde_json::to_string_pretty(self)
148 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
149 let path = dir.join(AUTH_FILE);
150 fs::write(&path, json)?;
151 #[cfg(unix)]
152 {
153 use std::os::unix::fs::PermissionsExt;
154 let perms = fs::Permissions::from_mode(0o600);
155 fs::set_permissions(&path, perms)?;
156 }
157 Ok(())
158 }
159
160 pub fn load(dir: &Path) -> io::Result<Self> {
164 let path = dir.join(AUTH_FILE);
165 match fs::read_to_string(&path) {
166 Ok(json) => serde_json::from_str(&json)
167 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)),
168 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(UserStore::new()),
169 Err(e) => Err(e),
170 }
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn create_then_authenticate() {
180 let mut s = UserStore::new();
181 s.create_user("alice", "pw", "admin").unwrap();
182 assert!(s.authenticate("alice", "pw").is_some());
183 assert!(s.authenticate("alice", "bad").is_none());
184 assert!(s.authenticate("nobody", "pw").is_none());
185 }
186
187 #[test]
188 fn duplicate_create_rejected() {
189 let mut s = UserStore::new();
190 s.create_user("a", "pw", "readonly").unwrap();
191 assert!(matches!(
192 s.create_user("a", "pw2", "readonly"),
193 Err(AuthError::UserExists(_))
194 ));
195 }
196
197 #[test]
198 fn empty_and_len_track_users() {
199 let mut s = UserStore::new();
200 assert!(s.is_empty());
201 assert_eq!(s.len(), 0);
202 s.create_user("alice", "pw", "readwrite").unwrap();
203 assert!(!s.is_empty());
204 assert_eq!(s.len(), 1);
205 s.create_user("bob", "pw", "readonly").unwrap();
206 assert_eq!(s.len(), 2);
207 s.delete_user("alice").unwrap();
208 assert_eq!(s.len(), 1);
209 assert!(!s.is_empty());
210 s.delete_user("bob").unwrap();
211 assert!(s.is_empty());
212 }
213
214 #[test]
215 fn set_password_changes_credential() {
216 let mut s = UserStore::new();
217 s.create_user("alice", "old", "readwrite").unwrap();
218 assert!(s.authenticate("alice", "old").is_some());
219 s.set_password("alice", "new").unwrap();
220 assert!(s.authenticate("alice", "old").is_none());
222 assert!(s.authenticate("alice", "new").is_some());
223 assert_eq!(s.authenticate("alice", "new").unwrap().role, "readwrite");
224 }
225
226 #[test]
227 fn set_password_unknown_user_rejected() {
228 let mut s = UserStore::new();
229 assert!(matches!(
230 s.set_password("ghost", "pw"),
231 Err(AuthError::UnknownUser(_))
232 ));
233 }
234
235 #[test]
236 fn create_with_unknown_role_rejected() {
237 let mut s = UserStore::new();
238 assert!(matches!(
239 s.create_user("a", "pw", "wizard"),
240 Err(AuthError::UnknownRole(_))
241 ));
242 }
243}