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}