batata_client/auth/
credentials.rs

1use base64::Engine;
2use hmac::{Hmac, Mac};
3use sha1::Sha1;
4
5use crate::common::current_time_millis;
6
7/// Authentication credentials
8#[derive(Clone, Debug, Default)]
9pub struct Credentials {
10    /// Username for basic auth
11    pub username: Option<String>,
12    /// Password for basic auth
13    pub password: Option<String>,
14    /// Access key for RAM auth (Alibaba Cloud style)
15    pub access_key: Option<String>,
16    /// Secret key for RAM auth
17    pub secret_key: Option<String>,
18    /// ACM endpoint (for Alibaba Cloud ACM)
19    pub endpoint: Option<String>,
20    /// ACM region ID
21    pub region_id: Option<String>,
22    /// RAM role name (for ECS instance role auth)
23    pub ram_role_name: Option<String>,
24}
25
26impl Credentials {
27    /// Create empty credentials
28    pub fn new() -> Self {
29        Self::default()
30    }
31
32    /// Create credentials with username and password
33    pub fn with_username_password(username: impl Into<String>, password: impl Into<String>) -> Self {
34        Self {
35            username: Some(username.into()),
36            password: Some(password.into()),
37            ..Default::default()
38        }
39    }
40
41    /// Create credentials with access key and secret key
42    pub fn with_access_key(access_key: impl Into<String>, secret_key: impl Into<String>) -> Self {
43        Self {
44            access_key: Some(access_key.into()),
45            secret_key: Some(secret_key.into()),
46            ..Default::default()
47        }
48    }
49
50    /// Create credentials for Alibaba Cloud ACM
51    pub fn with_acm(
52        access_key: impl Into<String>,
53        secret_key: impl Into<String>,
54        endpoint: impl Into<String>,
55        region_id: impl Into<String>,
56    ) -> Self {
57        Self {
58            access_key: Some(access_key.into()),
59            secret_key: Some(secret_key.into()),
60            endpoint: Some(endpoint.into()),
61            region_id: Some(region_id.into()),
62            ..Default::default()
63        }
64    }
65
66    /// Set ACM endpoint
67    pub fn set_endpoint(&mut self, endpoint: impl Into<String>) {
68        self.endpoint = Some(endpoint.into());
69    }
70
71    /// Set ACM region ID
72    pub fn set_region_id(&mut self, region_id: impl Into<String>) {
73        self.region_id = Some(region_id.into());
74    }
75
76    /// Set RAM role name for ECS instance role auth
77    pub fn set_ram_role_name(&mut self, role_name: impl Into<String>) {
78        self.ram_role_name = Some(role_name.into());
79    }
80
81    /// Check if credentials are configured
82    pub fn is_configured(&self) -> bool {
83        self.has_basic_auth() || self.has_ak_sk_auth()
84    }
85
86    /// Check if basic auth is configured
87    pub fn has_basic_auth(&self) -> bool {
88        self.username.is_some() && self.password.is_some()
89    }
90
91    /// Check if AK/SK auth is configured
92    pub fn has_ak_sk_auth(&self) -> bool {
93        self.access_key.is_some() && self.secret_key.is_some()
94    }
95
96    /// Check if ACM auth is configured
97    pub fn has_acm_auth(&self) -> bool {
98        self.has_ak_sk_auth() && self.endpoint.is_some() && self.region_id.is_some()
99    }
100
101    /// Generate signature for AK/SK auth
102    pub fn generate_signature(&self, resource: &str) -> Option<SignatureInfo> {
103        let access_key = self.access_key.as_ref()?;
104        let secret_key = self.secret_key.as_ref()?;
105
106        let timestamp = current_time_millis().to_string();
107        let sign_str = format!("{}+{}", resource, timestamp);
108
109        // HMAC-SHA1 signature
110        let mut mac = Hmac::<Sha1>::new_from_slice(secret_key.as_bytes()).ok()?;
111        mac.update(sign_str.as_bytes());
112        let result = mac.finalize();
113        let signature = base64::engine::general_purpose::STANDARD.encode(result.into_bytes());
114
115        Some(SignatureInfo {
116            access_key: access_key.clone(),
117            signature,
118            timestamp,
119        })
120    }
121
122    /// Generate ACM-style signature for Alibaba Cloud ACM
123    ///
124    /// ACM uses a different signature format: HMAC-SHA1(secretKey, resource + "+" + timestamp)
125    /// The timestamp is in ISO 8601 format for ACM.
126    pub fn generate_acm_signature(&self, resource: &str) -> Option<AcmSignatureInfo> {
127        let access_key = self.access_key.as_ref()?;
128        let secret_key = self.secret_key.as_ref()?;
129
130        // ACM uses milliseconds timestamp
131        let timestamp = current_time_millis().to_string();
132        let sign_str = format!("{}+{}", resource, timestamp);
133
134        // HMAC-SHA1 signature
135        let mut mac = Hmac::<Sha1>::new_from_slice(secret_key.as_bytes()).ok()?;
136        mac.update(sign_str.as_bytes());
137        let result = mac.finalize();
138        let signature = base64::engine::general_purpose::STANDARD.encode(result.into_bytes());
139
140        Some(AcmSignatureInfo {
141            access_key: access_key.clone(),
142            signature,
143            timestamp,
144            endpoint: self.endpoint.clone(),
145            region_id: self.region_id.clone(),
146        })
147    }
148}
149
150/// Signature information for AK/SK auth
151#[derive(Clone, Debug)]
152pub struct SignatureInfo {
153    pub access_key: String,
154    pub signature: String,
155    pub timestamp: String,
156}
157
158/// ACM signature information for Alibaba Cloud ACM
159#[derive(Clone, Debug)]
160pub struct AcmSignatureInfo {
161    pub access_key: String,
162    pub signature: String,
163    pub timestamp: String,
164    pub endpoint: Option<String>,
165    pub region_id: Option<String>,
166}
167
168/// Access token from login
169#[derive(Clone, Debug, Default)]
170pub struct AccessToken {
171    /// Token value
172    pub token: String,
173    /// Token expiration time in milliseconds
174    pub expire_time: i64,
175    /// Whether token is global admin
176    pub global_admin: bool,
177}
178
179impl AccessToken {
180    /// Check if token is expired
181    pub fn is_expired(&self) -> bool {
182        if self.token.is_empty() {
183            return true;
184        }
185        // Consider expired 30 seconds before actual expiration
186        current_time_millis() >= self.expire_time - 30000
187    }
188
189    /// Check if token is valid
190    pub fn is_valid(&self) -> bool {
191        !self.token.is_empty() && !self.is_expired()
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn test_credentials_basic_auth() {
201        let creds = Credentials::with_username_password("admin", "password");
202        assert!(creds.has_basic_auth());
203        assert!(!creds.has_ak_sk_auth());
204        assert!(creds.is_configured());
205    }
206
207    #[test]
208    fn test_credentials_ak_sk_auth() {
209        let creds = Credentials::with_access_key("ak123", "sk456");
210        assert!(!creds.has_basic_auth());
211        assert!(creds.has_ak_sk_auth());
212        assert!(creds.is_configured());
213    }
214
215    #[test]
216    fn test_generate_signature() {
217        let creds = Credentials::with_access_key("test-ak", "test-sk");
218        let sig = creds.generate_signature("test-resource");
219        assert!(sig.is_some());
220        let sig = sig.unwrap();
221        assert_eq!(sig.access_key, "test-ak");
222        assert!(!sig.signature.is_empty());
223        assert!(!sig.timestamp.is_empty());
224    }
225
226    #[test]
227    fn test_access_token_expired() {
228        let token = AccessToken {
229            token: "test-token".to_string(),
230            expire_time: 0,
231            global_admin: false,
232        };
233        assert!(token.is_expired());
234        assert!(!token.is_valid());
235    }
236
237    #[test]
238    fn test_access_token_valid() {
239        let token = AccessToken {
240            token: "test-token".to_string(),
241            expire_time: current_time_millis() + 60000,
242            global_admin: false,
243        };
244        assert!(!token.is_expired());
245        assert!(token.is_valid());
246    }
247}