diff --git a/daemon/src/main.rs b/daemon/src/main.rs index 014748b..04b168a 100644 --- a/daemon/src/main.rs +++ b/daemon/src/main.rs @@ -1,6 +1,8 @@ use anyhow::Result; use tracing::info; +mod state; + #[tokio::main] async fn main() -> Result<()> { tracing_subscriber::fmt() diff --git a/daemon/src/state.rs b/daemon/src/state.rs new file mode 100644 index 0000000..28e597a --- /dev/null +++ b/daemon/src/state.rs @@ -0,0 +1,124 @@ +use authforge_common::types::{Credential, Method, PendingFlag, Policy, Transport}; +use std::collections::HashMap; +use tokio::sync::RwLock; + +#[derive(Default)] +pub struct AppState { + inner: RwLock, +} + +#[derive(Default)] +struct StateInner { + credentials: HashMap>, + policy: Policy, + pending: HashMap, +} + +// `dead_code` allow lifts in Task 1.8 when dbus.rs starts calling these methods. +#[allow(dead_code)] +impl AppState { + pub fn with_fixtures() -> Self { + let mut inner = StateInner::default(); + inner.credentials.insert( + "alice".to_string(), + vec![Credential { + id: "fixture-cred-1".to_string(), + nickname: "Alice's Yubikey".to_string(), + method: Method::Fido2, + transport: Transport::Usb, + created_unix: 1_700_000_000, + }], + ); + Self { inner: RwLock::new(inner) } + } + + pub async fn list_credentials(&self, user: &str) -> Vec { + self.inner.read().await.credentials.get(user).cloned().unwrap_or_default() + } + + pub async fn add_credential(&self, user: &str, c: Credential) { + self.inner.write().await.credentials.entry(user.to_string()).or_default().push(c); + } + + pub async fn remove_credential(&self, user: &str, cred_id: &str) -> bool { + let mut g = self.inner.write().await; + let Some(list) = g.credentials.get_mut(user) else { return false }; + let before = list.len(); + list.retain(|c| c.id != cred_id); + list.len() != before + } + + pub async fn get_policy(&self) -> Policy { + self.inner.read().await.policy.clone() + } + + pub async fn set_policy(&self, p: Policy) { + self.inner.write().await.policy = p; + } + + pub async fn set_pending(&self, user: &str, f: PendingFlag) { + self.inner.write().await.pending.insert(user.to_string(), f); + } + + pub async fn clear_pending(&self, user: &str) -> bool { + self.inner.write().await.pending.remove(user).is_some() + } + + pub async fn has_pending(&self, user: &str) -> bool { + self.inner.read().await.pending.contains_key(user) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn fixtures_seeded_for_alice() { + let s = AppState::with_fixtures(); + let creds = s.list_credentials("alice").await; + assert_eq!(creds.len(), 1); + assert_eq!(creds[0].nickname, "Alice's Yubikey"); + } + + #[tokio::test] + async fn unknown_user_has_no_creds() { + let s = AppState::with_fixtures(); + assert!(s.list_credentials("nobody").await.is_empty()); + } + + #[tokio::test] + async fn add_then_remove_credential() { + let s = AppState::default(); + let c = Credential { + id: "id1".to_string(), + nickname: "x".to_string(), + method: Method::Fido2, + transport: Transport::Usb, + created_unix: 0, + }; + s.add_credential("bob", c).await; + assert_eq!(s.list_credentials("bob").await.len(), 1); + assert!(s.remove_credential("bob", "id1").await); + assert!(s.list_credentials("bob").await.is_empty()); + } + + #[tokio::test] + async fn pending_set_clear_roundtrip() { + let s = AppState::default(); + assert!(!s.has_pending("alice").await); + s.set_pending( + "alice", + PendingFlag { + required_methods: vec![Method::Fido2], + created_unix: 1, + deadline_unix: 0, + re_enroll: false, + }, + ) + .await; + assert!(s.has_pending("alice").await); + assert!(s.clear_pending("alice").await); + assert!(!s.has_pending("alice").await); + } +}