Skip to main content

object_storage_proxy/credentials/
models.rs

1/// A parsed credential for an S3/COS bucket.
2///
3/// Credentials are returned by the optional `bucket_creds_fetcher` Python
4/// callback in one of several formats:
5///
6/// * A JSON object with `access_key` and `secret_key` → [`BucketCredential::Hmac`]
7/// * A JSON object with `api_key` or `apikey` → [`BucketCredential::ApiKey`]
8/// * Any other plain string → treated as a raw API key ([`BucketCredential::ApiKey`])
9pub enum BucketCredential {
10    /// AWS-style HMAC credentials (SigV4).
11    Hmac {
12        access_key: String,
13        secret_key: String,
14    },
15    /// IBM COS API key (exchanged for an IAM bearer token before forwarding).
16    ApiKey(String),
17}
18
19impl BucketCredential {
20    /// Parse a raw credential string into a [`BucketCredential`].
21    ///
22    /// Tries JSON first.  Recognises `access_key`/`secret_key` (HMAC) and
23    /// `api_key`/`apikey` (IBM API key).  Falls back to treating the entire
24    /// input as a plain API-key string.
25    pub fn parse(raw: &str) -> Self {
26        if let Ok(json_val) = serde_json::from_str::<serde_json::Value>(raw) {
27            if let (Some(ak), Some(sk)) = (json_val.get("access_key"), json_val.get("secret_key")) {
28                return BucketCredential::Hmac {
29                    access_key: ak.as_str().unwrap_or_default().to_owned(),
30                    secret_key: sk.as_str().unwrap_or_default().to_owned(),
31                };
32            }
33            if let Some(apikey) = json_val.get("api_key").or_else(|| json_val.get("apikey")) {
34                return BucketCredential::ApiKey(apikey.as_str().unwrap_or_default().to_owned());
35            }
36        }
37
38        BucketCredential::ApiKey(raw.to_owned())
39    }
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45
46    #[test]
47    fn parse_bucket_credential_variants() {
48        let hmac_json = r#"{ "access_key": "AK", "secret_key": "SK" }"#;
49        match BucketCredential::parse(hmac_json) {
50            BucketCredential::Hmac {
51                access_key,
52                secret_key,
53            } => {
54                assert_eq!(access_key, "AK");
55                assert_eq!(secret_key, "SK");
56            }
57            _ => panic!("Expected Hmac variant"),
58        }
59
60        let api_json = r#"{ "api_key": "APIKEY" }"#;
61        if let BucketCredential::ApiKey(k) = BucketCredential::parse(api_json) {
62            assert_eq!(k, "APIKEY");
63        } else {
64            panic!("Expected ApiKey variant");
65        }
66
67        let raw = "raw_token";
68        if let BucketCredential::ApiKey(k) = BucketCredential::parse(raw) {
69            assert_eq!(k, raw);
70        } else {
71            panic!("Expected fallback ApiKey variant");
72        }
73    }
74
75    #[test]
76    fn parse_apikey_alias() {
77        // "apikey" (no underscore) must also be recognised
78        let json = r#"{ "apikey": "MY_API_KEY" }"#;
79        if let BucketCredential::ApiKey(k) = BucketCredential::parse(json) {
80            assert_eq!(k, "MY_API_KEY");
81        } else {
82            panic!("Expected ApiKey variant for 'apikey' alias");
83        }
84    }
85
86    #[test]
87    fn parse_invalid_json_falls_back_to_api_key() {
88        let raw = "not-json-at-all";
89        if let BucketCredential::ApiKey(k) = BucketCredential::parse(raw) {
90            assert_eq!(k, raw);
91        } else {
92            panic!("Expected ApiKey fallback for non-JSON input");
93        }
94    }
95
96    #[test]
97    fn parse_json_missing_keys_falls_back_to_api_key() {
98        // Valid JSON but neither hmac nor api_key fields present
99        let raw = r#"{"foo": "bar"}"#;
100        if let BucketCredential::ApiKey(k) = BucketCredential::parse(raw) {
101            assert_eq!(k, raw);
102        } else {
103            panic!("Expected ApiKey fallback when JSON has no recognised keys");
104        }
105    }
106
107    #[test]
108    fn parse_empty_string_falls_back_to_api_key() {
109        if let BucketCredential::ApiKey(k) = BucketCredential::parse("") {
110            assert_eq!(k, "");
111        } else {
112            panic!("Expected ApiKey fallback for empty string");
113        }
114    }
115}