oci_api/auth/
config.rs

1//! OCI configuration struct definition
2//!
3//! This module defines the configuration structure required for OCI API authentication.
4
5use crate::error::{OciError, Result};
6
7/// OCI configuration
8#[derive(Debug, Clone)]
9pub struct OciConfig {
10    /// User ID (OCID format)
11    pub user_id: String,
12
13    /// Tenancy ID (OCID format)
14    pub tenancy_id: String,
15
16    /// Region (e.g., ap-seoul-1)
17    pub region: String,
18
19    /// Private key fingerprint
20    pub fingerprint: String,
21
22    /// Private key content (PEM format)
23    pub private_key: String,
24
25    /// Compartment ID (OCID format, optional - defaults to tenancy_id if not set)
26    pub compartment_id: Option<String>,
27}
28
29impl OciConfig {
30    /// Load configuration from environment variables
31    ///
32    /// # Priority (highest to lowest):
33    /// 1. Individual environment variables (OCI_USER_ID, etc.) - override everything
34    /// 2. OCI_CONFIG content (if set) - provides base values  
35    /// 3. Error if required fields are missing
36    ///
37    /// # Environment Variables
38    ///
39    /// ## Base configuration (lower priority):
40    /// - `OCI_CONFIG`: INI content string or file path to OCI config file
41    ///
42    /// ## Override configuration (higher priority):
43    /// - `OCI_USER_ID`: User ID (overrides value from OCI_CONFIG)
44    /// - `OCI_TENANCY_ID`: Tenancy ID (overrides value from OCI_CONFIG)
45    /// - `OCI_REGION`: Region (overrides value from OCI_CONFIG)
46    /// - `OCI_FINGERPRINT`: Private key fingerprint (overrides value from OCI_CONFIG)
47    /// - `OCI_PRIVATE_KEY`: Private key file path or PEM content (overrides key_file from OCI_CONFIG)
48    /// - `OCI_COMPARTMENT_ID`: Compartment ID (optional, defaults to tenancy_id)
49    ///
50    /// # Private Key Loading
51    ///
52    /// Private key is loaded in the following priority:
53    /// 1. `OCI_PRIVATE_KEY` environment variable (if set) - file path or PEM content
54    /// 2. `key_file` field from `OCI_CONFIG` (if OCI_CONFIG is set and contains key_file)
55    /// 3. Error if neither is available
56    pub fn from_env() -> Result<Self> {
57        use crate::auth::config_loader::ConfigLoader;
58        use crate::auth::key_loader::KeyLoader;
59        use std::env;
60
61        // Step 1: Load partial configuration from OCI_CONFIG if available
62        let partial_config = if let Ok(config_value) = env::var("OCI_CONFIG") {
63            Some(ConfigLoader::load_partial_from_env_var(&config_value)?)
64        } else {
65            None
66        };
67
68        // Step 2: Merge with individual environment variables (highest priority)
69        let user_id = env::var("OCI_USER_ID")
70            .ok()
71            .or_else(|| partial_config.as_ref().and_then(|c| c.user_id.clone()))
72            .ok_or_else(|| {
73                OciError::EnvError(
74                    "OCI_USER_ID must be set (either directly or via OCI_CONFIG)".to_string(),
75                )
76            })?;
77
78        let tenancy_id = env::var("OCI_TENANCY_ID")
79            .ok()
80            .or_else(|| partial_config.as_ref().and_then(|c| c.tenancy_id.clone()))
81            .ok_or_else(|| {
82                OciError::EnvError(
83                    "OCI_TENANCY_ID must be set (either directly or via OCI_CONFIG)".to_string(),
84                )
85            })?;
86
87        let region = env::var("OCI_REGION")
88            .ok()
89            .or_else(|| partial_config.as_ref().and_then(|c| c.region.clone()))
90            .ok_or_else(|| {
91                OciError::EnvError(
92                    "OCI_REGION must be set (either directly or via OCI_CONFIG)".to_string(),
93                )
94            })?;
95
96        let fingerprint = env::var("OCI_FINGERPRINT")
97            .ok()
98            .or_else(|| partial_config.as_ref().and_then(|c| c.fingerprint.clone()))
99            .ok_or_else(|| {
100                OciError::EnvError(
101                    "OCI_FINGERPRINT must be set (either directly or via OCI_CONFIG)".to_string(),
102                )
103            })?;
104
105        // Step 3: Load private key
106        // Priority: OCI_PRIVATE_KEY env var > key_file from OCI_CONFIG
107        let private_key = if let Ok(key_input) = env::var("OCI_PRIVATE_KEY") {
108            // OCI_PRIVATE_KEY provided - use it (file path or PEM content)
109            KeyLoader::load(&key_input)?
110        } else if let Ok(config_value) = env::var("OCI_CONFIG") {
111            // Fall back to loading from config file (which includes key_file)
112            let full_config = ConfigLoader::load_from_env_var(&config_value, None)?;
113            full_config.private_key
114        } else {
115            return Err(OciError::EnvError(
116                "OCI_PRIVATE_KEY must be set (or key_file must be in OCI_CONFIG)".to_string(),
117            ));
118        };
119
120        // Step 4: Optional compartment ID (defaults to tenancy_id)
121        let compartment_id = env::var("OCI_COMPARTMENT_ID").ok();
122
123        Ok(Self {
124            user_id,
125            tenancy_id,
126            region,
127            fingerprint,
128            private_key,
129            compartment_id,
130        })
131    }
132
133    /// Get region
134    pub fn region(&self) -> &str {
135        &self.region
136    }
137
138    /// Start builder pattern
139    pub fn builder() -> OciConfigBuilder {
140        OciConfigBuilder::default()
141    }
142}
143
144/// OCI configuration builder
145#[derive(Default)]
146pub struct OciConfigBuilder {
147    user_id: Option<String>,
148    tenancy_id: Option<String>,
149    region: Option<String>,
150    fingerprint: Option<String>,
151    private_key: Option<String>,
152    compartment_id: Option<String>,
153}
154
155impl OciConfigBuilder {
156    /// Load configuration from OCI config file
157    ///
158    /// Always uses the "DEFAULT" profile.
159    ///
160    /// # Arguments
161    /// - `path`: File path to OCI config file (e.g., `~/.oci/config`)
162    ///
163    /// # Example
164    /// ```no_run
165    /// # use oci_api::auth::OciConfig;
166    /// // Load from file
167    /// let config = OciConfig::builder()
168    ///     .config("/path/to/.oci/config")?
169    ///     .private_key("/path/to/key.pem")?  // Optional override
170    ///     .build()?;
171    /// # Ok::<(), Box<dyn std::error::Error>>(())
172    /// ```
173    pub fn config(mut self, path: impl AsRef<std::path::Path>) -> Result<Self> {
174        use crate::auth::config_loader::ConfigLoader;
175
176        let loaded = ConfigLoader::load_from_file(path.as_ref(), Some("DEFAULT"))?;
177
178        // Set all fields from loaded config (will be overridden by individual setters if called)
179        self.user_id = Some(loaded.user_id);
180        self.tenancy_id = Some(loaded.tenancy_id);
181        self.region = Some(loaded.region);
182        self.fingerprint = Some(loaded.fingerprint);
183        self.private_key = Some(loaded.private_key);
184        // Don't set compartment_id from config - only if explicitly set by user
185
186        Ok(self)
187    }
188
189    pub fn user_id(mut self, user_id: impl Into<String>) -> Self {
190        self.user_id = Some(user_id.into());
191        self
192    }
193
194    pub fn tenancy_id(mut self, tenancy_id: impl Into<String>) -> Self {
195        self.tenancy_id = Some(tenancy_id.into());
196        self
197    }
198
199    pub fn region(mut self, region: impl Into<String>) -> Self {
200        self.region = Some(region.into());
201        self
202    }
203
204    pub fn fingerprint(mut self, fingerprint: impl Into<String>) -> Self {
205        self.fingerprint = Some(fingerprint.into());
206        self
207    }
208
209    /// Set private key (file path or PEM content)
210    ///
211    /// Automatically detects whether the input is a file path or PEM content.
212    ///
213    /// # Example
214    /// ```no_run
215    /// # use oci_api::auth::OciConfig;
216    /// // From file path
217    /// let config = OciConfig::builder()
218    ///     .config("/path/to/.oci/config")?
219    ///     .private_key("/path/to/key.pem")?
220    ///     .build()?;
221    ///
222    /// // From PEM content
223    /// let config = OciConfig::builder()
224    ///     .config("/path/to/.oci/config")?
225    ///     .private_key("-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----")?
226    ///     .build()?;
227    /// # Ok::<(), Box<dyn std::error::Error>>(())
228    /// ```
229    pub fn private_key(mut self, private_key: impl Into<String>) -> Result<Self> {
230        use crate::auth::key_loader::KeyLoader;
231
232        let key_input = private_key.into();
233        let loaded_key = KeyLoader::load(&key_input)?;
234        self.private_key = Some(loaded_key);
235
236        Ok(self)
237    }
238
239    pub fn compartment_id(mut self, compartment_id: impl Into<String>) -> Self {
240        self.compartment_id = Some(compartment_id.into());
241        self
242    }
243
244    pub fn build(self) -> Result<OciConfig> {
245        Ok(OciConfig {
246            user_id: self
247                .user_id
248                .ok_or_else(|| OciError::ConfigError("user_id is not set".to_string()))?,
249            tenancy_id: self
250                .tenancy_id
251                .ok_or_else(|| OciError::ConfigError("tenancy_id is not set".to_string()))?,
252            region: self
253                .region
254                .ok_or_else(|| OciError::ConfigError("region is not set".to_string()))?,
255            fingerprint: self
256                .fingerprint
257                .ok_or_else(|| OciError::ConfigError("fingerprint is not set".to_string()))?,
258            private_key: self
259                .private_key
260                .ok_or_else(|| OciError::ConfigError("private_key is not set".to_string()))?,
261            compartment_id: self.compartment_id,
262        })
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269
270    #[test]
271    fn test_builder_all_fields() {
272        let user_id = "ocid1.user.test";
273        let tenancy_id = "ocid1.tenancy.test";
274        let region = "ap-seoul-1";
275        let fingerprint = "aa:bb:cc:dd";
276        let config = OciConfig::builder()
277            .user_id(user_id)
278            .tenancy_id(tenancy_id)
279            .region(region)
280            .fingerprint(fingerprint)
281            .private_key("-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----")
282            .unwrap()
283            .build()
284            .unwrap();
285
286        assert_eq!(config.user_id, user_id);
287        assert_eq!(config.tenancy_id, tenancy_id);
288        assert_eq!(config.region, region);
289        assert_eq!(config.fingerprint, fingerprint);
290        assert!(config.private_key.contains("BEGIN RSA PRIVATE KEY"));
291    }
292
293    #[test]
294    fn test_builder_missing_user_id() {
295        let result = OciConfig::builder()
296            .tenancy_id("ocid1.tenancy.test")
297            .region("ap-seoul-1")
298            .fingerprint("aa:bb:cc:dd")
299            .private_key("-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----")
300            .unwrap()
301            .build();
302
303        assert!(result.is_err());
304        match result.unwrap_err() {
305            OciError::ConfigError(msg) => assert!(msg.contains("user_id")),
306            _ => panic!("Expected ConfigError"),
307        }
308    }
309
310    #[test]
311    fn test_builder_missing_tenancy_id() {
312        let result = OciConfig::builder()
313            .user_id("ocid1.user.test")
314            .region("ap-seoul-1")
315            .fingerprint("aa:bb:cc:dd")
316            .private_key("-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----")
317            .unwrap()
318            .build();
319
320        assert!(result.is_err());
321        match result.unwrap_err() {
322            OciError::ConfigError(msg) => assert!(msg.contains("tenancy_id")),
323            _ => panic!("Expected ConfigError"),
324        }
325    }
326
327    #[test]
328    fn test_builder_missing_region() {
329        let result = OciConfig::builder()
330            .user_id("ocid1.user.test")
331            .tenancy_id("ocid1.tenancy.test")
332            .fingerprint("aa:bb:cc:dd")
333            .private_key("-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----")
334            .unwrap()
335            .build();
336
337        assert!(result.is_err());
338        match result.unwrap_err() {
339            OciError::ConfigError(msg) => assert!(msg.contains("region")),
340            _ => panic!("Expected ConfigError"),
341        }
342    }
343
344    #[test]
345    fn test_builder_missing_fingerprint() {
346        let result = OciConfig::builder()
347            .user_id("ocid1.user.test")
348            .tenancy_id("ocid1.tenancy.test")
349            .region("ap-seoul-1")
350            .private_key("-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----")
351            .unwrap()
352            .build();
353
354        assert!(result.is_err());
355        match result.unwrap_err() {
356            OciError::ConfigError(msg) => assert!(msg.contains("fingerprint")),
357            _ => panic!("Expected ConfigError"),
358        }
359    }
360
361    #[test]
362    fn test_builder_missing_private_key() {
363        let result = OciConfig::builder()
364            .user_id("ocid1.user.test")
365            .tenancy_id("ocid1.tenancy.test")
366            .region("ap-seoul-1")
367            .fingerprint("aa:bb:cc:dd")
368            .build();
369
370        assert!(result.is_err());
371        match result.unwrap_err() {
372            OciError::ConfigError(msg) => assert!(msg.contains("private_key")),
373            _ => panic!("Expected ConfigError"),
374        }
375    }
376
377    #[test]
378    fn test_from_env_missing_user_id() {
379        unsafe {
380            std::env::remove_var("OCI_CONFIG");
381            std::env::remove_var("OCI_USER_ID");
382            std::env::remove_var("OCI_TENANCY_ID");
383            std::env::remove_var("OCI_REGION");
384            std::env::remove_var("OCI_FINGERPRINT");
385            std::env::remove_var("OCI_PRIVATE_KEY");
386        }
387
388        let result = OciConfig::from_env();
389        assert!(result.is_err());
390        if let Err(e) = result {
391            match e {
392                OciError::EnvError(msg) => {
393                    assert!(msg.contains("OCI_USER_ID") || msg.contains("OCI_CONFIG"));
394                }
395                _ => panic!("Expected EnvError, got: {:?}", e),
396            }
397        }
398    }
399
400    #[test]
401    fn test_env_override_with_oci_config() {
402        unsafe {
403            // Clear all variables first
404            std::env::remove_var("OCI_CONFIG");
405            std::env::remove_var("OCI_USER_ID");
406            std::env::remove_var("OCI_TENANCY_ID");
407            std::env::remove_var("OCI_REGION");
408            std::env::remove_var("OCI_FINGERPRINT");
409            std::env::remove_var("OCI_PRIVATE_KEY");
410        }
411
412        unsafe {
413            // Override specific values with individual environment variables FIRST
414            std::env::set_var("OCI_USER_ID", "ocid1.user.from_env");
415            std::env::set_var("OCI_REGION", "ap-seoul-1");
416            std::env::set_var(
417                "OCI_PRIVATE_KEY",
418                "-----BEGIN PRIVATE KEY-----\ntest_key\n-----END PRIVATE KEY-----",
419            );
420
421            // THEN Setup OCI_CONFIG with base values
422            std::env::set_var(
423                "OCI_CONFIG",
424                r#"
425[DEFAULT]
426user=ocid1.user.from_config
427tenancy=ocid1.tenancy.from_config
428region=us-phoenix-1
429fingerprint=aa:bb:cc:dd:ee:ff
430"#,
431            );
432        }
433
434        let config = OciConfig::from_env().expect("Failed to load config");
435
436        // Individual environment variables should override OCI_CONFIG values
437        assert_eq!(config.user_id, "ocid1.user.from_env");
438        assert_eq!(config.region, "ap-seoul-1");
439        assert_eq!(
440            config.private_key,
441            "-----BEGIN PRIVATE KEY-----\ntest_key\n-----END PRIVATE KEY-----"
442        );
443
444        // Non-overridden values should come from OCI_CONFIG
445        assert_eq!(config.tenancy_id, "ocid1.tenancy.from_config");
446        assert_eq!(config.fingerprint, "aa:bb:cc:dd:ee:ff");
447
448        unsafe {
449            std::env::remove_var("OCI_CONFIG");
450            std::env::remove_var("OCI_USER_ID");
451            std::env::remove_var("OCI_REGION");
452            std::env::remove_var("OCI_PRIVATE_KEY");
453        }
454    }
455
456    #[test]
457    fn test_oci_private_key_not_in_config() {
458        unsafe {
459            // OCI_CONFIG should NOT contain private_key field
460            std::env::set_var(
461                "OCI_CONFIG",
462                r#"
463[DEFAULT]
464user=ocid1.user.test
465tenancy=ocid1.tenancy.test
466region=us-phoenix-1
467fingerprint=aa:bb:cc:dd:ee:ff
468private_key=this_should_be_ignored
469"#,
470            );
471
472            // OCI_PRIVATE_KEY is always required as separate environment variable
473            std::env::set_var(
474                "OCI_PRIVATE_KEY",
475                "-----BEGIN PRIVATE KEY-----\ntest_key\n-----END PRIVATE KEY-----",
476            );
477        }
478
479        let config = OciConfig::from_env().expect("Failed to load config");
480
481        // Should use OCI_PRIVATE_KEY, not the private_key field in OCI_CONFIG
482        assert_eq!(
483            config.private_key,
484            "-----BEGIN PRIVATE KEY-----\ntest_key\n-----END PRIVATE KEY-----"
485        );
486
487        unsafe {
488            std::env::remove_var("OCI_CONFIG");
489            std::env::remove_var("OCI_PRIVATE_KEY");
490        }
491    }
492}