use crate::auth::config_loader::ConfigLoader;
use crate::auth::key_loader::KeyLoader;
use crate::auth::providers::{
ApiKeyAuthProvider, DEFAULT_METADATA_BASE_URL, DEFAULT_REALM_DOMAIN_COMPONENT,
DynOciAuthProvider, InstancePrincipalAuthProvider, InstancePrincipalConfig, MetadataRegionInfo,
};
use crate::client::request_executor::RequestExecutor;
use crate::client::signer::OciSigner;
use crate::error::{Error, Result};
use crate::services::email::EmailDelivery;
use crate::services::keys::KeysClient;
use crate::services::object_storage::ObjectStorage;
use crate::services::vault::VaultSecretsClient;
use reqwest::{Client, blocking::Client as BlockingClient};
use std::env;
use std::sync::Arc;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthMode {
ApiKey,
InstancePrincipal,
}
const OCI_METADATA_PROBE_TIMEOUT: Duration = Duration::from_millis(500);
const OCI_METADATA_BOOTSTRAP_TIMEOUT: Duration = Duration::from_secs(2);
#[derive(Clone)]
pub struct Oci {
client: Client,
region: String,
realm_domain_component: String,
tenancy_id: String,
compartment_id: Option<String>,
auth_mode: AuthMode,
signer: Option<OciSigner>,
auth_provider: DynOciAuthProvider,
}
impl Default for Oci {
fn default() -> Self {
Self::from_env().expect("Failed to create OCI client from environment")
}
}
impl Oci {
pub fn from_env() -> Result<Self> {
let auth_mode = Self::resolve_auth_mode_from_env()?;
match auth_mode {
AuthMode::ApiKey => Self::from_api_key_env(),
AuthMode::InstancePrincipal => Self::from_instance_principal_env(),
}
}
pub fn resolve_auth_mode_from_env() -> Result<AuthMode> {
if let Some(auth_mode) = Self::explicit_auth_mode_from_env()? {
return Ok(auth_mode);
}
if Self::autodetect_instance_principal()? {
return Ok(AuthMode::InstancePrincipal);
}
Ok(AuthMode::ApiKey)
}
fn from_api_key_env() -> Result<Self> {
let partial_config = if let Ok(config_value) = env::var("OCI_CONFIG") {
Some(ConfigLoader::load_partial_from_env_var(&config_value)?)
} else {
None
};
let user_id = env::var("OCI_USER_ID")
.ok()
.or_else(|| partial_config.as_ref().and_then(|c| c.user_id.clone()))
.ok_or_else(|| {
Error::EnvError(
"OCI_USER_ID must be set (either directly or via OCI_CONFIG)".to_string(),
)
})?;
let tenancy_id = env::var("OCI_TENANCY_ID")
.ok()
.or_else(|| partial_config.as_ref().and_then(|c| c.tenancy_id.clone()))
.ok_or_else(|| {
Error::EnvError(
"OCI_TENANCY_ID must be set (either directly or via OCI_CONFIG)".to_string(),
)
})?;
let region = env::var("OCI_REGION")
.ok()
.or_else(|| partial_config.as_ref().and_then(|c| c.region.clone()))
.ok_or_else(|| {
Error::EnvError(
"OCI_REGION must be set (either directly or via OCI_CONFIG)".to_string(),
)
})?;
let fingerprint = env::var("OCI_FINGERPRINT")
.ok()
.or_else(|| partial_config.as_ref().and_then(|c| c.fingerprint.clone()))
.ok_or_else(|| {
Error::EnvError(
"OCI_FINGERPRINT must be set (either directly or via OCI_CONFIG)".to_string(),
)
})?;
let private_key = if let Ok(key_input) = env::var("OCI_PRIVATE_KEY") {
KeyLoader::load(&key_input)?
} else if let Ok(config_value) = env::var("OCI_CONFIG") {
let full_config = ConfigLoader::load_from_env_var(&config_value, None)?;
full_config.private_key
} else {
return Err(Error::EnvError(
"OCI_PRIVATE_KEY must be set (or key_file must be in OCI_CONFIG)".to_string(),
));
};
let compartment_id = env::var("OCI_COMPARTMENT_ID").ok();
Self::builder()
.auth_mode(AuthMode::ApiKey)
.user_id(user_id)
.tenancy_id(tenancy_id)
.region(region)
.fingerprint(fingerprint)
.private_key(private_key)?
.compartment_id_opt(compartment_id)
.build()
}
fn from_instance_principal_env() -> Result<Self> {
let metadata_base_url = env::var("OCI_METADATA_BASE_URL").ok();
let metadata_client = Self::blocking_metadata_client(OCI_METADATA_BOOTSTRAP_TIMEOUT)?;
let metadata_region_info =
Self::metadata_region_info(&metadata_client, metadata_base_url.as_deref());
Self::from_instance_principal_env_with(
&metadata_client,
metadata_base_url,
metadata_region_info,
)
}
fn from_instance_principal_env_with(
metadata_client: &BlockingClient,
metadata_base_url: Option<String>,
metadata_region_info: Option<MetadataRegionInfo>,
) -> Result<Self> {
let region = env::var("OCI_REGION")
.ok()
.or_else(|| {
metadata_region_info
.as_ref()
.map(|region_info| region_info.region_identifier.clone())
})
.ok_or_else(|| {
Error::EnvError(
"OCI_REGION must be set or discoverable from OCI metadata when OCI_AUTH_MODE=instance_principal"
.to_owned(),
)
})?;
let tenancy_id = env::var("OCI_TENANCY_ID")
.ok()
.or_else(|| {
InstancePrincipalAuthProvider::tenancy_id_from_metadata_certificate_blocking(
&metadata_client,
metadata_base_url
.as_deref()
.unwrap_or(DEFAULT_METADATA_BASE_URL),
)
.ok()
})
.ok_or_else(|| {
Error::EnvError(
"OCI_TENANCY_ID must be set or discoverable from OCI metadata when OCI_AUTH_MODE=instance_principal"
.to_owned(),
)
})?;
let compartment_id = env::var("OCI_COMPARTMENT_ID").ok();
let realm_domain_component = metadata_region_info
.as_ref()
.map(|region_info| region_info.realm_domain_component.clone())
.unwrap_or_else(|| DEFAULT_REALM_DOMAIN_COMPONENT.to_owned());
let mut builder = Self::builder()
.auth_mode(AuthMode::InstancePrincipal)
.region(region)
.realm_domain_component(realm_domain_component)
.tenancy_id(tenancy_id)
.compartment_id_opt(compartment_id);
if let Some(metadata_base_url) = metadata_base_url {
builder = builder.metadata_base_url(metadata_base_url);
}
builder.build()
}
fn explicit_auth_mode_from_env() -> Result<Option<AuthMode>> {
let Some(raw_auth_mode) = env::var("OCI_AUTH_MODE").ok() else {
return Ok(None);
};
let auth_mode = raw_auth_mode.trim();
if auth_mode.is_empty() {
return Ok(None);
}
match auth_mode {
"api_key" => Ok(Some(AuthMode::ApiKey)),
"instance_principal" => Ok(Some(AuthMode::InstancePrincipal)),
other => Err(Error::EnvError(format!(
"OCI_AUTH_MODE must be 'api_key' or 'instance_principal', got '{other}'"
))),
}
}
fn autodetect_instance_principal() -> Result<bool> {
let metadata_client = Self::blocking_metadata_client(OCI_METADATA_PROBE_TIMEOUT)?;
let metadata_base_url = env::var("OCI_METADATA_BASE_URL").ok();
Ok(Self::metadata_region_info(&metadata_client, metadata_base_url.as_deref()).is_some())
}
fn metadata_region_info(
metadata_client: &BlockingClient,
metadata_base_url: Option<&str>,
) -> Option<MetadataRegionInfo> {
InstancePrincipalAuthProvider::metadata_region_info_blocking(
metadata_client,
metadata_base_url.unwrap_or(DEFAULT_METADATA_BASE_URL),
)
.ok()
}
fn blocking_metadata_client(timeout: Duration) -> Result<BlockingClient> {
Ok(BlockingClient::builder()
.connect_timeout(timeout)
.timeout(timeout)
.build()?)
}
pub fn builder() -> OciBuilder {
OciBuilder::default()
}
pub fn signer(&self) -> &OciSigner {
self.signer
.as_ref()
.expect("Oci::signer() is only available in api_key mode")
}
pub fn client(&self) -> &Client {
&self.client
}
pub(crate) fn executor(&self) -> RequestExecutor {
RequestExecutor::new(self.client.clone(), Arc::clone(&self.auth_provider))
}
pub fn region(&self) -> &str {
&self.region
}
pub fn realm_domain(&self) -> &str {
&self.realm_domain_component
}
pub fn tenancy_id(&self) -> &str {
&self.tenancy_id
}
pub fn compartment_id(&self) -> &str {
self.compartment_id.as_ref().unwrap_or(&self.tenancy_id)
}
pub fn auth_mode(&self) -> AuthMode {
self.auth_mode
}
pub async fn email_delivery(&self) -> Result<EmailDelivery> {
EmailDelivery::new(self.clone()).await
}
pub fn object_storage(&self, namespace: impl Into<String>) -> ObjectStorage {
ObjectStorage::new(self, namespace)
}
pub fn vault(&self) -> VaultSecretsClient {
VaultSecretsClient::new(self)
}
pub fn keys(&self, management_endpoint: impl Into<String>) -> KeysClient {
KeysClient::new(self, management_endpoint)
}
}
#[derive(Default)]
pub struct OciBuilder {
user_id: Option<String>,
tenancy_id: Option<String>,
region: Option<String>,
realm_domain_component: Option<String>,
fingerprint: Option<String>,
private_key: Option<String>,
compartment_id: Option<String>,
auth_mode: AuthMode,
metadata_base_url: Option<String>,
}
impl OciBuilder {
pub fn config(mut self, path: impl AsRef<std::path::Path>) -> Result<Self> {
let loaded = ConfigLoader::load_from_file(path.as_ref(), Some("DEFAULT"))?;
self.user_id = Some(loaded.user_id);
self.tenancy_id = Some(loaded.tenancy_id);
self.region = Some(loaded.region);
self.fingerprint = Some(loaded.fingerprint);
self.private_key = Some(loaded.private_key);
Ok(self)
}
pub fn user_id(mut self, user_id: impl Into<String>) -> Self {
self.user_id = Some(user_id.into());
self
}
pub fn auth_mode(mut self, auth_mode: AuthMode) -> Self {
self.auth_mode = auth_mode;
self
}
pub fn tenancy_id(mut self, tenancy_id: impl Into<String>) -> Self {
self.tenancy_id = Some(tenancy_id.into());
self
}
pub fn region(mut self, region: impl Into<String>) -> Self {
self.region = Some(region.into());
self
}
pub fn realm_domain_component(mut self, realm_domain_component: impl Into<String>) -> Self {
self.realm_domain_component = Some(realm_domain_component.into());
self
}
pub fn fingerprint(mut self, fingerprint: impl Into<String>) -> Self {
self.fingerprint = Some(fingerprint.into());
self
}
pub fn private_key(mut self, private_key: impl Into<String>) -> Result<Self> {
let key_input = private_key.into();
let loaded_key = KeyLoader::load(&key_input)?;
self.private_key = Some(loaded_key);
Ok(self)
}
pub fn compartment_id(mut self, compartment_id: impl Into<String>) -> Self {
self.compartment_id = Some(compartment_id.into());
self
}
fn compartment_id_opt(mut self, compartment_id: Option<String>) -> Self {
self.compartment_id = compartment_id;
self
}
pub fn metadata_base_url(mut self, metadata_base_url: impl Into<String>) -> Self {
self.metadata_base_url = Some(metadata_base_url.into());
self
}
pub fn build(self) -> Result<Oci> {
let tenancy_id = self
.tenancy_id
.ok_or_else(|| Error::ConfigError("tenancy_id is not set".to_string()))?;
let region = self
.region
.ok_or_else(|| Error::ConfigError("region is not set".to_string()))?;
let realm_domain_component = self
.realm_domain_component
.unwrap_or_else(|| DEFAULT_REALM_DOMAIN_COMPONENT.to_owned());
let client = Client::builder().build()?;
let (signer, auth_provider) = match self.auth_mode {
AuthMode::ApiKey => {
let user_id = self
.user_id
.ok_or_else(|| Error::ConfigError("user_id is not set".to_owned()))?;
let fingerprint = self
.fingerprint
.ok_or_else(|| Error::ConfigError("fingerprint is not set".to_owned()))?;
let private_key = self
.private_key
.ok_or_else(|| Error::ConfigError("private_key is not set".to_owned()))?;
let signer = OciSigner::new(&user_id, &tenancy_id, &fingerprint, &private_key)?;
let provider =
Arc::new(ApiKeyAuthProvider::new(signer.clone())) as DynOciAuthProvider;
(Some(signer), provider)
}
AuthMode::InstancePrincipal => {
let config = if let Some(metadata_base_url) = self.metadata_base_url {
InstancePrincipalConfig::new(region.clone(), tenancy_id.clone())
.realm_domain_component(realm_domain_component.clone())
.metadata_base_url(metadata_base_url)
} else {
InstancePrincipalConfig::new(region.clone(), tenancy_id.clone())
.realm_domain_component(realm_domain_component.clone())
};
let provider = Arc::new(InstancePrincipalAuthProvider::new(client.clone(), config))
as DynOciAuthProvider;
(None, provider)
}
};
Ok(Oci {
client,
region,
realm_domain_component,
tenancy_id,
compartment_id: self.compartment_id,
signer,
auth_mode: self.auth_mode,
auth_provider,
})
}
}
impl Default for AuthMode {
fn default() -> Self {
Self::ApiKey
}
}
#[cfg(test)]
mod tests {
use super::*;
use mockito::Server;
use serial_test::serial;
const TEST_VALID_PEM: &str = r#"-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCvfVmTGipPCAsg
fr8khhrPpQxmjUW62+pH/54EecyKTd8KTkg11wT40Pi5zB/UAl8DGTPs9MNz1PQX
EGPh7YPccPTGJ4ZFfu87s2W9m3zp9UWUIy+n+Jr5FBpn8H7n7W/FPLTF7xRyzMSY
BGWFKIyHkufglkKJlRkyVK8+0w6vFBg5Ni/0Eo0uTT31AWvv1b5nuCRstSCME2O7
GbNUPo6vF1xEWNeFzp9Lp7JuMXu+tgLJiSkHKq7I2u25iQvklnqogDSLzxQigX/P
+08jd52R9HI0rWiwLVJ1QE/erZJ+DnKjikb3jpHNRVZmG7/tDM/54yh85L0JfzZx
yt+b3qS5AgMBAAECggEAGMAKERggnXLZ9uRJWwJa56w0eoY0Lm1ztmHTzHfNJDhl
W5O81XMU7W6zlai3WHRZKBu22hWPN1fycQpLvAJ+lWmM7CGI62ZCoV3k3IAAdxKz
lHf98ae7W6O9MamWjGlNWTj9mejlLme41mPQWZ5la32JnIA0tCjGG/YbnTWxHXnx
B5skseaEMR3DT98uBZa67IFKDLJDIIaD4aQNILMNtEb2PFOChblA0mm2szR3AMhv
Pl0VvrexHR+xdlteUBJ/G3Y3KuAB4MzTwl9rBarTmBaaZbl+iD1Kt3v+elNQdVCo
JPSfGr9AbVdFDHB0FS46sWqOyk3Rx9lScigUWb0mvQKBgQDnfUQJ7Uhqm7FByXQs
MWxLQIEHukWGG98btV2FjHO5N/IObrjXXUEl3qkTIW+oa+im48HRDKjlIZkTtN7l
tbhqRlt9lW7PXtR+J+YjSXxAeourNaaMxbaVy3U/fhVVP5KrWfLzBbb0ZOF2A7gq
g+rlHFVIVPOLj8lIPIlFjST9zwKBgQDCEiklTiFZZP6EjvgT7yMdJgvOkLFcJ4nF
A1PL72S7nYPKbwQZt0eUohMA/PVkDyemNpafTYeGjKx+waS60Zcn1/S6CMMDkmJL
DBAJVtCXwVmyaJTocS9kQwTeLqK+BBiHWL9nPTHmrTmEfrVwwB51eB9G+EJlv4fy
J8f4yPie9wKBgQCt/u3hOEUyPIxjknSLsype9cEGefA/+TsdrJj7BLMHCRIb3wV4
e1O4j0AubPdsdI+Owaqw4v8gGrzgnxbbOle/Kdsi7es4W2ME4CCPbXDDVlkc+1qQ
fRvcQ+2BJ9gJF5u6yAVgvW7jC+Cbv/fxnO41/7HqiE/3GsCEV1wmtwyS6QKBgQCe
h7VCuwr0+lIKuLsflYYKhoy4hWvMSqP44pnuCjUwKSCCGaOw2g3H9YkuknRl8xdB
aHAr22os1/cEaGyHCzS9oGRSH1wmK8rNYSIsbtVgUdpSqamSIvtCnJh6YoAgVjov
PajEzbFYrQJCIDtYyidXb/OkxqF+ejGz9xkcOhcVywKBgQCCmIJbRrHKB7YYPD68
NJo0kGnesUmsBzrFxWsckCTYpVkqjDI4VPeOYVFpXtlPkVMIIy7PSjZHCu9ujcDC
Oj3UlzzFzA70eAdkFrBlFxIembT4SjSoptN/8GP8wIe7xgnvj0gZJTH3W+z8AiBr
Ae/wEOcaaJD3g0i9hhz8Blf4IA==
-----END PRIVATE KEY-----"#;
const TENANT_CERT_PEM: &str = "-----BEGIN CERTIFICATE-----\n\
MIIDXzCCAkegAwIBAgIUONFqOCNE1N3Aps1ZQaPpY7SQzngwDQYJKoZIhvcNAQEL\n\
BQAwPzEuMCwGA1UECgwlb3BjLXRlbmFudDpvY2lkMS50ZW5hbnR5Lm9jMS4uZXhh\n\
bXBsZTENMAsGA1UEAwwEdGVzdDAeFw0yNjA1MTEwNjQ1NTFaFw0yNjA1MTIwNjQ1\n\
NTFaMD8xLjAsBgNVBAoMJW9wYy10ZW5hbnQ6b2NpZDEudGVuYW5jeS5vYzEuLmV4\n\
YW1wbGUxDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\n\
AoIBAQDMblfnza9gqREWumv1mTJbR939nQIYZUynTxusVBXciNRjKaqB0jFSUFg9\n\
E2pwtr7G/zr6rpIum9yaRT3O/hhIACP7CJvOoIPTV8qDmNcRnlT78nWBN8jnma1A\n\
T9AZhtR14BJVe03eSSHBTnIDNNDQZu1+p6hUiGPVG1xe/F3/HOwbUrxzsChDnliZ\n\
C46FL0JMIu/uH/Q/iSg0wYsJQKzE+iIvLo5edTeaTvdaTth8XLmltWM2DEwC/fyU\n\
D2lxoOmvBhCVl1OCvT3Db0hMXRVV79BAXNS+qUyKbWnAgkiAMDGmEtYzizAoqCl4\n\
GpDeqNfSI/xo8Zt1RqU1PgleQslDAgMBAAGjUzBRMB0GA1UdDgQWBBRnTn//hXKL\n\
fWGEt7RY27CGihg+DjAfBgNVHSMEGDAWgBRnTn//hXKLfWGEt7RY27CGihg+DjAP\n\
BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAwRR1OsfwCP1UF4PWK\n\
jQLcBHrwEL7q9/HG47G6IsD4YN365ZPKzv7cOVzL7sPXVs18f3XDZwVNhwMiP2lo\n\
ShLlHDIog2ZMD0kppoZlwf1EdbVVOr30qtHaRpd1/YHY1omuUCdis51iJzO/wMwL\n\
m3yCFx7OCb46vCHwWc+CwiF9I9HKFMJyVpmhsEw91EPH3JaHWW1wn/RSIXuWpX0Q\n\
t+CmwNhI9TC99JL2cfr5lFUjA8nQ5Xx68L9gyfQZ2aicx5XD+s+nt0mgc06oOWv3\n\
ubYEGH/Vy8oK3rEoKdcNVdZUTgA0Fs2g+ItlrBFsJl5A1/TP3f0fbV6j9eY2SpdB\n\
Eo34\n\
-----END CERTIFICATE-----\n";
struct EnvGuard {
saved: Vec<(&'static str, Option<String>)>,
}
impl EnvGuard {
fn new(keys: &[&'static str]) -> Self {
Self {
saved: keys.iter().map(|key| (*key, env::var(key).ok())).collect(),
}
}
fn set(&self, key: &'static str, value: Option<&str>) {
unsafe {
match value {
Some(value) => env::set_var(key, value),
None => env::remove_var(key),
}
}
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
for (key, value) in &self.saved {
unsafe {
match value {
Some(value) => env::set_var(key, value),
None => env::remove_var(key),
}
}
}
}
}
#[test]
#[serial]
fn resolve_auth_mode_prefers_explicit_api_key_over_oci_autodetect() {
let mut server = Server::new();
let _region_info = server
.mock("GET", "/opc/v2/instance/regionInfo")
.match_header("authorization", "Bearer Oracle")
.with_status(200)
.with_body(
r#"{"realmKey":"oc1","realmDomainComponent":"oraclecloud.com","regionKey":"PHX","regionIdentifier":"us-phoenix-1"}"#,
)
.create();
let guard = EnvGuard::new(&[
"OCI_AUTH_MODE",
"OCI_REGION",
"OCI_TENANCY_ID",
"OCI_METADATA_BASE_URL",
"OCI_COMPARTMENT_ID",
]);
guard.set("OCI_AUTH_MODE", Some("api_key"));
guard.set("OCI_REGION", None);
guard.set("OCI_TENANCY_ID", None);
guard.set(
"OCI_METADATA_BASE_URL",
Some(&format!("{}/opc/v2", server.url())),
);
guard.set("OCI_COMPARTMENT_ID", None);
assert_eq!(Oci::resolve_auth_mode_from_env().unwrap(), AuthMode::ApiKey);
}
#[test]
#[serial]
fn resolve_auth_mode_autodetects_instance_principal_when_metadata_is_reachable() {
let mut server = Server::new();
let _region_info = server
.mock("GET", "/opc/v2/instance/regionInfo")
.match_header("authorization", "Bearer Oracle")
.with_status(200)
.with_body(
r#"{"realmKey":"oc1","realmDomainComponent":"oraclecloud.com","regionKey":"PHX","regionIdentifier":"us-phoenix-1"}"#,
)
.create();
let guard = EnvGuard::new(&[
"OCI_AUTH_MODE",
"OCI_REGION",
"OCI_TENANCY_ID",
"OCI_METADATA_BASE_URL",
"OCI_COMPARTMENT_ID",
]);
guard.set("OCI_AUTH_MODE", None);
guard.set("OCI_REGION", None);
guard.set("OCI_TENANCY_ID", None);
guard.set(
"OCI_METADATA_BASE_URL",
Some(&format!("{}/opc/v2", server.url())),
);
guard.set("OCI_COMPARTMENT_ID", None);
assert_eq!(
Oci::resolve_auth_mode_from_env().unwrap(),
AuthMode::InstancePrincipal
);
}
#[test]
#[serial]
fn resolve_auth_mode_falls_back_to_api_key_when_metadata_is_unavailable() {
let guard = EnvGuard::new(&[
"OCI_AUTH_MODE",
"OCI_REGION",
"OCI_TENANCY_ID",
"OCI_METADATA_BASE_URL",
]);
guard.set("OCI_AUTH_MODE", None);
guard.set("OCI_REGION", None);
guard.set("OCI_TENANCY_ID", None);
guard.set("OCI_METADATA_BASE_URL", Some("http://127.0.0.1:9/opc/v2"));
assert_eq!(Oci::resolve_auth_mode_from_env().unwrap(), AuthMode::ApiKey);
}
#[test]
#[serial]
fn resolve_auth_mode_treats_empty_override_as_unset() {
let guard = EnvGuard::new(&["OCI_AUTH_MODE", "OCI_METADATA_BASE_URL"]);
guard.set("OCI_AUTH_MODE", Some(" "));
guard.set("OCI_METADATA_BASE_URL", Some("http://127.0.0.1:9/opc/v2"));
assert_eq!(Oci::resolve_auth_mode_from_env().unwrap(), AuthMode::ApiKey);
}
#[test]
#[serial]
fn resolve_auth_mode_rejects_invalid_override() {
let guard = EnvGuard::new(&["OCI_AUTH_MODE"]);
guard.set("OCI_AUTH_MODE", Some("foo"));
let error = Oci::resolve_auth_mode_from_env().unwrap_err();
assert!(matches!(error, Error::EnvError(_)));
assert!(
error
.to_string()
.contains("OCI_AUTH_MODE must be 'api_key' or 'instance_principal'")
);
}
#[test]
#[serial]
fn from_env_autodetects_instance_principal_when_bootstrap_envs_are_missing() {
let mut server = Server::new();
let _region_info = server
.mock("GET", "/opc/v2/instance/regionInfo")
.match_header("authorization", "Bearer Oracle")
.with_status(200)
.with_body(
r#"{"realmKey":"oc1","realmDomainComponent":"oraclecloud.com","regionKey":"PHX","regionIdentifier":"us-phoenix-1"}"#,
)
.create();
let _leaf_cert = server
.mock("GET", "/opc/v2/identity/cert.pem")
.match_header("authorization", "Bearer Oracle")
.with_status(200)
.with_body(TENANT_CERT_PEM)
.create();
let guard = EnvGuard::new(&[
"OCI_AUTH_MODE",
"OCI_REGION",
"OCI_TENANCY_ID",
"OCI_METADATA_BASE_URL",
"OCI_COMPARTMENT_ID",
]);
guard.set("OCI_AUTH_MODE", None);
guard.set("OCI_REGION", None);
guard.set("OCI_TENANCY_ID", None);
guard.set(
"OCI_METADATA_BASE_URL",
Some(&format!("{}/opc/v2", server.url())),
);
guard.set("OCI_COMPARTMENT_ID", None);
let oci = Oci::from_env().unwrap();
assert_eq!(oci.region(), "us-phoenix-1");
assert_eq!(oci.realm_domain(), "oraclecloud.com");
assert_eq!(oci.tenancy_id(), "ocid1.tenancy.oc1..example");
assert_eq!(oci.auth_mode(), AuthMode::InstancePrincipal);
}
#[test]
#[serial]
fn from_env_uses_explicit_api_key_mode_when_requested() {
let guard = EnvGuard::new(&[
"OCI_AUTH_MODE",
"OCI_USER_ID",
"OCI_TENANCY_ID",
"OCI_REGION",
"OCI_FINGERPRINT",
"OCI_PRIVATE_KEY",
"OCI_CONFIG",
]);
guard.set("OCI_AUTH_MODE", Some("api_key"));
guard.set("OCI_USER_ID", Some("ocid1.user.oc1..example"));
guard.set("OCI_TENANCY_ID", Some("ocid1.tenancy.oc1..example"));
guard.set("OCI_REGION", Some("ap-chuncheon-1"));
guard.set(
"OCI_FINGERPRINT",
Some("11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00"),
);
guard.set("OCI_PRIVATE_KEY", Some(TEST_VALID_PEM));
guard.set("OCI_CONFIG", None);
let oci = Oci::from_env().unwrap();
assert_eq!(oci.auth_mode(), AuthMode::ApiKey);
assert_eq!(oci.region(), "ap-chuncheon-1");
}
}