Skip to main content

reduct_rs/client/
token.rs

1use super::{ReductClient, Result};
2use crate::http_client::HttpClient;
3use chrono::{DateTime, Utc};
4use reduct_base::msg::token_api::{
5    Permissions, Token, TokenCreateRequest, TokenCreateResponse, TokenList,
6};
7use reqwest::Method;
8use std::sync::Arc;
9use std::time::Duration;
10
11/// Builder for creating an access token.
12pub struct CreateTokenBuilder {
13    name: String,
14    request: TokenCreateRequest,
15    http_client: Arc<HttpClient>,
16}
17
18impl CreateTokenBuilder {
19    pub(super) fn new(name: String, http_client: Arc<HttpClient>) -> Self {
20        Self {
21            name,
22            request: TokenCreateRequest::default(),
23            http_client,
24        }
25    }
26
27    /// Set token permissions.
28    pub fn permissions(mut self, permissions: Permissions) -> Self {
29        self.request.permissions = permissions;
30        self
31    }
32
33    /// Set the absolute expiration time for the token.
34    pub fn expires_at(mut self, expires_at: DateTime<Utc>) -> Self {
35        self.request.expires_at = Some(expires_at);
36        self
37    }
38
39    /// Set the inactivity timeout for the token.
40    pub fn ttl(mut self, ttl: Duration) -> Self {
41        self.request.ttl = Some(ttl.as_secs());
42        self
43    }
44
45    /// Restrict the token to the provided client IP addresses.
46    pub fn ip_allowlist(mut self, ip_allowlist: Vec<String>) -> Self {
47        self.request.ip_allowlist = ip_allowlist;
48        self
49    }
50
51    /// Send the token creation request.
52    pub async fn send(self) -> Result<TokenCreateResponse> {
53        self.http_client
54            .send_and_receive_json::<TokenCreateRequest, TokenCreateResponse>(
55                Method::POST,
56                &format!("/tokens/{}", self.name),
57                Some(self.request),
58            )
59            .await
60    }
61}
62
63impl ReductClient {
64    /// Get the token with permissions for the current user.
65    ///
66    /// # Returns
67    ///
68    /// The token or HttpError
69    pub async fn me(&self) -> Result<Token> {
70        self.http_client
71            .send_and_receive_json::<(), Token>(Method::GET, "/me", None)
72            .await
73    }
74
75    /// Get extended token info for the current user.
76    pub async fn me_info(&self) -> Result<Token> {
77        self.http_client
78            .send_and_receive_json::<(), Token>(Method::GET, "/me", None)
79            .await
80    }
81
82    /// Create an access token.
83    pub async fn create_token(&self, name: &str, permissions: Permissions) -> Result<String> {
84        let token = self
85            .create_token_builder(name)
86            .permissions(permissions)
87            .send()
88            .await?;
89        Ok(token.value)
90    }
91
92    /// Create an access token with optional settings such as expiration, inactivity TTL,
93    /// and IP allowlist.
94    pub fn create_token_builder(&self, name: &str) -> CreateTokenBuilder {
95        CreateTokenBuilder::new(name.to_string(), Arc::clone(&self.http_client))
96    }
97
98    /// Rotate an access token value and revoke the old one.
99    pub async fn rotate_token(&self, name: &str) -> Result<TokenCreateResponse> {
100        self.http_client
101            .send_and_receive_json::<(), TokenCreateResponse>(
102                Method::POST,
103                &format!("/tokens/{}/rotate", name),
104                None,
105            )
106            .await
107    }
108
109    /// Get an access token
110    ///
111    /// # Arguments
112    ///
113    /// * `name` - The name of the token
114    ///
115    /// # Returns
116    ///
117    /// The token or an error
118    pub async fn get_token(&self, name: &str) -> Result<Token> {
119        self.http_client
120            .send_and_receive_json::<(), Token>(Method::GET, &format!("/tokens/{}", name), None)
121            .await
122    }
123
124    /// Delete an access token
125    ///
126    /// # Arguments
127    ///
128    /// * `name` - The name of the token
129    ///
130    /// # Returns
131    ///
132    /// Ok if the token was deleted, otherwise an error
133    pub async fn delete_token(&self, name: &str) -> Result<()> {
134        let request = self
135            .http_client
136            .request(Method::DELETE, &format!("/tokens/{}", name));
137        self.http_client.send_request(request).await?;
138        Ok(())
139    }
140
141    /// List all access tokens
142    ///
143    /// # Returns
144    ///
145    /// The list of tokens or an error
146    pub async fn list_tokens(&self) -> Result<Vec<Token>> {
147        let list = self
148            .http_client
149            .send_and_receive_json::<(), TokenList>(Method::GET, "/tokens", None)
150            .await?;
151        Ok(list.tokens)
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use crate::client::tests::client;
159    use rstest::rstest;
160
161    #[rstest]
162    #[tokio::test]
163    async fn test_me(#[future] client: ReductClient) {
164        let token = client.await.me().await.unwrap();
165        assert_eq!(token.name, "init-token");
166        assert!(token.permissions.unwrap().full_access);
167    }
168
169    #[rstest]
170    #[tokio::test]
171    async fn test_me_info(#[future] client: ReductClient) {
172        let token = client.await.me_info().await.unwrap();
173        assert_eq!(token.name, "init-token");
174        assert!(token.permissions.unwrap().full_access);
175    }
176
177    #[rstest]
178    #[tokio::test]
179    async fn test_create_token(#[future] client: ReductClient) {
180        let token_value = client
181            .await
182            .create_token(
183                "test-token",
184                Permissions {
185                    full_access: false,
186                    read: vec!["test-bucket".to_string()],
187                    write: vec!["test-bucket".to_string()],
188                },
189            )
190            .await
191            .unwrap();
192
193        assert!(token_value.starts_with("test-token"));
194    }
195
196    #[cfg(feature = "test-api-119")]
197    #[rstest]
198    #[tokio::test]
199    async fn test_create_token_builder(#[future] client: ReductClient) {
200        let token = client
201            .await
202            .create_token_builder("test-token-options")
203            .permissions(Permissions {
204                full_access: false,
205                read: vec!["test-bucket".to_string()],
206                write: vec!["test-bucket".to_string()],
207            })
208            .ttl(Duration::from_secs(3600))
209            .ip_allowlist(vec!["127.0.0.1".to_string()])
210            .send()
211            .await
212            .unwrap();
213
214        assert!(token.value.starts_with("test-token-options"));
215    }
216
217    #[rstest]
218    #[tokio::test]
219    async fn test_get_token(#[future] client: ReductClient) {
220        let token = client.await.get_token("init-token").await.unwrap();
221        assert_eq!(token.name, "init-token");
222        assert!(token.is_provisioned);
223
224        let permissions = token.permissions.unwrap();
225        assert!(permissions.full_access);
226        assert!(permissions.read.is_empty());
227        assert!(permissions.write.is_empty());
228    }
229
230    #[rstest]
231    #[tokio::test]
232    async fn test_list_tokens(#[future] client: ReductClient) {
233        let tokens = client.await.list_tokens().await.unwrap();
234        assert!(!tokens.is_empty());
235    }
236
237    #[cfg(feature = "test-api-119")]
238    #[rstest]
239    #[tokio::test]
240    async fn test_rotate_token(#[future] client: ReductClient) {
241        let client = client.await;
242        client
243            .create_token("test-token-rotate", Permissions::default())
244            .await
245            .unwrap();
246        let rotated = client.rotate_token("test-token-rotate").await.unwrap();
247        assert!(rotated.value.starts_with("test-token-rotate"));
248        client.delete_token("test-token-rotate").await.unwrap();
249    }
250
251    #[rstest]
252    #[tokio::test]
253    async fn delete_token(#[future] client: ReductClient) {
254        let client = client.await;
255        client
256            .create_token("test-token", Permissions::default())
257            .await
258            .unwrap();
259        client.delete_token("test-token").await.unwrap();
260    }
261}