Skip to main content

fnox_core/providers/
mod.rs

1use crate::error::Result;
2use async_trait::async_trait;
3use std::collections::HashMap;
4
5// Provider implementation modules
6pub mod age;
7pub mod aws_kms;
8pub mod aws_ps;
9pub mod aws_sm;
10pub mod azure_kms;
11pub mod azure_sm;
12pub mod bitwarden;
13pub mod bitwarden_sm;
14pub mod doppler;
15#[cfg(not(target_env = "musl"))]
16pub mod fido2;
17pub mod foks;
18pub mod gcp_kms;
19pub mod gcp_sm;
20pub mod hw_encrypt;
21pub mod infisical;
22pub mod keepass;
23pub mod keychain;
24pub mod onepassword;
25pub mod password_store;
26pub mod passwordstate;
27pub mod plain;
28pub mod proton_pass;
29pub mod resolved;
30pub mod resolver;
31pub mod secret_ref;
32pub mod vault;
33pub mod yubikey;
34pub mod yubikey_usb;
35
36pub use bitwarden::BitwardenBackend;
37pub use resolver::resolve_provider_config;
38pub use secret_ref::{OptionStringOrSecretRef, StringOrSecretRef};
39
40/// Provider capabilities - what a provider can do
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum ProviderCapability {
43    /// Provider can encrypt/decrypt values locally (stores ciphertext in config)
44    Encryption,
45    /// Provider stores values remotely (stores only references in config)
46    RemoteStorage,
47    /// Provider fetches values from a remote source (like 1Password, read-only)
48    RemoteRead,
49}
50
51/// Category for grouping providers in the wizard
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum WizardCategory {
54    Local,
55    PasswordManager,
56    CloudKms,
57    CloudSecretsManager,
58    OsKeychain,
59}
60
61impl WizardCategory {
62    /// Display name for the category
63    pub fn display_name(&self) -> &'static str {
64        match self {
65            Self::Local => "Local (easy to start)",
66            Self::PasswordManager => "Password Manager",
67            Self::CloudKms => "Cloud KMS",
68            Self::CloudSecretsManager => "Cloud Secrets Manager",
69            Self::OsKeychain => "OS Keychain",
70        }
71    }
72
73    /// Description for the category
74    pub fn description(&self) -> &'static str {
75        match self {
76            Self::Local => "Plain text or local encryption - no external dependencies",
77            Self::PasswordManager => {
78                "1Password, Bitwarden, Infisical - use your existing password manager"
79            }
80            Self::CloudKms => "AWS KMS, Azure Key Vault, GCP KMS - encrypt with cloud keys",
81            Self::CloudSecretsManager => {
82                "AWS, Azure, GCP, HashiCorp Vault - store secrets remotely"
83            }
84            Self::OsKeychain => "Use your operating system's secure keychain",
85        }
86    }
87
88    /// All categories in display order
89    pub fn all() -> &'static [WizardCategory] {
90        &[
91            Self::Local,
92            Self::PasswordManager,
93            Self::CloudKms,
94            Self::CloudSecretsManager,
95            Self::OsKeychain,
96        ]
97    }
98}
99
100/// A field that the wizard needs to collect
101#[derive(Debug, Clone)]
102pub struct WizardField {
103    /// Internal field name (e.g., "region")
104    pub name: &'static str,
105    /// Prompt shown to user (e.g., "AWS Region:")
106    pub label: &'static str,
107    /// Placeholder value (e.g., "us-east-1")
108    pub placeholder: &'static str,
109    /// Whether field must have a value
110    pub required: bool,
111}
112
113/// Complete wizard metadata for a provider type
114#[derive(Debug, Clone)]
115pub struct WizardInfo {
116    /// Provider type identifier (e.g., "aws-sm")
117    pub provider_type: &'static str,
118    /// Display name (e.g., "AWS Secrets Manager")
119    pub display_name: &'static str,
120    /// Short description for selection menu
121    pub description: &'static str,
122    /// Category for grouping
123    pub category: WizardCategory,
124    /// Multi-line setup instructions
125    pub setup_instructions: &'static str,
126    /// Default provider name (e.g., "sm")
127    pub default_name: &'static str,
128    /// Fields to collect from user
129    pub fields: &'static [WizardField],
130}
131
132// Include generated code for ProviderConfig and ResolvedProviderConfig enums
133mod generated {
134    pub(super) mod providers_config {
135        include!(concat!(env!("OUT_DIR"), "/generated/providers_config.rs"));
136    }
137    pub(super) mod providers_methods {
138        include!(concat!(env!("OUT_DIR"), "/generated/providers_methods.rs"));
139    }
140    pub(super) mod providers_instantiate {
141        // Need to import provider modules for instantiation
142        #[cfg(not(target_env = "musl"))]
143        use super::super::fido2;
144        use super::super::{
145            age, aws_kms, aws_ps, aws_sm, azure_kms, azure_sm, bitwarden, bitwarden_sm, doppler,
146            foks, gcp_kms, gcp_sm, infisical, keepass, keychain, onepassword, password_store,
147            passwordstate, plain, proton_pass, vault, yubikey,
148        };
149        include!(concat!(
150            env!("OUT_DIR"),
151            "/generated/providers_instantiate.rs"
152        ));
153    }
154    pub(super) mod providers_wizard {
155        include!(concat!(env!("OUT_DIR"), "/generated/providers_wizard.rs"));
156    }
157    pub(super) mod providers_resolver {
158        include!(concat!(env!("OUT_DIR"), "/generated/providers_resolver.rs"));
159    }
160}
161
162// Re-export generated types
163pub use generated::providers_config::{ProviderConfig, ResolvedProviderConfig};
164pub use generated::providers_instantiate::get_provider_from_resolved;
165pub use generated::providers_wizard::ALL_WIZARD_INFO;
166
167#[async_trait]
168pub trait Provider: Send + Sync {
169    /// Get a secret value from the provider (decrypt if needed)
170    async fn get_secret(&self, value: &str) -> Result<String>;
171
172    /// Get multiple secrets in a batch (more efficient for some providers)
173    ///
174    /// Takes a slice of (key, value) tuples where:
175    /// - key: the environment variable name (e.g., "MY_SECRET")
176    /// - value: the provider-specific reference (e.g., "op://vault/item/field")
177    ///
178    /// Returns a HashMap of successfully resolved secrets. Failures are logged but don't
179    /// stop other secrets from being resolved.
180    ///
181    /// Default implementation fetches secrets in parallel using tokio tasks.
182    /// Providers can override this for true batch operations (e.g., single API call)
183    /// or different concurrency using `get_secrets_concurrent`.
184    async fn get_secrets_batch(
185        &self,
186        secrets: &[(String, String)],
187    ) -> HashMap<String, Result<String>> {
188        get_secrets_concurrent(self, secrets, 10).await
189    }
190
191    /// Encrypt a value with this provider (for encryption providers)
192    async fn encrypt(&self, _value: &str) -> Result<String> {
193        // Default implementation for non-encryption providers
194        Err(crate::error::FnoxError::Provider(
195            "This provider does not support encryption".to_string(),
196        ))
197    }
198
199    /// Store a secret and return the value to save in config
200    ///
201    /// This is a unified method for both encryption and remote storage:
202    /// - Encryption providers (age, aws-kms): encrypt the value and return ciphertext
203    /// - Remote storage providers (aws-sm, keychain): store remotely and return the key name
204    /// - Read-only providers: return an error
205    ///
206    /// Returns the value that should be stored in the config file.
207    async fn put_secret(&self, _key: &str, value: &str) -> Result<String> {
208        let capabilities = self.capabilities();
209
210        if capabilities.contains(&ProviderCapability::Encryption) {
211            // Encryption provider - encrypt and return ciphertext
212            self.encrypt(value).await
213        } else if capabilities.contains(&ProviderCapability::RemoteStorage) {
214            // Remote storage provider - should override this method
215            Err(crate::error::FnoxError::Provider(
216                "Remote storage provider must implement put_secret".to_string(),
217            ))
218        } else {
219            // Read-only provider
220            Err(crate::error::FnoxError::Provider(
221                "This provider does not support storing secrets".to_string(),
222            ))
223        }
224    }
225
226    /// Get the capabilities of this provider
227    fn capabilities(&self) -> Vec<ProviderCapability> {
228        // Default: read-only remote provider (like 1Password, Bitwarden)
229        vec![ProviderCapability::RemoteRead]
230    }
231
232    /// Test if the provider is accessible and properly configured
233    async fn test_connection(&self) -> Result<()> {
234        // Default implementation does a basic check
235        Ok(())
236    }
237}
238
239/// Fetch secrets concurrently with configurable concurrency limit.
240///
241/// Helper for providers that want to use the default parallel fetch behavior
242/// but with a different concurrency level.
243pub async fn get_secrets_concurrent(
244    provider: &(impl Provider + ?Sized),
245    secrets: &[(String, String)],
246    concurrency: usize,
247) -> HashMap<String, Result<String>> {
248    use futures::stream::{self, StreamExt};
249
250    // Clone the secrets to avoid lifetime issues with async closures
251    let secrets_vec: Vec<_> = secrets.to_vec();
252
253    // Fetch all secrets in parallel (up to `concurrency` concurrent)
254    let results: Vec<_> = stream::iter(secrets_vec)
255        .map(|(key, value)| async move {
256            let result = provider.get_secret(&value).await;
257            (key, result)
258        })
259        .buffer_unordered(concurrency)
260        .collect()
261        .await;
262
263    results.into_iter().collect()
264}
265
266impl ProviderConfig {
267    /// Get wizard info for providers in a specific category
268    pub fn wizard_info_by_category(category: WizardCategory) -> Vec<&'static WizardInfo> {
269        ALL_WIZARD_INFO
270            .iter()
271            .filter(|info| info.category == category)
272            .collect()
273    }
274}
275
276/// Create a provider from an unresolved provider configuration.
277///
278/// This is a convenience wrapper that first resolves any secret references in the
279/// configuration (using the provided config and profile), then creates the provider.
280///
281/// For providers that don't have any secret references, this is equivalent to calling
282/// `get_provider_from_resolved` directly with a resolved config.
283pub async fn get_provider_resolved(
284    config: &crate::config::Config,
285    profile: &str,
286    provider_name: &str,
287    provider_config: &ProviderConfig,
288) -> Result<Box<dyn Provider>> {
289    let resolved = resolve_provider_config(config, profile, provider_name, provider_config).await?;
290    get_provider_from_resolved(provider_name, &resolved)
291}