Skip to main content

tsafe_bitwarden/
config.rs

1//! Bitwarden pull configuration.
2
3/// Bitwarden pull configuration.
4///
5/// Auth uses the `client_credentials` grant against the Bitwarden identity
6/// endpoint.  The machine token does **not** carry the master password, so
7/// cipher values returned by the REST sync endpoint are E2E-encrypted and
8/// cannot be decrypted in-process.  This crate uses the `bw` CLI subprocess
9/// to unlock and list items, which handles local decryption after `bw unlock`.
10///
11/// # Environment variables
12///
13/// | Variable                  | Description                                     |
14/// |---------------------------|-------------------------------------------------|
15/// | `TSAFE_BW_CLIENT_ID`      | Bitwarden API client ID (org or personal)       |
16/// | `TSAFE_BW_CLIENT_SECRET`  | Bitwarden API client secret                     |
17/// | `TSAFE_BW_PASSWORD`       | Master password for `bw unlock`                 |
18#[derive(Debug, Clone)]
19pub struct BitwConfig {
20    /// Bitwarden API base URL (default: `https://api.bitwarden.com`).
21    pub api_url: String,
22    /// Bitwarden identity base URL (default: `https://identity.bitwarden.com`).
23    pub identity_url: String,
24    /// OAuth2 client ID.  Maps to `TSAFE_BW_CLIENT_ID` when not set in the manifest.
25    pub client_id: String,
26    /// OAuth2 client secret.  Maps to `TSAFE_BW_CLIENT_SECRET` when not set in the manifest.
27    pub client_secret: String,
28}
29
30impl BitwConfig {
31    /// Construct from explicit values.
32    pub fn new(
33        api_url: impl Into<String>,
34        identity_url: impl Into<String>,
35        client_id: impl Into<String>,
36        client_secret: impl Into<String>,
37    ) -> Self {
38        Self {
39            api_url: api_url.into(),
40            identity_url: identity_url.into(),
41            client_id: client_id.into(),
42            client_secret: client_secret.into(),
43        }
44    }
45
46    /// Construct from environment variables (`TSAFE_BW_CLIENT_ID` / `TSAFE_BW_CLIENT_SECRET`).
47    ///
48    /// Returns `None` if either variable is unset or empty.
49    pub fn from_env() -> Option<Self> {
50        let client_id = std::env::var("TSAFE_BW_CLIENT_ID")
51            .ok()
52            .filter(|s| !s.is_empty())?;
53        let client_secret = std::env::var("TSAFE_BW_CLIENT_SECRET")
54            .ok()
55            .filter(|s| !s.is_empty())?;
56        Some(Self {
57            api_url: std::env::var("TSAFE_BW_API_URL")
58                .unwrap_or_else(|_| "https://api.bitwarden.com".into()),
59            identity_url: std::env::var("TSAFE_BW_IDENTITY_URL")
60                .unwrap_or_else(|_| "https://identity.bitwarden.com".into()),
61            client_id,
62            client_secret,
63        })
64    }
65
66    /// Default cloud API URL.
67    pub fn default_api_url() -> &'static str {
68        "https://api.bitwarden.com"
69    }
70
71    /// Default cloud identity URL.
72    pub fn default_identity_url() -> &'static str {
73        "https://identity.bitwarden.com"
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn new_stores_fields() {
83        let cfg = BitwConfig::new(
84            "https://api.example.com",
85            "https://identity.example.com",
86            "org.abc123",
87            "secret-xyz",
88        );
89        assert_eq!(cfg.api_url, "https://api.example.com");
90        assert_eq!(cfg.identity_url, "https://identity.example.com");
91        assert_eq!(cfg.client_id, "org.abc123");
92        assert_eq!(cfg.client_secret, "secret-xyz");
93    }
94
95    #[test]
96    fn from_env_returns_none_when_vars_absent() {
97        temp_env::with_vars(
98            [
99                ("TSAFE_BW_CLIENT_ID", None::<&str>),
100                ("TSAFE_BW_CLIENT_SECRET", None::<&str>),
101            ],
102            || {
103                assert!(BitwConfig::from_env().is_none());
104            },
105        );
106    }
107
108    #[test]
109    fn from_env_uses_client_id_and_secret() {
110        temp_env::with_vars(
111            [
112                ("TSAFE_BW_CLIENT_ID", Some("org.testid")),
113                ("TSAFE_BW_CLIENT_SECRET", Some("testsecret")),
114                ("TSAFE_BW_API_URL", None::<&str>),
115                ("TSAFE_BW_IDENTITY_URL", None::<&str>),
116            ],
117            || {
118                let cfg = BitwConfig::from_env().expect("should build from env");
119                assert_eq!(cfg.client_id, "org.testid");
120                assert_eq!(cfg.client_secret, "testsecret");
121                assert_eq!(cfg.api_url, "https://api.bitwarden.com");
122                assert_eq!(cfg.identity_url, "https://identity.bitwarden.com");
123            },
124        );
125    }
126
127    #[test]
128    fn from_env_uses_custom_urls_when_set() {
129        temp_env::with_vars(
130            [
131                ("TSAFE_BW_CLIENT_ID", Some("myid")),
132                ("TSAFE_BW_CLIENT_SECRET", Some("mysecret")),
133                ("TSAFE_BW_API_URL", Some("https://vaultwarden.internal/api")),
134                (
135                    "TSAFE_BW_IDENTITY_URL",
136                    Some("https://vaultwarden.internal/identity"),
137                ),
138            ],
139            || {
140                let cfg = BitwConfig::from_env().expect("should build from env");
141                assert_eq!(cfg.api_url, "https://vaultwarden.internal/api");
142                assert_eq!(cfg.identity_url, "https://vaultwarden.internal/identity");
143            },
144        );
145    }
146}