1use crate::backend::{AuthInteraction, VaultAuthBackend};
8use crate::password::PasswordBackend;
9use crate::ssh::SshAgentBackend;
10use crate::vault_meta::VaultMetadata;
11use core_types::{AuthCombineMode, TrustProfileName};
12use std::path::Path;
13
14pub struct AuthDispatcher {
20 backends: Vec<Box<dyn VaultAuthBackend>>,
21}
22
23impl AuthDispatcher {
24 #[must_use]
25 pub fn new() -> Self {
26 Self {
27 backends: vec![
28 Box::new(SshAgentBackend::new()),
29 Box::new(PasswordBackend::new()),
30 ],
31 }
32 }
33
34 #[must_use]
36 pub fn backends(&self) -> &[Box<dyn VaultAuthBackend>] {
37 &self.backends
38 }
39
40 pub async fn applicable_backends(
45 &self,
46 profile: &TrustProfileName,
47 config_dir: &Path,
48 meta: &VaultMetadata,
49 ) -> Vec<&dyn VaultAuthBackend> {
50 let mut applicable = Vec::new();
51 for backend in &self.backends {
52 if meta.has_factor(backend.factor_id()) && backend.can_unlock(profile, config_dir).await
53 {
54 applicable.push(backend.as_ref());
55 }
56 }
57 applicable
58 }
59
60 pub async fn find_auto_backend(
62 &self,
63 profile: &TrustProfileName,
64 config_dir: &Path,
65 ) -> Option<&dyn VaultAuthBackend> {
66 for backend in &self.backends {
67 if backend.requires_interaction() == AuthInteraction::None
68 && backend.is_enrolled(profile, config_dir)
69 && backend.can_unlock(profile, config_dir).await
70 {
71 return Some(backend.as_ref());
72 }
73 }
74 None
75 }
76
77 pub async fn can_auto_unlock(
80 &self,
81 profile: &TrustProfileName,
82 config_dir: &Path,
83 meta: &VaultMetadata,
84 ) -> bool {
85 match &meta.auth_policy {
86 AuthCombineMode::Any => {
87 self.find_auto_backend(profile, config_dir).await.is_some()
89 }
90 AuthCombineMode::All | AuthCombineMode::Policy(_) => {
91 let applicable = self.applicable_backends(profile, config_dir, meta).await;
94 if applicable.is_empty() {
95 return false;
96 }
97 applicable
98 .iter()
99 .all(|b| b.requires_interaction() == AuthInteraction::None)
100 }
101 }
102 }
103
104 #[must_use]
111 pub fn password_backend(&self) -> &dyn VaultAuthBackend {
112 self.backends
113 .iter()
114 .find(|b| b.backend_id() == "password")
115 .map(std::convert::AsRef::as_ref)
116 .expect("password backend is always registered")
117 }
118}
119
120impl Default for AuthDispatcher {
121 fn default() -> Self {
122 Self::new()
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 fn test_profile() -> TrustProfileName {
131 TrustProfileName::try_from("test-profile").unwrap()
132 }
133
134 #[tokio::test]
135 async fn find_auto_backend_returns_none_without_enrollment() {
136 let dispatcher = AuthDispatcher::new();
137 let profile = test_profile();
138 let result = dispatcher
139 .find_auto_backend(&profile, std::path::Path::new("/nonexistent"))
140 .await;
141 assert!(result.is_none());
142 }
143
144 #[test]
145 fn password_backend_always_available() {
146 let dispatcher = AuthDispatcher::new();
147 let backend = dispatcher.password_backend();
148 assert_eq!(backend.backend_id(), "password");
149 assert_eq!(
150 backend.requires_interaction(),
151 AuthInteraction::PasswordEntry
152 );
153 }
154
155 #[tokio::test]
156 async fn applicable_backends_empty_when_no_enrollment() {
157 let dispatcher = AuthDispatcher::new();
158 let profile = test_profile();
159 let meta = VaultMetadata::new_password(AuthCombineMode::Any);
160 let applicable = dispatcher
162 .applicable_backends(&profile, std::path::Path::new("/nonexistent"), &meta)
163 .await;
164 assert!(applicable.is_empty());
166 }
167
168 #[tokio::test]
169 async fn can_auto_unlock_false_without_enrollment() {
170 let dispatcher = AuthDispatcher::new();
171 let profile = test_profile();
172 let meta = VaultMetadata::new_password(AuthCombineMode::Any);
173 assert!(
174 !dispatcher
175 .can_auto_unlock(&profile, std::path::Path::new("/nonexistent"), &meta)
176 .await
177 );
178 }
179}