Skip to content

Commit 289f959

Browse files
authored
Feat: Add api_token field to client configuration (#514)
* Add api_token field to client configuration for reading API tokens from env vars Signed-off-by: Rob Geada <[email protected]> * Move env var read to deserialization Signed-off-by: Rob Geada <[email protected]> --------- Signed-off-by: Rob Geada <[email protected]>
1 parent 947f848 commit 289f959

File tree

4 files changed

+86
-4
lines changed

4 files changed

+86
-4
lines changed

src/clients.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,11 @@ pub async fn create_http_client(
254254
.layer(http_trace_layer())
255255
.layer(TimeoutLayer::new(request_timeout))
256256
.service(client);
257-
Ok(HttpClient::new(base_url, client))
257+
Ok(HttpClient::new(
258+
base_url,
259+
service_config.api_token.clone(),
260+
client,
261+
))
258262
}
259263

260264
pub async fn create_grpc_client<C: Debug + Clone>(

src/clients/http.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,15 +119,17 @@ pub trait HttpClientExt: Client {
119119
pub struct HttpClient {
120120
base_url: Url,
121121
health_url: Url,
122+
api_token: Option<String>,
122123
inner: HttpClientInner,
123124
}
124125

125126
impl HttpClient {
126-
pub fn new(base_url: Url, inner: HttpClientInner) -> Self {
127+
pub fn new(base_url: Url, api_token: Option<String>, inner: HttpClientInner) -> Self {
127128
let health_url = base_url.join("health").unwrap();
128129
Self {
129130
base_url,
130131
health_url,
132+
api_token,
131133
inner,
132134
}
133135
}
@@ -140,6 +142,19 @@ impl HttpClient {
140142
self.base_url.join(path).unwrap()
141143
}
142144

145+
/// Injects the API token as a Bearer token in the Authorization header if configured and present in the environment.
146+
fn inject_api_token(&self, headers: &mut HeaderMap) -> Result<(), Error> {
147+
if let Some(token) = &self.api_token {
148+
headers.insert(
149+
http::header::AUTHORIZATION,
150+
HeaderValue::from_str(&format!("Bearer {}", token)).map_err(|e| Error::Http {
151+
code: StatusCode::INTERNAL_SERVER_ERROR,
152+
message: format!("invalid authorization header: {e}"),
153+
})?,
154+
);
155+
}
156+
Ok(())
157+
}
143158
pub async fn get(
144159
&self,
145160
url: Url,
@@ -162,11 +177,14 @@ impl HttpClient {
162177
&self,
163178
url: Url,
164179
method: Method,
165-
headers: HeaderMap,
180+
mut headers: HeaderMap,
166181
body: impl RequestBody,
167182
) -> Result<Response, Error> {
168183
let ctx = Span::current().context();
184+
185+
self.inject_api_token(&mut headers)?;
169186
let headers = trace::with_traceparent_header(&ctx, headers);
187+
170188
let mut builder = hyper::http::request::Builder::new()
171189
.method(method)
172190
.uri(url.as_uri());

src/config.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ use std::{
2323
use serde::Deserialize;
2424
use tracing::{debug, error, info, warn};
2525

26-
use crate::clients::{chunker::DEFAULT_CHUNKER_ID, is_valid_hostname};
26+
use crate::{
27+
clients::{chunker::DEFAULT_CHUNKER_ID, is_valid_hostname},
28+
utils::from_env,
29+
};
2730

2831
/// Default allowed headers to passthrough to clients.
2932
const DEFAULT_ALLOWED_HEADERS: &[&str] = &[];
@@ -86,6 +89,9 @@ pub struct ServiceConfig {
8689
pub http2_keep_alive_interval: Option<u64>,
8790
/// Keep-alive timeout in seconds for client calls [currently only for grpc generation]
8891
pub keep_alive_timeout: Option<u64>,
92+
/// Name of environment variable that contains the API key to use for this service [currently only for http generation]
93+
#[serde(default, deserialize_with = "from_env")]
94+
pub api_token: Option<String>,
8995
}
9096

9197
impl ServiceConfig {
@@ -101,6 +107,7 @@ impl ServiceConfig {
101107
max_retries: None,
102108
http2_keep_alive_interval: None,
103109
keep_alive_timeout: None,
110+
api_token: None,
104111
}
105112
}
106113
}

src/utils.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,56 @@ where
3434
OneOrMany::Many(values) => Ok(values),
3535
}
3636
}
37+
38+
/// Serde helper to deserialize value from environment variable.
39+
pub fn from_env<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
40+
where
41+
D: Deserializer<'de>,
42+
{
43+
let env_name: Option<String> = Option::deserialize(deserializer)?;
44+
if let Some(env_name) = env_name {
45+
let value = std::env::var(&env_name)
46+
.map_err(|_| serde::de::Error::custom(format!("env var `{env_name}` not found")))?;
47+
Ok(Some(value))
48+
} else {
49+
Ok(None)
50+
}
51+
}
52+
53+
#[cfg(test)]
54+
mod tests {
55+
use serde::Deserialize;
56+
use serde_json::json;
57+
58+
use super::from_env;
59+
60+
#[derive(Debug, Deserialize)]
61+
pub struct Config {
62+
#[serde(default, deserialize_with = "from_env")]
63+
pub api_token: Option<String>,
64+
}
65+
66+
#[test]
67+
fn test_from_env() -> Result<(), Box<dyn std::error::Error>> {
68+
// Test no value
69+
let config: Config = serde_json::from_value(json!({}))?;
70+
assert_eq!(config.api_token, None);
71+
72+
// Test invalid value
73+
let config: Result<Config, serde_json::error::Error> = serde_json::from_value(json!({
74+
"api_token": "DOES_NOT_EXIST"
75+
}));
76+
assert!(config.is_err_and(|err| err.to_string() == "env var `DOES_NOT_EXIST` not found"));
77+
78+
// Test valid value
79+
unsafe {
80+
std::env::set_var("CLIENT_API_TOKEN", "token");
81+
}
82+
let config: Config = serde_json::from_value(json!({
83+
"api_token": "CLIENT_API_TOKEN"
84+
}))?;
85+
assert_eq!(config.api_token, Some("token".into()));
86+
87+
Ok(())
88+
}
89+
}

0 commit comments

Comments
 (0)