use std::collections::BTreeMap;
use std::fs;
use std::io;
use std::path::Path;
use serde::{Deserialize, Serialize};
use zeroize::Zeroizing;
use crate::error::AuthError;
use crate::hash::{hash_password, verify_password};
use crate::role::Role;
const AUTH_FILE: &str = "auth.json";
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct User {
pub name: String,
pub password_hash: String,
pub role: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct UserStore {
users: BTreeMap<String, User>,
}
impl UserStore {
pub fn new() -> Self {
UserStore {
users: BTreeMap::new(),
}
}
pub fn create_user(&mut self, name: &str, password: &str, role: &str) -> Result<(), AuthError> {
if self.users.contains_key(name) {
return Err(AuthError::UserExists(name.to_string()));
}
if Role::builtin(role).is_none() {
return Err(AuthError::UnknownRole(role.to_string()));
}
let secret = Zeroizing::new(password.to_string());
let password_hash = hash_password(&secret)?;
self.users.insert(
name.to_string(),
User {
name: name.to_string(),
password_hash,
role: role.to_string(),
},
);
Ok(())
}
pub fn authenticate(&self, name: &str, candidate: &str) -> Option<&User> {
let user = self.users.get(name)?;
if verify_password(&user.password_hash, candidate) {
Some(user)
} else {
None
}
}
pub fn set_role(&mut self, name: &str, role: &str) -> Result<(), AuthError> {
if Role::builtin(role).is_none() {
return Err(AuthError::UnknownRole(role.to_string()));
}
let user = self
.users
.get_mut(name)
.ok_or_else(|| AuthError::UnknownUser(name.to_string()))?;
user.role = role.to_string();
Ok(())
}
pub fn set_password(&mut self, name: &str, new_password: &str) -> Result<(), AuthError> {
let secret = Zeroizing::new(new_password.to_string());
let password_hash = hash_password(&secret)?;
let user = self
.users
.get_mut(name)
.ok_or_else(|| AuthError::UnknownUser(name.to_string()))?;
user.password_hash = password_hash;
Ok(())
}
pub fn delete_user(&mut self, name: &str) -> Result<(), AuthError> {
self.users
.remove(name)
.map(|_| ())
.ok_or_else(|| AuthError::UnknownUser(name.to_string()))
}
pub fn len(&self) -> usize {
self.users.len()
}
pub fn is_empty(&self) -> bool {
self.users.is_empty()
}
pub fn list_users(&self) -> Vec<(String, String)> {
self.users
.values()
.map(|u| (u.name.clone(), u.role.clone()))
.collect()
}
pub fn save(&self, dir: &Path) -> io::Result<()> {
let json = serde_json::to_string_pretty(self)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
let path = dir.join(AUTH_FILE);
fs::write(&path, json)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = fs::Permissions::from_mode(0o600);
fs::set_permissions(&path, perms)?;
}
Ok(())
}
pub fn load(dir: &Path) -> io::Result<Self> {
let path = dir.join(AUTH_FILE);
match fs::read_to_string(&path) {
Ok(json) => serde_json::from_str(&json)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(UserStore::new()),
Err(e) => Err(e),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn create_then_authenticate() {
let mut s = UserStore::new();
s.create_user("alice", "pw", "admin").unwrap();
assert!(s.authenticate("alice", "pw").is_some());
assert!(s.authenticate("alice", "bad").is_none());
assert!(s.authenticate("nobody", "pw").is_none());
}
#[test]
fn duplicate_create_rejected() {
let mut s = UserStore::new();
s.create_user("a", "pw", "readonly").unwrap();
assert!(matches!(
s.create_user("a", "pw2", "readonly"),
Err(AuthError::UserExists(_))
));
}
#[test]
fn empty_and_len_track_users() {
let mut s = UserStore::new();
assert!(s.is_empty());
assert_eq!(s.len(), 0);
s.create_user("alice", "pw", "readwrite").unwrap();
assert!(!s.is_empty());
assert_eq!(s.len(), 1);
s.create_user("bob", "pw", "readonly").unwrap();
assert_eq!(s.len(), 2);
s.delete_user("alice").unwrap();
assert_eq!(s.len(), 1);
assert!(!s.is_empty());
s.delete_user("bob").unwrap();
assert!(s.is_empty());
}
#[test]
fn set_password_changes_credential() {
let mut s = UserStore::new();
s.create_user("alice", "old", "readwrite").unwrap();
assert!(s.authenticate("alice", "old").is_some());
s.set_password("alice", "new").unwrap();
assert!(s.authenticate("alice", "old").is_none());
assert!(s.authenticate("alice", "new").is_some());
assert_eq!(s.authenticate("alice", "new").unwrap().role, "readwrite");
}
#[test]
fn set_password_unknown_user_rejected() {
let mut s = UserStore::new();
assert!(matches!(
s.set_password("ghost", "pw"),
Err(AuthError::UnknownUser(_))
));
}
#[test]
fn create_with_unknown_role_rejected() {
let mut s = UserStore::new();
assert!(matches!(
s.create_user("a", "pw", "wizard"),
Err(AuthError::UnknownRole(_))
));
}
}