vaultrs/
client.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
use crate::api::AuthInfo;
use crate::api::{token::responses::LookupTokenResponse, EndpointMiddleware};
use crate::error::ClientError;
use async_trait::async_trait;
pub use reqwest::Identity;
use rustify::clients::reqwest::Client as HTTPClient;
use std::time::Duration;
use std::{env, fs};
use url::Url;

/// Valid URL schemes that can be used for a Vault server address
const VALID_SCHEMES: [&str; 2] = ["http", "https"];

/// The client interface capabale of interacting with API functions
#[async_trait]
pub trait Client: Send + Sync + Sized {
    /// Returns the underlying HTTP client being used for API calls
    fn http(&self) -> &HTTPClient;

    /// Returns the middleware to be used when executing API calls
    fn middle(&self) -> &EndpointMiddleware;

    /// Returns the settings used to configure this client
    fn settings(&self) -> &VaultClientSettings;

    /// Sets the underlying token for this client
    fn set_token(&mut self, token: &str);

    /// Looks up the current token being used by this client
    async fn lookup(&self) -> Result<LookupTokenResponse, ClientError> {
        crate::token::lookup_self(self).await
    }

    /// Renews the current token being used by this client
    async fn renew(&self, increment: Option<&str>) -> Result<AuthInfo, ClientError> {
        crate::token::renew_self(self, increment).await
    }

    /// Revokes the current token being used by this client
    async fn revoke(&self) -> Result<(), ClientError> {
        crate::token::revoke_self(self).await
    }

    /// Returns the status of the configured Vault server
    async fn status(&self) -> Result<crate::sys::ServerStatus, ClientError> {
        crate::sys::status(self).await
    }
}

/// A client which can be used to execute calls against a Vault server.
///
/// A vault client is configured using [VaultClientSettings] and will
/// automatically configure a backing instance of a [HTTPClient] which is
/// used for executing [Endpoints][rustify::endpoint::Endpoint].
pub struct VaultClient {
    pub http: HTTPClient,
    pub middle: EndpointMiddleware,
    pub settings: VaultClientSettings,
}

#[async_trait]
impl Client for VaultClient {
    fn http(&self) -> &HTTPClient {
        &self.http
    }

    fn middle(&self) -> &EndpointMiddleware {
        &self.middle
    }

    fn settings(&self) -> &VaultClientSettings {
        &self.settings
    }

    fn set_token(&mut self, token: &str) {
        self.settings.token = token.to_string();
        self.middle.token = token.to_string();
    }
}

impl VaultClient {
    /// Creates a new [VaultClient] using the given [VaultClientSettings].
    #[instrument(skip(settings), err)]
    pub fn new(settings: VaultClientSettings) -> Result<VaultClient, ClientError> {
        let mut http_client = reqwest::ClientBuilder::new();

        // Optionally set timeout on client
        http_client = if let Some(timeout) = settings.timeout {
            http_client.timeout(timeout)
        } else {
            http_client
        };

        // Disable TLS checks if specified
        if !settings.verify {
            event!(tracing::Level::WARN, "Disabling TLS verification");
        }
        http_client = http_client.danger_accept_invalid_certs(!settings.verify);

        // Adds CA certificates
        for path in &settings.ca_certs {
            let content = std::fs::read(path).map_err(|e| ClientError::FileReadError {
                source: e,
                path: path.clone(),
            })?;
            let cert = reqwest::Certificate::from_pem(&content).map_err(|e| {
                ClientError::ParseCertificateError {
                    source: e,
                    path: path.clone(),
                }
            })?;

            info!("Importing CA certificate from {}", path);
            http_client = http_client.add_root_certificate(cert);
        }

        // Adds client certificates
        if let Some(identity) = &settings.identity {
            http_client = http_client.identity(identity.clone());
        }

        // Configures middleware for endpoints to append API version and token
        debug!("Using API version {}", settings.version);
        let version_str = format!("v{}", settings.version);
        let middle = EndpointMiddleware {
            token: settings.token.clone(),
            version: version_str,
            wrap: None,
            namespace: settings.namespace.clone(),
        };

        let http_client = http_client
            .build()
            .map_err(|e| ClientError::RestClientBuildError { source: e })?;
        let http = HTTPClient::new(settings.address.as_str(), http_client);
        Ok(VaultClient {
            settings,
            middle,
            http,
        })
    }
}

/// Contains settings for configuring a [VaultClient].
///
/// Most settings that are not directly configured will have their default value
/// pulled from their respective environment variables. Specifically:
///
/// * `address`: VAULT_ADDR
/// * `ca_certs: VAULT_CACERT / VAULT_CAPATH
/// * `token`: VAULT_TOKEN
/// * verify`: VAULT_SKIP_VERIFY
///
/// The `address` is validated when the settings are built and will throw an
/// error if the format is invalid.
#[derive(Builder, Clone, Debug)]
#[builder(build_fn(validate = "Self::validate"))]
pub struct VaultClientSettings {
    #[builder(setter(custom), default = "self.default_address()?")]
    pub address: Url,
    #[builder(default = "self.default_ca_certs()")]
    pub ca_certs: Vec<String>,
    #[builder(default = "self.default_identity()")]
    pub identity: Option<Identity>,
    #[builder(default)]
    pub timeout: Option<Duration>,
    #[builder(setter(into), default = "self.default_token()")]
    pub token: String,
    #[builder(default = "self.default_verify()")]
    pub verify: bool,
    #[builder(setter(into, strip_option), default = "1")]
    pub version: u8,
    #[builder(default = "false")]
    pub wrapping: bool,
    #[builder(default)]
    pub namespace: Option<String>,
}

impl VaultClientSettingsBuilder {
    /// Set an address for vault. Note that if not set, it will default
    /// to the `VAULT_ADDR` environment variable and if that is not set either,
    /// it will default to `http://127.0.0.1:8200`.
    ///
    /// # Panics
    ///
    /// The setter will panic if the address given contains an invalid URL format.
    pub fn address<T>(&mut self, address: T) -> &mut Self
    where
        T: AsRef<str>,
    {
        let url = Url::parse(address.as_ref())
            .map_err(|_| format!("Invalid URL format: {}", address.as_ref()))
            .unwrap();
        self.address = Some(url);
        self
    }

    pub fn set_namespace(&mut self, str: String) -> &mut Self {
        self.namespace = Some(Some(str));
        self
    }

    fn default_address(&self) -> Result<Url, String> {
        let address = if let Ok(address) = env::var("VAULT_ADDR") {
            info!("Using vault address from $VAULT_ADDR: {address}");
            address
        } else {
            info!("Using default vault address http://127.0.0.1:8200");
            String::from("http://127.0.0.1:8200")
        };
        let url = Url::parse(&address);
        let url = url.map_err(|_| format!("Invalid URL format: {}", &address))?;
        // validation in derive_builder does not happen for defaults,
        // so we need to do it ourselves, here:
        self.validate_url(&url)?;
        Ok(url)
    }

    fn default_token(&self) -> String {
        match env::var("VAULT_TOKEN") {
            Ok(s) => {
                info!("Using vault token from $VAULT_TOKEN");
                s
            }
            Err(_) => {
                info!("Using default empty vault token");
                String::from("")
            }
        }
    }

    fn default_verify(&self) -> bool {
        info!("Checking TLS verification using $VAULT_SKIP_VERIFY");
        match env::var("VAULT_SKIP_VERIFY") {
            Ok(value) => !matches!(value.to_lowercase().as_str(), "0" | "f" | "false"),
            Err(_) => true,
        }
    }

    fn default_ca_certs(&self) -> Vec<String> {
        let mut paths: Vec<String> = Vec::new();

        if let Ok(s) = env::var("VAULT_CACERT") {
            info!("Found CA certificate in $VAULT_CACERT");
            paths.push(s);
        }

        if let Ok(s) = env::var("VAULT_CAPATH") {
            info!("Found CA certificate path in $VAULT_CAPATH");
            if let Ok(p) = fs::read_dir(s) {
                for path in p {
                    paths.push(path.unwrap().path().to_str().unwrap().to_string())
                }
            }
        }

        paths
    }

    fn default_identity(&self) -> Option<reqwest::Identity> {
        // Default value can be set from environment
        let env_client_cert = env::var("VAULT_CLIENT_CERT").unwrap_or_default();
        let env_client_key = env::var("VAULT_CLIENT_KEY").unwrap_or_default();

        if env_client_cert.is_empty() || env_client_key.is_empty() {
            debug!("No client certificate (env VAULT_CLIENT_CERT & VAULT_CLIENT_KEY are not set)");
            return None;
        }

        #[cfg(feature = "rustls")]
        {
            let mut client_cert = match fs::read(&env_client_cert) {
                Ok(content) => content,
                Err(err) => {
                    error!("error reading client cert '{}': {}", env_client_cert, err);
                    return None;
                }
            };

            let mut client_key = match fs::read(&env_client_key) {
                Ok(content) => content,
                Err(err) => {
                    error!("error reading client key '{}': {}", env_client_key, err);
                    return None;
                }
            };

            // concat certificate and key
            client_cert.append(&mut client_key);

            match reqwest::Identity::from_pem(&client_cert) {
                Ok(pkcs8) => return Some(pkcs8),
                Err(err) => error!("error creating identity: {}", err),
            };
        }

        #[cfg(feature = "native-tls")]
        {
            error!("Client certificates not implemented for native-tls");
        }

        None
    }

    fn validate(&self) -> Result<(), String> {
        // Verify URL is valid
        if let Some(url) = &self.address {
            self.validate_url(url)
        } else {
            Ok(())
        }
    }

    fn validate_url(&self, url: &Url) -> Result<(), String> {
        // Verify scheme is valid HTTP endpoint
        if !VALID_SCHEMES.contains(&url.scheme()) {
            Err(format!("Invalid scheme for HTTP URL: {}", url.scheme()))
        } else {
            Ok(())
        }
    }
}