Skip to main content

oci_api/client/
http.rs

1//! OCI HTTP client
2//!
3//! OCI API HTTP client with custom request signing
4
5use 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/// OCI HTTP client
29#[derive(Clone)]
30pub struct Oci {
31    /// HTTP client
32    client: Client,
33
34    /// Region
35    region: String,
36
37    /// Realm domain component
38    realm_domain_component: String,
39
40    /// Tenancy ID
41    tenancy_id: String,
42
43    /// Compartment ID
44    compartment_id: Option<String>,
45
46    /// Authentication mode
47    auth_mode: AuthMode,
48    /// API key signer for compatibility
49    signer: Option<OciSigner>,
50    /// Authentication provider
51    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    /// Create new OCI client from environment variables
62    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        // Step 1: Load partial configuration from OCI_CONFIG if available
84        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        // Step 2: Merge with individual environment variables (highest priority)
91        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        // Step 3: Load private key
128        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        // Step 4: Optional compartment ID
140        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    /// Start builder pattern
212    pub fn builder() -> OciBuilder {
213        OciBuilder::default()
214    }
215
216    /// Get request signer
217    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    /// Return HTTP client reference
224    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    /// Return region
233    pub fn region(&self) -> &str {
234        &self.region
235    }
236
237    /// Return realm domain component
238    pub fn realm_domain(&self) -> &str {
239        &self.realm_domain_component
240    }
241
242    /// Return tenancy ID
243    pub fn tenancy_id(&self) -> &str {
244        &self.tenancy_id
245    }
246
247    /// Return compartment ID (defaults to tenancy_id if not set)
248    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    /// Create Email Delivery client
257    pub async fn email_delivery(&self) -> Result<EmailDelivery> {
258        EmailDelivery::new(self.clone()).await
259    }
260
261    /// Create Object Storage client
262    pub fn object_storage(&self, namespace: impl Into<String>) -> ObjectStorage {
263        ObjectStorage::new(self, namespace)
264    }
265
266    /// Create Vault Secrets client
267    pub fn vault(&self) -> VaultSecretsClient {
268        VaultSecretsClient::new(self)
269    }
270
271    /// Create Keys client
272    pub fn keys(&self, management_endpoint: impl Into<String>) -> KeysClient {
273        KeysClient::new(self, management_endpoint)
274    }
275}
276
277/// OCI client builder
278#[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    /// Load configuration from OCI config file
293    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    // Internal helper for optional compartment_id
348    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}