1use crate::auth::config_loader::ConfigLoader;
6use crate::auth::key_loader::KeyLoader;
7use crate::auth::providers::{
8 ApiKeyAuthProvider, DEFAULT_METADATA_BASE_URL, DEFAULT_REALM_DOMAIN_COMPONENT,
9 DynOciAuthProvider, InstancePrincipalAuthProvider, InstancePrincipalConfig,
10};
11use crate::client::request_executor::RequestExecutor;
12use crate::client::signer::OciSigner;
13use crate::error::{Error, Result};
14use crate::services::email::EmailDelivery;
15use crate::services::keys::KeysClient;
16use crate::services::object_storage::ObjectStorage;
17use crate::services::vault::VaultSecretsClient;
18use reqwest::Client;
19use std::env;
20use std::sync::Arc;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum AuthMode {
24 ApiKey,
25 InstancePrincipal,
26}
27
28#[derive(Clone)]
30pub struct Oci {
31 client: Client,
33
34 region: String,
36
37 realm_domain_component: String,
39
40 tenancy_id: String,
42
43 compartment_id: Option<String>,
45
46 auth_mode: AuthMode,
48 signer: Option<OciSigner>,
50 auth_provider: DynOciAuthProvider,
52}
53
54impl Default for Oci {
55 fn default() -> Self {
56 Self::from_env().expect("Failed to create OCI client from environment")
57 }
58}
59
60impl Oci {
61 pub fn from_env() -> Result<Self> {
63 let auth_mode = match env::var("OCI_AUTH_MODE")
64 .unwrap_or_else(|_| "api_key".to_owned())
65 .as_str()
66 {
67 "api_key" => AuthMode::ApiKey,
68 "instance_principal" => AuthMode::InstancePrincipal,
69 other => {
70 return Err(Error::EnvError(format!(
71 "OCI_AUTH_MODE must be 'api_key' or 'instance_principal', got '{other}'"
72 )));
73 }
74 };
75
76 match auth_mode {
77 AuthMode::ApiKey => Self::from_api_key_env(),
78 AuthMode::InstancePrincipal => Self::from_instance_principal_env(),
79 }
80 }
81
82 fn from_api_key_env() -> Result<Self> {
83 let partial_config = if let Ok(config_value) = env::var("OCI_CONFIG") {
85 Some(ConfigLoader::load_partial_from_env_var(&config_value)?)
86 } else {
87 None
88 };
89
90 let user_id = env::var("OCI_USER_ID")
92 .ok()
93 .or_else(|| partial_config.as_ref().and_then(|c| c.user_id.clone()))
94 .ok_or_else(|| {
95 Error::EnvError(
96 "OCI_USER_ID must be set (either directly or via OCI_CONFIG)".to_string(),
97 )
98 })?;
99
100 let tenancy_id = env::var("OCI_TENANCY_ID")
101 .ok()
102 .or_else(|| partial_config.as_ref().and_then(|c| c.tenancy_id.clone()))
103 .ok_or_else(|| {
104 Error::EnvError(
105 "OCI_TENANCY_ID must be set (either directly or via OCI_CONFIG)".to_string(),
106 )
107 })?;
108
109 let region = env::var("OCI_REGION")
110 .ok()
111 .or_else(|| partial_config.as_ref().and_then(|c| c.region.clone()))
112 .ok_or_else(|| {
113 Error::EnvError(
114 "OCI_REGION must be set (either directly or via OCI_CONFIG)".to_string(),
115 )
116 })?;
117
118 let fingerprint = env::var("OCI_FINGERPRINT")
119 .ok()
120 .or_else(|| partial_config.as_ref().and_then(|c| c.fingerprint.clone()))
121 .ok_or_else(|| {
122 Error::EnvError(
123 "OCI_FINGERPRINT must be set (either directly or via OCI_CONFIG)".to_string(),
124 )
125 })?;
126
127 let private_key = if let Ok(key_input) = env::var("OCI_PRIVATE_KEY") {
129 KeyLoader::load(&key_input)?
130 } else if let Ok(config_value) = env::var("OCI_CONFIG") {
131 let full_config = ConfigLoader::load_from_env_var(&config_value, None)?;
132 full_config.private_key
133 } else {
134 return Err(Error::EnvError(
135 "OCI_PRIVATE_KEY must be set (or key_file must be in OCI_CONFIG)".to_string(),
136 ));
137 };
138
139 let compartment_id = env::var("OCI_COMPARTMENT_ID").ok();
141
142 Self::builder()
143 .auth_mode(AuthMode::ApiKey)
144 .user_id(user_id)
145 .tenancy_id(tenancy_id)
146 .region(region)
147 .fingerprint(fingerprint)
148 .private_key(private_key)?
149 .compartment_id_opt(compartment_id)
150 .build()
151 }
152
153 fn from_instance_principal_env() -> Result<Self> {
154 let metadata_client = reqwest::blocking::Client::new();
155 let metadata_base_url = env::var("OCI_METADATA_BASE_URL").ok();
156 let metadata_region_info = InstancePrincipalAuthProvider::metadata_region_info_blocking(
157 &metadata_client,
158 metadata_base_url
159 .as_deref()
160 .unwrap_or(DEFAULT_METADATA_BASE_URL),
161 )
162 .ok();
163 let region = env::var("OCI_REGION")
164 .ok()
165 .or_else(|| {
166 metadata_region_info
167 .as_ref()
168 .map(|region_info| region_info.region_identifier.clone())
169 })
170 .ok_or_else(|| {
171 Error::EnvError(
172 "OCI_REGION must be set or discoverable from OCI metadata when OCI_AUTH_MODE=instance_principal"
173 .to_owned(),
174 )
175 })?;
176 let tenancy_id = env::var("OCI_TENANCY_ID")
177 .ok()
178 .or_else(|| {
179 InstancePrincipalAuthProvider::tenancy_id_from_metadata_certificate_blocking(
180 &metadata_client,
181 metadata_base_url
182 .as_deref()
183 .unwrap_or(DEFAULT_METADATA_BASE_URL),
184 )
185 .ok()
186 })
187 .ok_or_else(|| {
188 Error::EnvError(
189 "OCI_TENANCY_ID must be set or discoverable from OCI metadata when OCI_AUTH_MODE=instance_principal"
190 .to_owned(),
191 )
192 })?;
193 let compartment_id = env::var("OCI_COMPARTMENT_ID").ok();
194 let realm_domain_component = metadata_region_info
195 .as_ref()
196 .map(|region_info| region_info.realm_domain_component.clone())
197 .unwrap_or_else(|| DEFAULT_REALM_DOMAIN_COMPONENT.to_owned());
198
199 let mut builder = Self::builder()
200 .auth_mode(AuthMode::InstancePrincipal)
201 .region(region)
202 .realm_domain_component(realm_domain_component)
203 .tenancy_id(tenancy_id)
204 .compartment_id_opt(compartment_id);
205 if let Some(metadata_base_url) = metadata_base_url {
206 builder = builder.metadata_base_url(metadata_base_url);
207 }
208 builder.build()
209 }
210
211 pub fn builder() -> OciBuilder {
213 OciBuilder::default()
214 }
215
216 pub fn signer(&self) -> &OciSigner {
218 self.signer
219 .as_ref()
220 .expect("Oci::signer() is only available in api_key mode")
221 }
222
223 pub fn client(&self) -> &Client {
225 &self.client
226 }
227
228 pub(crate) fn executor(&self) -> RequestExecutor {
229 RequestExecutor::new(self.client.clone(), Arc::clone(&self.auth_provider))
230 }
231
232 pub fn region(&self) -> &str {
234 &self.region
235 }
236
237 pub fn realm_domain(&self) -> &str {
239 &self.realm_domain_component
240 }
241
242 pub fn tenancy_id(&self) -> &str {
244 &self.tenancy_id
245 }
246
247 pub fn compartment_id(&self) -> &str {
249 self.compartment_id.as_ref().unwrap_or(&self.tenancy_id)
250 }
251
252 pub fn auth_mode(&self) -> AuthMode {
253 self.auth_mode
254 }
255
256 pub async fn email_delivery(&self) -> Result<EmailDelivery> {
258 EmailDelivery::new(self.clone()).await
259 }
260
261 pub fn object_storage(&self, namespace: impl Into<String>) -> ObjectStorage {
263 ObjectStorage::new(self, namespace)
264 }
265
266 pub fn vault(&self) -> VaultSecretsClient {
268 VaultSecretsClient::new(self)
269 }
270
271 pub fn keys(&self, management_endpoint: impl Into<String>) -> KeysClient {
273 KeysClient::new(self, management_endpoint)
274 }
275}
276
277#[derive(Default)]
279pub struct OciBuilder {
280 user_id: Option<String>,
281 tenancy_id: Option<String>,
282 region: Option<String>,
283 realm_domain_component: Option<String>,
284 fingerprint: Option<String>,
285 private_key: Option<String>,
286 compartment_id: Option<String>,
287 auth_mode: AuthMode,
288 metadata_base_url: Option<String>,
289}
290
291impl OciBuilder {
292 pub fn config(mut self, path: impl AsRef<std::path::Path>) -> Result<Self> {
294 let loaded = ConfigLoader::load_from_file(path.as_ref(), Some("DEFAULT"))?;
295
296 self.user_id = Some(loaded.user_id);
297 self.tenancy_id = Some(loaded.tenancy_id);
298 self.region = Some(loaded.region);
299 self.fingerprint = Some(loaded.fingerprint);
300 self.private_key = Some(loaded.private_key);
301
302 Ok(self)
303 }
304
305 pub fn user_id(mut self, user_id: impl Into<String>) -> Self {
306 self.user_id = Some(user_id.into());
307 self
308 }
309
310 pub fn auth_mode(mut self, auth_mode: AuthMode) -> Self {
311 self.auth_mode = auth_mode;
312 self
313 }
314
315 pub fn tenancy_id(mut self, tenancy_id: impl Into<String>) -> Self {
316 self.tenancy_id = Some(tenancy_id.into());
317 self
318 }
319
320 pub fn region(mut self, region: impl Into<String>) -> Self {
321 self.region = Some(region.into());
322 self
323 }
324
325 pub fn realm_domain_component(mut self, realm_domain_component: impl Into<String>) -> Self {
326 self.realm_domain_component = Some(realm_domain_component.into());
327 self
328 }
329
330 pub fn fingerprint(mut self, fingerprint: impl Into<String>) -> Self {
331 self.fingerprint = Some(fingerprint.into());
332 self
333 }
334
335 pub fn private_key(mut self, private_key: impl Into<String>) -> Result<Self> {
336 let key_input = private_key.into();
337 let loaded_key = KeyLoader::load(&key_input)?;
338 self.private_key = Some(loaded_key);
339 Ok(self)
340 }
341
342 pub fn compartment_id(mut self, compartment_id: impl Into<String>) -> Self {
343 self.compartment_id = Some(compartment_id.into());
344 self
345 }
346
347 fn compartment_id_opt(mut self, compartment_id: Option<String>) -> Self {
349 self.compartment_id = compartment_id;
350 self
351 }
352
353 pub fn metadata_base_url(mut self, metadata_base_url: impl Into<String>) -> Self {
354 self.metadata_base_url = Some(metadata_base_url.into());
355 self
356 }
357
358 pub fn build(self) -> Result<Oci> {
359 let tenancy_id = self
360 .tenancy_id
361 .ok_or_else(|| Error::ConfigError("tenancy_id is not set".to_string()))?;
362 let region = self
363 .region
364 .ok_or_else(|| Error::ConfigError("region is not set".to_string()))?;
365 let realm_domain_component = self
366 .realm_domain_component
367 .unwrap_or_else(|| DEFAULT_REALM_DOMAIN_COMPONENT.to_owned());
368 let client = Client::builder().build()?;
369
370 let (signer, auth_provider) = match self.auth_mode {
371 AuthMode::ApiKey => {
372 let user_id = self
373 .user_id
374 .ok_or_else(|| Error::ConfigError("user_id is not set".to_owned()))?;
375 let fingerprint = self
376 .fingerprint
377 .ok_or_else(|| Error::ConfigError("fingerprint is not set".to_owned()))?;
378 let private_key = self
379 .private_key
380 .ok_or_else(|| Error::ConfigError("private_key is not set".to_owned()))?;
381 let signer = OciSigner::new(&user_id, &tenancy_id, &fingerprint, &private_key)?;
382 let provider =
383 Arc::new(ApiKeyAuthProvider::new(signer.clone())) as DynOciAuthProvider;
384 (Some(signer), provider)
385 }
386 AuthMode::InstancePrincipal => {
387 let config = if let Some(metadata_base_url) = self.metadata_base_url {
388 InstancePrincipalConfig::new(region.clone(), tenancy_id.clone())
389 .realm_domain_component(realm_domain_component.clone())
390 .metadata_base_url(metadata_base_url)
391 } else {
392 InstancePrincipalConfig::new(region.clone(), tenancy_id.clone())
393 .realm_domain_component(realm_domain_component.clone())
394 };
395 let provider = Arc::new(InstancePrincipalAuthProvider::new(client.clone(), config))
396 as DynOciAuthProvider;
397 (None, provider)
398 }
399 };
400
401 Ok(Oci {
402 client,
403 region,
404 realm_domain_component,
405 tenancy_id,
406 compartment_id: self.compartment_id,
407 signer,
408 auth_mode: self.auth_mode,
409 auth_provider,
410 })
411 }
412}
413
414impl Default for AuthMode {
415 fn default() -> Self {
416 Self::ApiKey
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423
424 use mockito::Server;
425 use serial_test::serial;
426
427 const TENANT_CERT_PEM: &str = "-----BEGIN CERTIFICATE-----\n\
428MIIDXzCCAkegAwIBAgIUONFqOCNE1N3Aps1ZQaPpY7SQzngwDQYJKoZIhvcNAQEL\n\
429BQAwPzEuMCwGA1UECgwlb3BjLXRlbmFudDpvY2lkMS50ZW5hbnR5Lm9jMS4uZXhh\n\
430bXBsZTENMAsGA1UEAwwEdGVzdDAeFw0yNjA1MTEwNjQ1NTFaFw0yNjA1MTIwNjQ1\n\
431NTFaMD8xLjAsBgNVBAoMJW9wYy10ZW5hbnQ6b2NpZDEudGVuYW5jeS5vYzEuLmV4\n\
432YW1wbGUxDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\n\
433AoIBAQDMblfnza9gqREWumv1mTJbR939nQIYZUynTxusVBXciNRjKaqB0jFSUFg9\n\
434E2pwtr7G/zr6rpIum9yaRT3O/hhIACP7CJvOoIPTV8qDmNcRnlT78nWBN8jnma1A\n\
435T9AZhtR14BJVe03eSSHBTnIDNNDQZu1+p6hUiGPVG1xe/F3/HOwbUrxzsChDnliZ\n\
436C46FL0JMIu/uH/Q/iSg0wYsJQKzE+iIvLo5edTeaTvdaTth8XLmltWM2DEwC/fyU\n\
437D2lxoOmvBhCVl1OCvT3Db0hMXRVV79BAXNS+qUyKbWnAgkiAMDGmEtYzizAoqCl4\n\
438GpDeqNfSI/xo8Zt1RqU1PgleQslDAgMBAAGjUzBRMB0GA1UdDgQWBBRnTn//hXKL\n\
439fWGEt7RY27CGihg+DjAfBgNVHSMEGDAWgBRnTn//hXKLfWGEt7RY27CGihg+DjAP\n\
440BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAwRR1OsfwCP1UF4PWK\n\
441jQLcBHrwEL7q9/HG47G6IsD4YN365ZPKzv7cOVzL7sPXVs18f3XDZwVNhwMiP2lo\n\
442ShLlHDIog2ZMD0kppoZlwf1EdbVVOr30qtHaRpd1/YHY1omuUCdis51iJzO/wMwL\n\
443m3yCFx7OCb46vCHwWc+CwiF9I9HKFMJyVpmhsEw91EPH3JaHWW1wn/RSIXuWpX0Q\n\
444t+CmwNhI9TC99JL2cfr5lFUjA8nQ5Xx68L9gyfQZ2aicx5XD+s+nt0mgc06oOWv3\n\
445ubYEGH/Vy8oK3rEoKdcNVdZUTgA0Fs2g+ItlrBFsJl5A1/TP3f0fbV6j9eY2SpdB\n\
446Eo34\n\
447-----END CERTIFICATE-----\n";
448
449 struct EnvGuard {
450 saved: Vec<(&'static str, Option<String>)>,
451 }
452
453 impl EnvGuard {
454 fn new(keys: &[&'static str]) -> Self {
455 Self {
456 saved: keys.iter().map(|key| (*key, env::var(key).ok())).collect(),
457 }
458 }
459
460 fn set(&self, key: &'static str, value: Option<&str>) {
461 unsafe {
462 match value {
463 Some(value) => env::set_var(key, value),
464 None => env::remove_var(key),
465 }
466 }
467 }
468 }
469
470 impl Drop for EnvGuard {
471 fn drop(&mut self) {
472 for (key, value) in &self.saved {
473 unsafe {
474 match value {
475 Some(value) => env::set_var(key, value),
476 None => env::remove_var(key),
477 }
478 }
479 }
480 }
481 }
482
483 #[test]
484 #[serial]
485 fn from_instance_principal_env_uses_metadata_region_info_when_bootstrap_envs_are_missing() {
486 let mut server = Server::new();
487 let _region_info = server
488 .mock("GET", "/opc/v2/instance/regionInfo")
489 .match_header("authorization", "Bearer Oracle")
490 .with_status(200)
491 .with_body(
492 r#"{"realmKey":"oc1","realmDomainComponent":"oraclecloud.com","regionKey":"PHX","regionIdentifier":"us-phoenix-1"}"#,
493 )
494 .create();
495 let _leaf_cert = server
496 .mock("GET", "/opc/v2/identity/cert.pem")
497 .match_header("authorization", "Bearer Oracle")
498 .with_status(200)
499 .with_body(TENANT_CERT_PEM)
500 .create();
501
502 let guard = EnvGuard::new(&[
503 "OCI_AUTH_MODE",
504 "OCI_REGION",
505 "OCI_TENANCY_ID",
506 "OCI_METADATA_BASE_URL",
507 "OCI_COMPARTMENT_ID",
508 ]);
509 guard.set("OCI_AUTH_MODE", Some("instance_principal"));
510 guard.set("OCI_REGION", None);
511 guard.set("OCI_TENANCY_ID", None);
512 guard.set(
513 "OCI_METADATA_BASE_URL",
514 Some(&format!("{}/opc/v2", server.url())),
515 );
516 guard.set("OCI_COMPARTMENT_ID", None);
517
518 let oci = Oci::from_env().unwrap();
519
520 assert_eq!(oci.region(), "us-phoenix-1");
521 assert_eq!(oci.realm_domain(), "oraclecloud.com");
522 assert_eq!(oci.tenancy_id(), "ocid1.tenancy.oc1..example");
523 }
524}