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