1use std::sync::Arc;
2use std::time::{Duration, SystemTime, UNIX_EPOCH};
3
4use async_trait::async_trait;
5use base64::{
6 Engine as _, engine::general_purpose::STANDARD, engine::general_purpose::URL_SAFE_NO_PAD,
7};
8use reqwest::{Client, blocking::Client as BlockingClient};
9use rsa::RsaPrivateKey;
10use rsa::pkcs8::{DecodePrivateKey, EncodePrivateKey, EncodePublicKey, LineEnding};
11use rsa::rand_core::OsRng;
12use serde::Deserialize;
13use sha1::Sha1;
14use sha2::Digest;
15use tokio::sync::Mutex;
16use x509_parser::{parse_x509_certificate, pem::parse_x509_pem};
17
18use crate::auth::providers::{OciAuthProvider, SignRequest, SignedHeaders};
19use crate::client::signer::OciSigner;
20use crate::error::{Error, Result};
21
22pub(crate) const DEFAULT_METADATA_BASE_URL: &str = "http://169.254.169.254/opc/v2";
23pub(crate) const DEFAULT_REALM_DOMAIN_COMPONENT: &str = "oraclecloud.com";
24const METADATA_AUTHORIZATION: &str = "Bearer Oracle";
25const REGION_INFO_PATH: &str = "/instance/regionInfo";
26const LEAF_CERTIFICATE_PATH: &str = "/identity/cert.pem";
27const LEAF_PRIVATE_KEY_PATH: &str = "/identity/key.pem";
28const INTERMEDIATE_CERTIFICATE_PATH: &str = "/identity/intermediate.pem";
29const DEFAULT_REFRESH_WINDOW_SECS: u64 = 300;
30const TENANCY_PREFIX: &str = "opc-tenant:";
31const IDENTITY_PREFIX: &str = "opc-identity:";
32
33#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
34pub struct MetadataRegionInfo {
35 #[serde(rename = "regionIdentifier")]
36 pub region_identifier: String,
37 #[serde(rename = "realmDomainComponent")]
38 pub realm_domain_component: String,
39}
40
41#[derive(Debug, Clone)]
42pub struct InstancePrincipalConfig {
43 pub region: String,
44 pub tenancy_id: String,
45 pub realm_domain_component: String,
46 pub metadata_base_url: String,
47 pub refresh_window: Duration,
48 pub auth_scheme: String,
49 pub auth_host_override: Option<String>,
50}
51
52impl InstancePrincipalConfig {
53 pub fn new(region: impl Into<String>, tenancy_id: impl Into<String>) -> Self {
54 Self {
55 region: region.into(),
56 tenancy_id: tenancy_id.into(),
57 realm_domain_component: DEFAULT_REALM_DOMAIN_COMPONENT.to_owned(),
58 metadata_base_url: DEFAULT_METADATA_BASE_URL.to_owned(),
59 refresh_window: Duration::from_secs(DEFAULT_REFRESH_WINDOW_SECS),
60 auth_scheme: "https".to_owned(),
61 auth_host_override: None,
62 }
63 }
64
65 pub fn metadata_base_url(mut self, metadata_base_url: impl Into<String>) -> Self {
66 self.metadata_base_url = metadata_base_url.into();
67 self
68 }
69
70 pub fn realm_domain_component(mut self, realm_domain_component: impl Into<String>) -> Self {
71 self.realm_domain_component = realm_domain_component.into();
72 self
73 }
74
75 pub fn refresh_window(mut self, refresh_window: Duration) -> Self {
76 self.refresh_window = refresh_window;
77 self
78 }
79
80 pub fn auth_scheme(mut self, auth_scheme: impl Into<String>) -> Self {
81 self.auth_scheme = auth_scheme.into();
82 self
83 }
84
85 pub fn auth_host_override(mut self, auth_host_override: impl Into<String>) -> Self {
86 self.auth_host_override = Some(auth_host_override.into());
87 self
88 }
89}
90
91#[derive(Clone)]
92pub struct InstancePrincipalAuthProvider {
93 client: Client,
94 config: InstancePrincipalConfig,
95 state: Arc<Mutex<Option<InstancePrincipalState>>>,
96}
97
98struct InstancePrincipalState {
99 signer: OciSigner,
100 expires_at: SystemTime,
101}
102
103#[derive(Deserialize)]
104struct FederationResponse {
105 token: String,
106}
107
108impl InstancePrincipalAuthProvider {
109 pub fn new(client: Client, config: InstancePrincipalConfig) -> Self {
110 Self {
111 client,
112 config,
113 state: Arc::new(Mutex::new(None)),
114 }
115 }
116
117 async fn ensure_state(&self) -> Result<OciSigner> {
118 let mut guard = self.state.lock().await;
119
120 if let Some(state) = guard.as_ref() {
121 let refresh_at = state
122 .expires_at
123 .checked_sub(self.config.refresh_window)
124 .unwrap_or(UNIX_EPOCH);
125 if SystemTime::now() < refresh_at {
126 return Ok(state.signer.clone());
127 }
128 }
129
130 let state = self.refresh_state().await?;
131 let signer = state.signer.clone();
132 *guard = Some(state);
133 Ok(signer)
134 }
135
136 async fn refresh_state(&self) -> Result<InstancePrincipalState> {
137 let metadata = self.fetch_metadata_materials().await?;
138 let session_private_key_pem = new_session_private_key_pem()?;
139 let session_public_key = session_public_key_pem(&session_private_key_pem)?;
140
141 let auth_key_id = format!(
142 "{}/fed-x509/{}",
143 self.config.tenancy_id,
144 certificate_fingerprint(&metadata.leaf_certificate)?
145 );
146 let auth_signer = OciSigner::new_with_key_id(auth_key_id, &metadata.leaf_private_key)?;
147
148 let request_body = serde_json::json!({
149 "certificate": sanitize_pem_body(&metadata.leaf_certificate),
150 "publicKey": sanitize_pem_body(&session_public_key),
151 "intermediateCertificates": [sanitize_pem_body(&metadata.intermediate_certificate)],
152 });
153 let body_json = serde_json::to_string(&request_body)?;
154 let path = "/v1/x509";
155 let host = self.auth_host();
156 let signed = auth_signer.sign_request_headers(
157 "POST",
158 path,
159 None,
160 Some(&body_json),
161 Some("application/json"),
162 None,
163 )?;
164
165 let response = self
166 .client
167 .post(format!("{}://{host}{path}", self.config.auth_scheme))
168 .header("date", &signed.date)
169 .header("authorization", &signed.authorization)
170 .header(
171 "content-type",
172 signed
173 .content_type
174 .unwrap_or_else(|| "application/json".to_owned()),
175 )
176 .header(
177 "content-length",
178 signed
179 .content_length
180 .unwrap_or_else(|| body_json.len().to_string()),
181 )
182 .header(
183 "x-content-sha256",
184 signed
185 .x_content_sha256
186 .ok_or_else(|| Error::AuthError("Missing x-content-sha256".to_owned()))?,
187 )
188 .body(body_json)
189 .send()
190 .await?;
191
192 if !response.status().is_success() {
193 let status = response.status();
194 let body = response.text().await?;
195 return Err(Error::ApiError {
196 code: status.to_string(),
197 message: body,
198 });
199 }
200
201 let FederationResponse { token } = response.json().await?;
202 let expires_at = jwt_expiration(&token)?;
203 let session_signer =
204 OciSigner::new_with_key_id(format!("ST${token}"), &session_private_key_pem)?;
205
206 Ok(InstancePrincipalState {
207 signer: session_signer,
208 expires_at,
209 })
210 }
211
212 async fn fetch_metadata_materials(&self) -> Result<MetadataMaterials> {
213 let leaf_certificate = self.fetch_metadata_text(LEAF_CERTIFICATE_PATH).await?;
214 let leaf_private_key = self.fetch_metadata_text(LEAF_PRIVATE_KEY_PATH).await?;
215 let intermediate_certificate = self
216 .fetch_metadata_text(INTERMEDIATE_CERTIFICATE_PATH)
217 .await?;
218
219 Ok(MetadataMaterials {
220 leaf_certificate,
221 leaf_private_key,
222 intermediate_certificate,
223 })
224 }
225
226 async fn fetch_metadata_text(&self, path: &str) -> Result<String> {
227 let response = self
228 .client
229 .get(format!("{}{}", self.config.metadata_base_url, path))
230 .header("authorization", METADATA_AUTHORIZATION)
231 .send()
232 .await?;
233
234 if !response.status().is_success() {
235 let status = response.status();
236 let body = response.text().await?;
237 return Err(Error::ApiError {
238 code: status.to_string(),
239 message: body,
240 });
241 }
242
243 response.text().await.map_err(Into::into)
244 }
245
246 pub async fn metadata_region(client: &Client, metadata_base_url: &str) -> Result<String> {
247 let region_info = Self::metadata_region_info(client, metadata_base_url).await?;
248 Ok(region_info.region_identifier)
249 }
250
251 pub async fn metadata_region_info(
252 client: &Client,
253 metadata_base_url: &str,
254 ) -> Result<MetadataRegionInfo> {
255 let response = client
256 .get(format!("{metadata_base_url}{REGION_INFO_PATH}"))
257 .header("authorization", METADATA_AUTHORIZATION)
258 .send()
259 .await?;
260
261 if !response.status().is_success() {
262 let status = response.status();
263 let body = response.text().await?;
264 return Err(Error::ApiError {
265 code: status.to_string(),
266 message: body,
267 });
268 }
269
270 response.json().await.map_err(Into::into)
271 }
272
273 pub(crate) fn metadata_region_info_blocking(
274 client: &BlockingClient,
275 metadata_base_url: &str,
276 ) -> Result<MetadataRegionInfo> {
277 let response = client
278 .get(format!("{metadata_base_url}{REGION_INFO_PATH}"))
279 .header("authorization", METADATA_AUTHORIZATION)
280 .send()?;
281
282 if !response.status().is_success() {
283 let status = response.status();
284 let body = response.text()?;
285 return Err(Error::ApiError {
286 code: status.to_string(),
287 message: body,
288 });
289 }
290
291 response.json().map_err(Into::into)
292 }
293
294 pub(crate) fn tenancy_id_from_metadata_certificate_blocking(
295 client: &BlockingClient,
296 metadata_base_url: &str,
297 ) -> Result<String> {
298 let certificate_pem =
299 Self::metadata_text_blocking(client, metadata_base_url, LEAF_CERTIFICATE_PATH)?;
300 tenancy_id_from_certificate(&certificate_pem)
301 }
302
303 fn metadata_text_blocking(
304 client: &BlockingClient,
305 metadata_base_url: &str,
306 path: &str,
307 ) -> Result<String> {
308 let response = client
309 .get(format!("{metadata_base_url}{path}"))
310 .header("authorization", METADATA_AUTHORIZATION)
311 .send()?;
312
313 if !response.status().is_success() {
314 let status = response.status();
315 let body = response.text()?;
316 return Err(Error::ApiError {
317 code: status.to_string(),
318 message: body,
319 });
320 }
321
322 response.text().map_err(Into::into)
323 }
324
325 fn auth_host(&self) -> String {
326 self.config.auth_host_override.clone().unwrap_or_else(|| {
327 format!(
328 "auth.{}.{}",
329 self.config.region, self.config.realm_domain_component
330 )
331 })
332 }
333}
334
335#[async_trait]
336impl OciAuthProvider for InstancePrincipalAuthProvider {
337 async fn sign(&self, request: &SignRequest<'_>) -> Result<SignedHeaders> {
338 let signer = self.ensure_state().await?;
339 let signed = signer.sign_request_headers(
340 request.method,
341 request.path,
342 request.host,
343 request.body,
344 request.content_type,
345 None,
346 )?;
347
348 Ok(SignedHeaders {
349 date: signed.date,
350 authorization: signed.authorization,
351 content_type: signed.content_type,
352 content_length: signed.content_length,
353 x_content_sha256: signed.x_content_sha256,
354 extra_headers: Vec::new(),
355 })
356 }
357}
358
359struct MetadataMaterials {
360 leaf_certificate: String,
361 leaf_private_key: String,
362 intermediate_certificate: String,
363}
364
365fn sanitize_pem_body(value: &str) -> String {
366 value
367 .replace("-----BEGIN CERTIFICATE-----", "")
368 .replace("-----END CERTIFICATE-----", "")
369 .replace("-----BEGIN PUBLIC KEY-----", "")
370 .replace("-----END PUBLIC KEY-----", "")
371 .replace('\n', "")
372}
373
374fn certificate_fingerprint(certificate_pem: &str) -> Result<String> {
375 let der_body = sanitize_pem_body(certificate_pem);
376 let der = STANDARD
377 .decode(der_body)
378 .map_err(|e| Error::AuthError(format!("Failed to decode certificate: {e}")))?;
379 let digest = Sha1::digest(der);
380 Ok(digest
381 .iter()
382 .map(|byte| format!("{byte:02X}"))
383 .collect::<Vec<_>>()
384 .join(":"))
385}
386
387fn new_session_private_key_pem() -> Result<String> {
388 let private_key = RsaPrivateKey::new(&mut OsRng, 2048)
389 .map_err(|e| Error::AuthError(format!("Failed to generate session key: {e}")))?;
390 private_key
391 .to_pkcs8_pem(LineEnding::LF)
392 .map(|pem| pem.to_string())
393 .map_err(|e| Error::AuthError(format!("Failed to encode session key: {e}")))
394}
395
396fn session_public_key_pem(private_key_pem: &str) -> Result<String> {
397 let private_key = RsaPrivateKey::from_pkcs8_pem(private_key_pem)
398 .map_err(|e| Error::AuthError(format!("Failed to parse session key: {e}")))?;
399 private_key
400 .to_public_key()
401 .to_public_key_pem(LineEnding::LF)
402 .map_err(|e| Error::AuthError(format!("Failed to encode session public key: {e}")))
403}
404
405fn jwt_expiration(token: &str) -> Result<SystemTime> {
406 let payload = token
407 .split('.')
408 .nth(1)
409 .ok_or_else(|| Error::AuthError("Security token payload is missing".to_owned()))?;
410 let decoded = URL_SAFE_NO_PAD
411 .decode(payload)
412 .map_err(|e| Error::AuthError(format!("Failed to decode security token: {e}")))?;
413 let value: serde_json::Value = serde_json::from_slice(&decoded)?;
414 let exp = value
415 .get("exp")
416 .and_then(|value| value.as_u64())
417 .ok_or_else(|| Error::AuthError("Security token exp claim is missing".to_owned()))?;
418 Ok(UNIX_EPOCH + Duration::from_secs(exp))
419}
420
421fn tenancy_id_from_certificate(certificate_pem: &str) -> Result<String> {
422 let (_, pem) = parse_x509_pem(certificate_pem.as_bytes())
423 .map_err(|e| Error::AuthError(format!("Failed to parse certificate PEM: {e}")))?;
424 let (_, certificate) = parse_x509_certificate(&pem.contents)
425 .map_err(|e| Error::AuthError(format!("Failed to parse certificate DER: {e}")))?;
426
427 let mut fallback: Option<String> = None;
428 for attribute in certificate.subject().iter_attributes() {
429 let value = attribute
430 .as_str()
431 .map_err(|e| Error::AuthError(format!("Failed to decode certificate subject: {e}")))?;
432 if let Some(tenancy_id) = value.strip_prefix(TENANCY_PREFIX) {
433 return Ok(tenancy_id.to_owned());
434 }
435 if let Some(tenancy_id) = value.strip_prefix(IDENTITY_PREFIX) {
436 fallback = Some(tenancy_id.to_owned());
437 }
438 }
439
440 fallback.ok_or_else(|| {
441 Error::AuthError(
442 "Certificate subject does not contain an opc-tenant or opc-identity value".to_owned(),
443 )
444 })
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450
451 use mockito::{Matcher, Server};
452 use rsa::pkcs8::EncodePrivateKey;
453
454 const TENANT_CERT_PEM: &str = "-----BEGIN CERTIFICATE-----\n\
455MIIDXzCCAkegAwIBAgIUONFqOCNE1N3Aps1ZQaPpY7SQzngwDQYJKoZIhvcNAQEL\n\
456BQAwPzEuMCwGA1UECgwlb3BjLXRlbmFudDpvY2lkMS50ZW5hbnR5Lm9jMS4uZXhh\n\
457bXBsZTENMAsGA1UEAwwEdGVzdDAeFw0yNjA1MTEwNjQ1NTFaFw0yNjA1MTIwNjQ1\n\
458NTFaMD8xLjAsBgNVBAoMJW9wYy10ZW5hbnQ6b2NpZDEudGVuYW5jeS5vYzEuLmV4\n\
459YW1wbGUxDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\n\
460AoIBAQDMblfnza9gqREWumv1mTJbR939nQIYZUynTxusVBXciNRjKaqB0jFSUFg9\n\
461E2pwtr7G/zr6rpIum9yaRT3O/hhIACP7CJvOoIPTV8qDmNcRnlT78nWBN8jnma1A\n\
462T9AZhtR14BJVe03eSSHBTnIDNNDQZu1+p6hUiGPVG1xe/F3/HOwbUrxzsChDnliZ\n\
463C46FL0JMIu/uH/Q/iSg0wYsJQKzE+iIvLo5edTeaTvdaTth8XLmltWM2DEwC/fyU\n\
464D2lxoOmvBhCVl1OCvT3Db0hMXRVV79BAXNS+qUyKbWnAgkiAMDGmEtYzizAoqCl4\n\
465GpDeqNfSI/xo8Zt1RqU1PgleQslDAgMBAAGjUzBRMB0GA1UdDgQWBBRnTn//hXKL\n\
466fWGEt7RY27CGihg+DjAfBgNVHSMEGDAWgBRnTn//hXKLfWGEt7RY27CGihg+DjAP\n\
467BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAwRR1OsfwCP1UF4PWK\n\
468jQLcBHrwEL7q9/HG47G6IsD4YN365ZPKzv7cOVzL7sPXVs18f3XDZwVNhwMiP2lo\n\
469ShLlHDIog2ZMD0kppoZlwf1EdbVVOr30qtHaRpd1/YHY1omuUCdis51iJzO/wMwL\n\
470m3yCFx7OCb46vCHwWc+CwiF9I9HKFMJyVpmhsEw91EPH3JaHWW1wn/RSIXuWpX0Q\n\
471t+CmwNhI9TC99JL2cfr5lFUjA8nQ5Xx68L9gyfQZ2aicx5XD+s+nt0mgc06oOWv3\n\
472ubYEGH/Vy8oK3rEoKdcNVdZUTgA0Fs2g+ItlrBFsJl5A1/TP3f0fbV6j9eY2SpdB\n\
473Eo34\n\
474-----END CERTIFICATE-----\n";
475 const IDENTITY_CERT_PEM: &str = "-----BEGIN CERTIFICATE-----\n\
476MIIDZTCCAk2gAwIBAgIUMOZAko5vvssEkoQ2WHQPY7f9x7gwDQYJKoZIhvcNAQEL\n\
477BQAwQjExMC8GA1UECgwob3BjLWlkZW50aXR5Om9jaWQxLnRlbmFuY3kub2MxLi5m\n\
478YWxsYmFjazENMAsGA1UEAwwEdGVzdDAeFw0yNjA1MTEwNjQ1NTFaFw0yNjA1MTIw\n\
479NjQ1NTFaMEIxMTAvBgNVBAoMKG9wYy1pZGVudGl0eTpvY2lkMS50ZW5hbmN5Lm9j\n\
480MS4uZmFsbGJhY2sxDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IB\n\
481DwAwggEKAoIBAQDmduilwMQ6tEwB/vyl2mtYWJ3H08t444tmq2vxpFl4XUlPT4S4\n\
482A3tdME87tmZdC0e4f5lUnEo+ZVO9H2pXdPP6pD0sBdvPxJ/FBZtTCCQiA4p9TSVR\n\
483grBXJFd9sNGff7Og6HVdWlTt0fj0K3MlBxg4Tae3+Dzlt7qOJ5xE88Fwh5agOxbS\n\
484vvHwKmAOkW47ArK/cIBv8LzJotINAdMhKykBuFRxc9WwIUWSbNQvYYeFu3YD3Ny1\n\
485v8qwbYPVC2HU/3M8SJmQmAbDgWFw1onqWk94fzoVenwdb7uS7fJtkjf7MppyMtx0\n\
486PhgPTt6al22K6sJvKOlN/lkFQ1DwzQqpPYNFAgMBAAGjUzBRMB0GA1UdDgQWBBRe\n\
487VO8o3p/eN6Qak29wCTCQAAnxqTAfBgNVHSMEGDAWgBReVO8o3p/eN6Qak29wCTCQ\n\
488AAnxqTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBCg/E2AjS7\n\
489cMz1GMGEy9zmpJ1OhD0lksrPHZpfp/LyfCiI677HSIKlBxCKjq720CZM5jAqw8eU\n\
490CsLG8fqtBqOmc6lH/h+3LjGMQsnTjNW7e9sX3rzOyfGblrOX+cVpYYXjUVxtJwS3\n\
491p62tIXpRa/waFgKfYyFv3QHFK//QW1ZVeklnIVJ1sTLgMfRmf6inGp51R5x/aclY\n\
492WdHlZRZUqf8KtLhLE+yevBpZh9YRvfIWvCYoNU4PF6c5XhPo6Q1jqzYKwkxVAKvR\n\
493Sp5TG8PoJmFKTSFP71z+N5kIy2Ez7h1YjBfU+46dGJMuIOAdF7fttUj4wjtd0xo8\n\
494tOmUqakVOgtb\n\
495-----END CERTIFICATE-----\n";
496
497 fn test_private_key_pem() -> String {
498 RsaPrivateKey::new(&mut OsRng, 2048)
499 .unwrap()
500 .to_pkcs8_pem(LineEnding::LF)
501 .unwrap()
502 .to_string()
503 }
504
505 fn test_certificate_pem(label: &str) -> String {
506 format!(
507 "-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----\n",
508 STANDARD.encode(label.as_bytes())
509 )
510 }
511
512 fn test_jwt(exp: u64) -> String {
513 let header = URL_SAFE_NO_PAD.encode(r#"{"alg":"RS256","typ":"JWT"}"#);
514 let payload =
515 URL_SAFE_NO_PAD.encode(format!(r#"{{"exp":{exp},"sub":"instance-principal"}}"#));
516 format!("{header}.{payload}.signature")
517 }
518
519 fn strip_http_scheme(url: &str) -> String {
520 url.trim_start_matches("http://").to_owned()
521 }
522
523 #[tokio::test]
524 async fn test_metadata_region_fetches_imds_value() {
525 let mut server = Server::new_async().await;
526 let _mock = server
527 .mock("GET", "/opc/v2/instance/regionInfo")
528 .match_header("authorization", METADATA_AUTHORIZATION)
529 .with_status(200)
530 .with_body(
531 r#"{"realmKey":"oc1","realmDomainComponent":"oraclecloud.com","regionKey":"ICN","regionIdentifier":"ap-seoul-1"}"#,
532 )
533 .create_async()
534 .await;
535
536 let region = InstancePrincipalAuthProvider::metadata_region(
537 &Client::new(),
538 &format!("{}/opc/v2", server.url()),
539 )
540 .await
541 .unwrap();
542
543 assert_eq!(region, "ap-seoul-1");
544 }
545
546 #[tokio::test]
547 async fn test_metadata_region_info_fetches_realm_domain() {
548 let mut server = Server::new_async().await;
549 let _mock = server
550 .mock("GET", "/opc/v2/instance/regionInfo")
551 .match_header("authorization", METADATA_AUTHORIZATION)
552 .with_status(200)
553 .with_body(
554 r#"{"realmKey":"oc2","realmDomainComponent":"oraclegovcloud.com","regionKey":"IAD","regionIdentifier":"us-langley-1"}"#,
555 )
556 .create_async()
557 .await;
558
559 let region_info = InstancePrincipalAuthProvider::metadata_region_info(
560 &Client::new(),
561 &format!("{}/opc/v2", server.url()),
562 )
563 .await
564 .unwrap();
565
566 assert_eq!(region_info.region_identifier, "us-langley-1");
567 assert_eq!(region_info.realm_domain_component, "oraclegovcloud.com");
568 }
569
570 #[test]
571 fn test_tenancy_id_from_certificate_prefers_opc_tenant_prefix() {
572 let tenancy_id = tenancy_id_from_certificate(TENANT_CERT_PEM).unwrap();
573 assert_eq!(tenancy_id, "ocid1.tenancy.oc1..example");
574 }
575
576 #[test]
577 fn test_tenancy_id_from_certificate_falls_back_to_opc_identity_prefix() {
578 let tenancy_id = tenancy_id_from_certificate(IDENTITY_CERT_PEM).unwrap();
579 assert_eq!(tenancy_id, "ocid1.tenancy.oc1..fallback");
580 }
581
582 #[tokio::test]
583 async fn test_sign_fetches_and_reuses_security_token() {
584 let mut metadata_server = Server::new_async().await;
585 let leaf_private_key = test_private_key_pem();
586 let leaf_certificate = test_certificate_pem("leaf");
587 let intermediate_certificate = test_certificate_pem("intermediate");
588
589 let _leaf_cert = metadata_server
590 .mock("GET", "/opc/v2/identity/cert.pem")
591 .match_header("authorization", METADATA_AUTHORIZATION)
592 .expect(1)
593 .with_status(200)
594 .with_body(leaf_certificate.clone())
595 .create_async()
596 .await;
597 let _leaf_key = metadata_server
598 .mock("GET", "/opc/v2/identity/key.pem")
599 .match_header("authorization", METADATA_AUTHORIZATION)
600 .expect(1)
601 .with_status(200)
602 .with_body(leaf_private_key.clone())
603 .create_async()
604 .await;
605 let _intermediate = metadata_server
606 .mock("GET", "/opc/v2/identity/intermediate.pem")
607 .match_header("authorization", METADATA_AUTHORIZATION)
608 .expect(1)
609 .with_status(200)
610 .with_body(intermediate_certificate.clone())
611 .create_async()
612 .await;
613
614 let exp = SystemTime::now()
615 .duration_since(UNIX_EPOCH)
616 .unwrap()
617 .as_secs()
618 + 3600;
619 let token = test_jwt(exp);
620
621 let mut auth_server = Server::new_async().await;
622 let auth_host = strip_http_scheme(&auth_server.url());
623 let _auth = auth_server
624 .mock("POST", "/v1/x509")
625 .match_header(
626 "content-type",
627 Matcher::Regex("application/json".to_owned()),
628 )
629 .match_header(
630 "authorization",
631 Matcher::Regex(r#"keyId="ocid1\.tenancy\.oc1\.\.example/fed-x509/"#.to_owned()),
632 )
633 .expect(1)
634 .with_status(200)
635 .with_body(format!(r#"{{"token":"{token}"}}"#))
636 .create_async()
637 .await;
638
639 let provider = InstancePrincipalAuthProvider::new(
640 Client::new(),
641 InstancePrincipalConfig::new("ap-seoul-1", "ocid1.tenancy.oc1..example")
642 .metadata_base_url(format!("{}/opc/v2", metadata_server.url()))
643 .auth_scheme("http")
644 .auth_host_override(auth_host)
645 .refresh_window(Duration::from_secs(60)),
646 );
647
648 let request = SignRequest {
649 method: "GET",
650 path: "/n/test_namespace/b/test_bucket/o/test_object",
651 host: Some("objectstorage.ap-seoul-1.oraclecloud.com"),
652 body: None,
653 content_type: None,
654 };
655
656 let first = provider.sign(&request).await.unwrap();
657 let second = provider.sign(&request).await.unwrap();
658
659 assert!(first.authorization.contains("keyId=\"ST$"));
660 assert_eq!(first.authorization, second.authorization);
661 }
662}