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}
19
20impl Credentials {
21    /// Create empty credentials
22    pub fn new() -> Self {
23        Self::default()
24    }
25
26    /// Create credentials with username and password
27    pub fn with_username_password(username: impl Into<String>, password: impl Into<String>) -> Self {
28        Self {
29            username: Some(username.into()),
30            password: Some(password.into()),
31            ..Default::default()
32        }
33    }
34
35    /// Create credentials with access key and secret key
36    pub fn with_access_key(access_key: impl Into<String>, secret_key: impl Into<String>) -> Self {
37        Self {
38            access_key: Some(access_key.into()),
39            secret_key: Some(secret_key.into()),
40            ..Default::default()
41        }
42    }
43
44    /// Check if credentials are configured
45    pub fn is_configured(&self) -> bool {
46        self.has_basic_auth() || self.has_ak_sk_auth()
47    }
48
49    /// Check if basic auth is configured
50    pub fn has_basic_auth(&self) -> bool {
51        self.username.is_some() && self.password.is_some()
52    }
53
54    /// Check if AK/SK auth is configured
55    pub fn has_ak_sk_auth(&self) -> bool {
56        self.access_key.is_some() && self.secret_key.is_some()
57    }
58
59    /// Generate signature for AK/SK auth
60    pub fn generate_signature(&self, resource: &str) -> Option<SignatureInfo> {
61        let access_key = self.access_key.as_ref()?;
62        let secret_key = self.secret_key.as_ref()?;
63
64        let timestamp = current_time_millis().to_string();
65        let sign_str = format!("{}+{}", resource, timestamp);
66
67        // HMAC-SHA1 signature
68        let mut mac = Hmac::<Sha1>::new_from_slice(secret_key.as_bytes()).ok()?;
69        mac.update(sign_str.as_bytes());
70        let result = mac.finalize();
71        let signature = base64::engine::general_purpose::STANDARD.encode(result.into_bytes());
72
73        Some(SignatureInfo {
74            access_key: access_key.clone(),
75            signature,
76            timestamp,
77        })
78    }
79}
80
81/// Signature information for AK/SK auth
82#[derive(Clone, Debug)]
83pub struct SignatureInfo {
84    pub access_key: String,
85    pub signature: String,
86    pub timestamp: String,
87}
88
89/// Access token from login
90#[derive(Clone, Debug, Default)]
91pub struct AccessToken {
92    /// Token value
93    pub token: String,
94    /// Token expiration time in milliseconds
95    pub expire_time: i64,
96    /// Whether token is global admin
97    pub global_admin: bool,
98}
99
100impl AccessToken {
101    /// Check if token is expired
102    pub fn is_expired(&self) -> bool {
103        if self.token.is_empty() {
104            return true;
105        }
106        // Consider expired 30 seconds before actual expiration
107        current_time_millis() >= self.expire_time - 30000
108    }
109
110    /// Check if token is valid
111    pub fn is_valid(&self) -> bool {
112        !self.token.is_empty() && !self.is_expired()
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_credentials_basic_auth() {
122        let creds = Credentials::with_username_password("admin", "password");
123        assert!(creds.has_basic_auth());
124        assert!(!creds.has_ak_sk_auth());
125        assert!(creds.is_configured());
126    }
127
128    #[test]
129    fn test_credentials_ak_sk_auth() {
130        let creds = Credentials::with_access_key("ak123", "sk456");
131        assert!(!creds.has_basic_auth());
132        assert!(creds.has_ak_sk_auth());
133        assert!(creds.is_configured());
134    }
135
136    #[test]
137    fn test_generate_signature() {
138        let creds = Credentials::with_access_key("test-ak", "test-sk");
139        let sig = creds.generate_signature("test-resource");
140        assert!(sig.is_some());
141        let sig = sig.unwrap();
142        assert_eq!(sig.access_key, "test-ak");
143        assert!(!sig.signature.is_empty());
144        assert!(!sig.timestamp.is_empty());
145    }
146
147    #[test]
148    fn test_access_token_expired() {
149        let token = AccessToken {
150            token: "test-token".to_string(),
151            expire_time: 0,
152            global_admin: false,
153        };
154        assert!(token.is_expired());
155        assert!(!token.is_valid());
156    }
157
158    #[test]
159    fn test_access_token_valid() {
160        let token = AccessToken {
161            token: "test-token".to_string(),
162            expire_time: current_time_millis() + 60000,
163            global_admin: false,
164        };
165        assert!(!token.is_expired());
166        assert!(token.is_valid());
167    }
168}