Skip to main content

hasp_backend_env/
lib.rs

1//! `env://` backend for hasp.
2//!
3//! Grammar: `env://VAR_NAME` where the host component is the
4//! environment variable name.
5//!
6//! Supported operations:
7//! - `get`: reads `std::env::var(var_name)`
8//! - `exists`: checks `std::env::var(var_name).is_ok()`
9//! - `put`, `list`, `delete`: `UnsupportedOperation`
10//!
11//! Platform-specific failure modes:
12//! - None (stdlib only). `NotFound` is returned when the variable is unset.
13
14use hasp_core::{secret_mem::wrap_secret, Backend, Entry, Error, SecretString};
15use url::Url;
16
17/// URL shape for `env://` addresses.
18///
19/// Host = variable name. No path, no query, no fragment.
20pub struct EnvUrl {
21    pub var_name: String,
22}
23
24impl TryFrom<&Url> for EnvUrl {
25    type Error = Error;
26
27    fn try_from(url: &Url) -> Result<Self, Self::Error> {
28        if url.scheme() != "env" {
29            return Err(Error::InvalidUrl("expected env:// scheme".into()));
30        }
31        let var_name = url
32            .host_str()
33            .ok_or_else(|| Error::InvalidUrl("env:// requires a host (variable name)".into()))?
34            .to_owned();
35        if var_name.is_empty() {
36            return Err(Error::InvalidUrl(
37                "env:// variable name must not be empty".into(),
38            ));
39        }
40        if url.path() != "/" && !url.path().is_empty() {
41            return Err(Error::InvalidUrl("env:// does not accept a path".into()));
42        }
43        if url.query().is_some() {
44            return Err(Error::InvalidUrl(
45                "env:// does not accept query parameters".into(),
46            ));
47        }
48        Ok(EnvUrl { var_name })
49    }
50}
51
52/// Stdlib-only backend that reads secrets from environment variables.
53///
54/// `put` returns `UnsupportedOperation` because a child process cannot
55/// set environment variables in its parent.
56pub struct EnvBackend;
57
58impl Backend for EnvBackend {
59    fn scheme(&self) -> &'static str {
60        "env"
61    }
62
63    fn validate(&self, url: &Url) -> Result<(), Error> {
64        EnvUrl::try_from(url).map(|_| ())
65    }
66
67    fn get(&self, url: &Url) -> Result<SecretString, Error> {
68        let env_url = EnvUrl::try_from(url)?;
69        match std::env::var(&env_url.var_name) {
70            Ok(value) => Ok(wrap_secret(value)),
71            Err(std::env::VarError::NotPresent) => Err(Error::NotFound(format!(
72                "env var '{}' not set",
73                env_url.var_name
74            ))),
75            Err(std::env::VarError::NotUnicode(_)) => Err(Error::Backend {
76                scheme: "env",
77                kind: hasp_core::BackendFailureKind::Permanent,
78                message: format!("env var '{}' is not valid Unicode", env_url.var_name),
79            }),
80        }
81    }
82
83    fn put(&self, _url: &Url, _value: &SecretString) -> Result<(), Error> {
84        Err(Error::UnsupportedOperation {
85            scheme: "env",
86            operation: "put",
87        })
88    }
89
90    fn list(&self, _url: &Url) -> Result<Vec<Entry>, Error> {
91        Err(Error::UnsupportedOperation {
92            scheme: "env",
93            operation: "list",
94        })
95    }
96
97    fn delete(&self, _url: &Url) -> Result<(), Error> {
98        Err(Error::UnsupportedOperation {
99            scheme: "env",
100            operation: "delete",
101        })
102    }
103
104    fn exists(&self, url: &Url) -> Result<bool, Error> {
105        let env_url = EnvUrl::try_from(url)?;
106        Ok(std::env::var(&env_url.var_name).is_ok())
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn parse_valid_url() {
116        let url = Url::parse("env://HOME").unwrap();
117        let env_url = EnvUrl::try_from(&url).unwrap();
118        assert_eq!(env_url.var_name, "HOME");
119    }
120
121    #[test]
122    fn parse_missing_host_fails() {
123        let url = Url::parse("env:///").unwrap();
124        assert!(EnvUrl::try_from(&url).is_err());
125    }
126
127    #[test]
128    fn parse_path_fails() {
129        let url = Url::parse("env://HOME/extra").unwrap();
130        assert!(EnvUrl::try_from(&url).is_err());
131    }
132
133    #[test]
134    fn parse_query_fails() {
135        let url = Url::parse("env://HOME?foo=bar").unwrap();
136        assert!(EnvUrl::try_from(&url).is_err());
137    }
138}