Skip to main content

codetether_agent/provider/bedrock/
auth.rs

1//! AWS credential loading and auth mode selection for the Bedrock provider.
2//!
3//! Supports credentials from environment variables, `~/.aws/credentials`, and
4//! region detection via env vars or `~/.aws/config`.
5//!
6//! # Examples
7//!
8//! ```rust,no_run
9//! use codetether_agent::provider::bedrock::AwsCredentials;
10//!
11//! // Load from env vars, falling back to ~/.aws/credentials
12//! let creds = AwsCredentials::from_environment()
13//!     .expect("no AWS credentials found");
14//! let region = AwsCredentials::detect_region()
15//!     .unwrap_or_else(|| "us-east-1".to_string());
16//! assert!(!creds.access_key_id.is_empty());
17//! assert!(!region.is_empty());
18//! ```
19
20/// AWS credentials used for SigV4 signing of Bedrock requests.
21///
22/// Typically constructed via [`AwsCredentials::from_environment`] rather than
23/// directly, so that env vars and the shared credentials file are both
24/// respected.
25///
26/// # Examples
27///
28/// ```rust
29/// use codetether_agent::provider::bedrock::AwsCredentials;
30///
31/// let creds = AwsCredentials {
32///     access_key_id: "AKIA...".to_string(),
33///     secret_access_key: "secret".to_string(),
34///     session_token: None,
35/// };
36/// assert_eq!(creds.access_key_id, "AKIA...");
37/// assert!(creds.session_token.is_none());
38/// ```
39#[derive(Debug, Clone)]
40pub struct AwsCredentials {
41    /// AWS access key ID (e.g. `AKIA...`).
42    pub access_key_id: String,
43    /// AWS secret access key (keep confidential).
44    pub secret_access_key: String,
45    /// Optional session token (present for STS / assumed-role credentials).
46    pub session_token: Option<String>,
47}
48
49impl AwsCredentials {
50    /// Load credentials from `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`
51    /// env vars, then fall back to `~/.aws/credentials` (default or named
52    /// profile via `AWS_PROFILE`).
53    ///
54    /// # Returns
55    ///
56    /// `Some(creds)` on success, `None` if neither env vars nor a readable
57    /// credentials file yielded a complete key pair.
58    ///
59    /// # Examples
60    ///
61    /// ```rust,no_run
62    /// use codetether_agent::provider::bedrock::AwsCredentials;
63    ///
64    /// match AwsCredentials::from_environment() {
65    ///     Some(c) => println!("loaded key id: {}", c.access_key_id),
66    ///     None => eprintln!("no AWS credentials available"),
67    /// }
68    /// ```
69    pub fn from_environment() -> Option<Self> {
70        if let (Ok(key_id), Ok(secret)) = (
71            std::env::var("AWS_ACCESS_KEY_ID"),
72            std::env::var("AWS_SECRET_ACCESS_KEY"),
73        ) && !key_id.is_empty()
74            && !secret.is_empty()
75        {
76            return Some(Self {
77                access_key_id: key_id,
78                secret_access_key: secret,
79                session_token: std::env::var("AWS_SESSION_TOKEN")
80                    .ok()
81                    .filter(|s| !s.is_empty()),
82            });
83        }
84
85        let profile = std::env::var("AWS_PROFILE").unwrap_or_else(|_| "default".to_string());
86        Self::from_credentials_file(&profile)
87    }
88
89    /// Parse `~/.aws/credentials` INI file for the given profile.
90    ///
91    /// # Arguments
92    ///
93    /// * `profile` — Profile name (e.g. `"default"`, `"prod"`).
94    ///
95    /// # Returns
96    ///
97    /// `Some(creds)` if a matching `[profile]` section with both
98    /// `aws_access_key_id` and `aws_secret_access_key` was found.
99    fn from_credentials_file(profile: &str) -> Option<Self> {
100        let home = std::env::var("HOME")
101            .or_else(|_| std::env::var("USERPROFILE"))
102            .ok()?;
103        let path = std::path::Path::new(&home).join(".aws").join("credentials");
104        let content = std::fs::read_to_string(&path).ok()?;
105
106        let section_header = format!("[{profile}]");
107        let mut in_section = false;
108        let mut key_id = None;
109        let mut secret = None;
110        let mut token = None;
111
112        for line in content.lines() {
113            let trimmed = line.trim();
114            if trimmed.starts_with('[') {
115                in_section = trimmed == section_header;
116                continue;
117            }
118            if !in_section {
119                continue;
120            }
121            if let Some((k, v)) = trimmed.split_once('=') {
122                let k = k.trim();
123                let v = v.trim();
124                match k {
125                    "aws_access_key_id" => key_id = Some(v.to_string()),
126                    "aws_secret_access_key" => secret = Some(v.to_string()),
127                    "aws_session_token" => token = Some(v.to_string()),
128                    _ => {}
129                }
130            }
131        }
132
133        Some(Self {
134            access_key_id: key_id?,
135            secret_access_key: secret?,
136            session_token: token,
137        })
138    }
139
140    /// Detect the target AWS region.
141    ///
142    /// Precedence: `AWS_REGION` → `AWS_DEFAULT_REGION` → `~/.aws/config`
143    /// (default or `[profile NAME]` per `AWS_PROFILE`).
144    ///
145    /// # Returns
146    ///
147    /// `Some(region)` on success, `None` if no region could be determined.
148    ///
149    /// # Examples
150    ///
151    /// ```rust,no_run
152    /// use codetether_agent::provider::bedrock::AwsCredentials;
153    ///
154    /// let region = AwsCredentials::detect_region()
155    ///     .unwrap_or_else(|| "us-east-1".to_string());
156    /// assert!(!region.is_empty());
157    /// ```
158    pub fn detect_region() -> Option<String> {
159        if let Ok(r) = std::env::var("AWS_REGION")
160            && !r.is_empty()
161        {
162            return Some(r);
163        }
164        if let Ok(r) = std::env::var("AWS_DEFAULT_REGION")
165            && !r.is_empty()
166        {
167            return Some(r);
168        }
169        let profile = std::env::var("AWS_PROFILE").unwrap_or_else(|_| "default".to_string());
170        let home = std::env::var("HOME")
171            .or_else(|_| std::env::var("USERPROFILE"))
172            .ok()?;
173        let path = std::path::Path::new(&home).join(".aws").join("config");
174        let content = std::fs::read_to_string(&path).ok()?;
175
176        let section_header = if profile == "default" {
177            "[default]".to_string()
178        } else {
179            format!("[profile {profile}]")
180        };
181        let mut in_section = false;
182        for line in content.lines() {
183            let trimmed = line.trim();
184            if trimmed.starts_with('[') {
185                in_section = trimmed == section_header;
186                continue;
187            }
188            if !in_section {
189                continue;
190            }
191            if let Some((k, v)) = trimmed.split_once('=')
192                && k.trim() == "region"
193            {
194                let v = v.trim();
195                if !v.is_empty() {
196                    return Some(v.to_string());
197                }
198            }
199        }
200        None
201    }
202}
203
204/// Authentication mode for the Bedrock provider.
205///
206/// Bedrock supports two auth modes:
207/// - **SigV4**: standard AWS IAM credentials (access key + secret).
208/// - **Bearer token**: an opaque token from an API Gateway fronting Bedrock or
209///   a Vault-managed key dispensary.
210///
211/// # Examples
212///
213/// ```rust
214/// use codetether_agent::provider::bedrock::{AwsCredentials, BedrockAuth};
215///
216/// let sigv4 = BedrockAuth::SigV4(AwsCredentials {
217///     access_key_id: "AKIA...".into(),
218///     secret_access_key: "secret".into(),
219///     session_token: None,
220/// });
221/// let bearer = BedrockAuth::BearerToken("token-abc".into());
222///
223/// match sigv4 {
224///     BedrockAuth::SigV4(_) => (),
225///     BedrockAuth::BearerToken(_) => panic!("unexpected"),
226/// }
227/// match bearer {
228///     BedrockAuth::BearerToken(t) => assert_eq!(t, "token-abc"),
229///     BedrockAuth::SigV4(_) => panic!("unexpected"),
230/// }
231/// ```
232#[derive(Debug, Clone)]
233pub enum BedrockAuth {
234    /// Standard AWS SigV4 signing with IAM credentials.
235    SigV4(AwsCredentials),
236    /// Bearer token (API Gateway or custom auth layer).
237    BearerToken(String),
238}