openzeppelin_relayer/repositories/api_key/
api_key_redis.rs

1//! Redis-backed implementation of the ApiKeyRepository.
2
3use crate::models::{ApiKeyRepoModel, PaginationQuery, RepositoryError};
4use crate::repositories::redis_base::RedisRepository;
5use crate::repositories::{ApiKeyRepositoryTrait, BatchRetrievalResult, PaginatedResult};
6use async_trait::async_trait;
7use redis::aio::ConnectionManager;
8use redis::AsyncCommands;
9use std::fmt;
10use std::sync::Arc;
11use tracing::{debug, error, warn};
12
13const API_KEY_PREFIX: &str = "apikey";
14const API_KEY_LIST_KEY: &str = "apikey_list";
15
16#[derive(Clone)]
17pub struct RedisApiKeyRepository {
18    pub client: Arc<ConnectionManager>,
19    pub key_prefix: String,
20}
21
22impl RedisRepository for RedisApiKeyRepository {}
23
24impl RedisApiKeyRepository {
25    pub fn new(
26        connection_manager: Arc<ConnectionManager>,
27        key_prefix: String,
28    ) -> Result<Self, RepositoryError> {
29        if key_prefix.is_empty() {
30            return Err(RepositoryError::InvalidData(
31                "Redis key prefix cannot be empty".to_string(),
32            ));
33        }
34
35        Ok(Self {
36            client: connection_manager,
37            key_prefix,
38        })
39    }
40
41    /// Generate key for api key data: apikey:{api_key_id}
42    fn api_key_key(&self, api_key_id: &str) -> String {
43        format!("{}:{}:{}", self.key_prefix, API_KEY_PREFIX, api_key_id)
44    }
45
46    /// Generate key for api key list: apikey_list (paginated list of api key IDs)
47    fn api_key_list_key(&self) -> String {
48        format!("{}:{}", self.key_prefix, API_KEY_LIST_KEY)
49    }
50
51    async fn get_by_ids(
52        &self,
53        ids: &[String],
54    ) -> Result<BatchRetrievalResult<ApiKeyRepoModel>, RepositoryError> {
55        if ids.is_empty() {
56            debug!("No api key IDs provided for batch fetch");
57            return Ok(BatchRetrievalResult {
58                results: vec![],
59                failed_ids: vec![],
60            });
61        }
62
63        let mut conn = self.client.as_ref().clone();
64        let keys: Vec<String> = ids.iter().map(|id| self.api_key_key(id)).collect();
65
66        let values: Vec<Option<String>> = conn
67            .mget(&keys)
68            .await
69            .map_err(|e| self.map_redis_error(e, "batch_fetch_api_keys"))?;
70
71        let mut apikeys = Vec::new();
72        let mut failed_count = 0;
73        let mut failed_ids = Vec::new();
74        for (i, value) in values.into_iter().enumerate() {
75            match value {
76                Some(json) => match self.deserialize_entity(&json, &ids[i], "apikey") {
77                    Ok(apikey) => apikeys.push(apikey),
78                    Err(e) => {
79                        failed_count += 1;
80                        error!("Failed to deserialize api key {}: {}", ids[i], e);
81                        failed_ids.push(ids[i].clone());
82                    }
83                },
84                None => {
85                    warn!("Plugin {} not found in batch fetch", ids[i]);
86                }
87            }
88        }
89
90        if failed_count > 0 {
91            warn!(
92                "Failed to deserialize {} out of {} api keys in batch",
93                failed_count,
94                ids.len()
95            );
96        }
97
98        Ok(BatchRetrievalResult {
99            results: apikeys,
100            failed_ids,
101        })
102    }
103}
104
105impl fmt::Debug for RedisApiKeyRepository {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        write!(
108            f,
109            "RedisApiKeyRepository {{ key_prefix: {} }}",
110            self.key_prefix
111        )
112    }
113}
114
115#[async_trait]
116impl ApiKeyRepositoryTrait for RedisApiKeyRepository {
117    async fn create(&self, entity: ApiKeyRepoModel) -> Result<ApiKeyRepoModel, RepositoryError> {
118        if entity.id.is_empty() {
119            return Err(RepositoryError::InvalidData(
120                "API Key ID cannot be empty".to_string(),
121            ));
122        }
123
124        let key = self.api_key_key(&entity.id);
125        let list_key = self.api_key_list_key();
126        let json = self.serialize_entity(&entity, |a| &a.id, "apikey")?;
127
128        let mut conn = self.client.as_ref().clone();
129
130        let existing: Option<String> = conn
131            .get(&key)
132            .await
133            .map_err(|e| self.map_redis_error(e, "create_api_key_check"))?;
134
135        if existing.is_some() {
136            return Err(RepositoryError::ConstraintViolation(format!(
137                "API Key with ID {} already exists",
138                entity.id
139            )));
140        }
141
142        // Use atomic pipeline for consistency
143        let mut pipe = redis::pipe();
144        pipe.atomic();
145        pipe.set(&key, json);
146        pipe.sadd(&list_key, &entity.id);
147
148        pipe.exec_async(&mut conn)
149            .await
150            .map_err(|e| self.map_redis_error(e, "create_api_key"))?;
151
152        debug!("Successfully created API Key {}", entity.id);
153        Ok(entity)
154    }
155
156    async fn list_paginated(
157        &self,
158        query: PaginationQuery,
159    ) -> Result<PaginatedResult<ApiKeyRepoModel>, RepositoryError> {
160        if query.page == 0 {
161            return Err(RepositoryError::InvalidData(
162                "Page number must be greater than 0".to_string(),
163            ));
164        }
165
166        if query.per_page == 0 {
167            return Err(RepositoryError::InvalidData(
168                "Per page count must be greater than 0".to_string(),
169            ));
170        }
171        let mut conn = self.client.as_ref().clone();
172        let api_key_list_key = self.api_key_list_key();
173
174        // Get total count
175        let total: u64 = conn
176            .scard(&api_key_list_key)
177            .await
178            .map_err(|e| self.map_redis_error(e, "list_paginated_count"))?;
179
180        if total == 0 {
181            return Ok(PaginatedResult {
182                items: vec![],
183                total: 0,
184                page: query.page,
185                per_page: query.per_page,
186            });
187        }
188
189        // Get all IDs and paginate in memory
190        let all_ids: Vec<String> = conn
191            .smembers(&api_key_list_key)
192            .await
193            .map_err(|e| self.map_redis_error(e, "list_paginated_members"))?;
194
195        let start = ((query.page - 1) * query.per_page) as usize;
196        let end = (start + query.per_page as usize).min(all_ids.len());
197
198        let ids_to_query = &all_ids[start..end];
199        let items = self.get_by_ids(ids_to_query).await?;
200
201        Ok(PaginatedResult {
202            items: items.results.clone(),
203            total,
204            page: query.page,
205            per_page: query.per_page,
206        })
207    }
208
209    async fn get_by_id(&self, id: &str) -> Result<Option<ApiKeyRepoModel>, RepositoryError> {
210        if id.is_empty() {
211            return Err(RepositoryError::InvalidData(
212                "API Key ID cannot be empty".to_string(),
213            ));
214        }
215
216        let mut conn = self.client.as_ref().clone();
217        let api_key_key = self.api_key_key(id);
218
219        debug!("Fetching api key with ID: {}", id);
220
221        let json: Option<String> = conn
222            .get(&api_key_key)
223            .await
224            .map_err(|e| self.map_redis_error(e, "get_api_key_by_id"))?;
225
226        match json {
227            Some(json) => {
228                debug!("Found api key with ID: {}", id);
229                self.deserialize_entity(&json, id, "apikey")
230            }
231            None => {
232                debug!("Api key with ID {} not found", id);
233                Ok(None)
234            }
235        }
236    }
237
238    async fn list_permissions(&self, api_key_id: &str) -> Result<Vec<String>, RepositoryError> {
239        let api_key = self.get_by_id(api_key_id).await?;
240        match api_key {
241            Some(api_key) => Ok(api_key.permissions),
242            None => Err(RepositoryError::NotFound(format!(
243                "Api key with ID {} not found",
244                api_key_id
245            ))),
246        }
247    }
248
249    async fn delete_by_id(&self, id: &str) -> Result<(), RepositoryError> {
250        if id.is_empty() {
251            return Err(RepositoryError::InvalidData(
252                "API Key ID cannot be empty".to_string(),
253            ));
254        }
255
256        let key = self.api_key_key(id);
257        let api_key_list_key = self.api_key_list_key();
258        let mut conn = self.client.as_ref().clone();
259
260        debug!("Deleting api key with ID: {}", id);
261
262        // Check if api key exists
263        let existing: Option<String> = conn
264            .get(&key)
265            .await
266            .map_err(|e| self.map_redis_error(e, "delete_api_key_check"))?;
267
268        if existing.is_none() {
269            return Err(RepositoryError::NotFound(format!(
270                "Api key with ID {} not found",
271                id
272            )));
273        }
274
275        // Use atomic pipeline to ensure consistency
276        let mut pipe = redis::pipe();
277        pipe.atomic();
278        pipe.del(&key);
279        pipe.srem(&api_key_list_key, id);
280
281        pipe.exec_async(&mut conn)
282            .await
283            .map_err(|e| self.map_redis_error(e, "delete_api_key"))?;
284
285        debug!("Successfully deleted api key {}", id);
286        Ok(())
287    }
288
289    async fn count(&self) -> Result<usize, RepositoryError> {
290        let mut conn = self.client.as_ref().clone();
291        let api_key_list_key = self.api_key_list_key();
292
293        let count: u64 = conn
294            .scard(&api_key_list_key)
295            .await
296            .map_err(|e| self.map_redis_error(e, "count_api_keys"))?;
297
298        Ok(count as usize)
299    }
300
301    async fn has_entries(&self) -> Result<bool, RepositoryError> {
302        let mut conn = self.client.as_ref().clone();
303        let plugin_list_key = self.api_key_list_key();
304
305        debug!("Checking if plugin entries exist");
306
307        let exists: bool = conn
308            .exists(&plugin_list_key)
309            .await
310            .map_err(|e| self.map_redis_error(e, "has_entries_check"))?;
311
312        debug!("Plugin entries exist: {}", exists);
313        Ok(exists)
314    }
315
316    async fn drop_all_entries(&self) -> Result<(), RepositoryError> {
317        let mut conn = self.client.as_ref().clone();
318        let plugin_list_key = self.api_key_list_key();
319
320        debug!("Dropping all plugin entries");
321
322        // Get all plugin IDs first
323        let plugin_ids: Vec<String> = conn
324            .smembers(&plugin_list_key)
325            .await
326            .map_err(|e| self.map_redis_error(e, "drop_all_entries_get_ids"))?;
327
328        if plugin_ids.is_empty() {
329            debug!("No plugin entries to drop");
330            return Ok(());
331        }
332
333        // Use pipeline for atomic operations
334        let mut pipe = redis::pipe();
335        pipe.atomic();
336
337        // Delete all individual plugin entries
338        for plugin_id in &plugin_ids {
339            let plugin_key = self.api_key_key(plugin_id);
340            pipe.del(&plugin_key);
341        }
342
343        // Delete the plugin list key
344        pipe.del(&plugin_list_key);
345
346        pipe.exec_async(&mut conn)
347            .await
348            .map_err(|e| self.map_redis_error(e, "drop_all_entries_pipeline"))?;
349
350        debug!("Dropped {} plugin entries", plugin_ids.len());
351        Ok(())
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use crate::models::SecretString;
358
359    use super::*;
360    use chrono::Utc;
361
362    fn create_test_api_key(id: &str) -> ApiKeyRepoModel {
363        ApiKeyRepoModel {
364            id: id.to_string(),
365            value: SecretString::new("test-value"),
366            name: "test-name".to_string(),
367            allowed_origins: vec!["*".to_string()],
368            permissions: vec!["relayer:all:execute".to_string()],
369            created_at: Utc::now().to_string(),
370        }
371    }
372
373    async fn setup_test_repo() -> RedisApiKeyRepository {
374        let redis_url =
375            std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379/".to_string());
376        let client = redis::Client::open(redis_url).expect("Failed to create Redis client");
377        let mut connection_manager = ConnectionManager::new(client)
378            .await
379            .expect("Failed to create Redis connection manager");
380
381        // Clear the api key list
382        connection_manager
383            .del::<&str, ()>("test_api_key:apikey_list")
384            .await
385            .unwrap();
386
387        RedisApiKeyRepository::new(Arc::new(connection_manager), "test_api_key".to_string())
388            .expect("Failed to create Redis api key repository")
389    }
390
391    #[tokio::test]
392    #[ignore = "Requires active Redis instance"]
393    async fn test_new_repository_creation() {
394        let repo = setup_test_repo().await;
395        assert_eq!(repo.key_prefix, "test_api_key");
396    }
397
398    #[tokio::test]
399    #[ignore = "Requires active Redis instance"]
400    async fn test_new_repository_empty_prefix_fails() {
401        let client =
402            redis::Client::open("redis://127.0.0.1:6379/").expect("Failed to create Redis client");
403        let connection_manager = redis::aio::ConnectionManager::new(client)
404            .await
405            .expect("Failed to create Redis connection manager");
406
407        let result = RedisApiKeyRepository::new(Arc::new(connection_manager), "".to_string());
408        assert!(result.is_err());
409        assert!(result
410            .unwrap_err()
411            .to_string()
412            .contains("key prefix cannot be empty"));
413    }
414
415    #[tokio::test]
416    #[ignore = "Requires active Redis instance"]
417    async fn test_key_generation() {
418        let repo = setup_test_repo().await;
419
420        let api_key_key = repo.api_key_key("test-api-key");
421        assert_eq!(api_key_key, "test_api_key:apikey:test-api-key");
422
423        let list_key = repo.api_key_list_key();
424        assert_eq!(list_key, "test_api_key:apikey_list");
425    }
426
427    #[tokio::test]
428    #[ignore = "Requires active Redis instance"]
429    async fn test_serialize_deserialize_api_key() {
430        let repo = setup_test_repo().await;
431        let api_key = create_test_api_key("test-api-key");
432
433        let json = repo
434            .serialize_entity(&api_key, |a| &a.id, "apikey")
435            .unwrap();
436        let deserialized: ApiKeyRepoModel = repo
437            .deserialize_entity(&json, &api_key.id, "apikey")
438            .unwrap();
439
440        assert_eq!(api_key.id, deserialized.id);
441        assert_eq!(api_key.value, deserialized.value);
442        assert_eq!(api_key.name, deserialized.name);
443        assert_eq!(api_key.allowed_origins, deserialized.allowed_origins);
444        assert_eq!(api_key.permissions, deserialized.permissions);
445        assert_eq!(api_key.created_at, deserialized.created_at);
446    }
447
448    #[tokio::test]
449    #[ignore = "Requires active Redis instance"]
450    async fn test_create_api_key() {
451        let repo = setup_test_repo().await;
452        let api_key_id = uuid::Uuid::new_v4().to_string();
453        let api_key = create_test_api_key(&api_key_id);
454
455        let result = repo.create(api_key.clone()).await;
456        assert!(result.is_ok());
457
458        let retrieved = repo.get_by_id(&api_key_id).await.unwrap();
459        assert!(retrieved.is_some());
460        let retrieved = retrieved.unwrap();
461        assert_eq!(retrieved.id, api_key.id);
462        assert_eq!(retrieved.value, api_key.value);
463    }
464
465    #[tokio::test]
466    #[ignore = "Requires active Redis instance"]
467    async fn test_get_nonexistent_api_key() {
468        let repo = setup_test_repo().await;
469
470        let result = repo.get_by_id("nonexistent-api-key").await;
471        assert!(matches!(result, Ok(None)));
472    }
473
474    #[tokio::test]
475    #[ignore = "Requires active Redis instance"]
476    async fn test_error_handling_empty_id() {
477        let repo = setup_test_repo().await;
478
479        let result = repo.get_by_id("").await;
480        assert!(result.is_err());
481        assert!(result
482            .unwrap_err()
483            .to_string()
484            .contains("ID cannot be empty"));
485    }
486
487    #[tokio::test]
488    #[ignore = "Requires active Redis instance"]
489    async fn test_get_by_ids_api_keys() {
490        let repo = setup_test_repo().await;
491        let api_key_id1 = uuid::Uuid::new_v4().to_string();
492        let api_key_id2 = uuid::Uuid::new_v4().to_string();
493        let api_key1 = create_test_api_key(&api_key_id1);
494        let api_key2 = create_test_api_key(&api_key_id2);
495
496        repo.create(api_key1.clone()).await.unwrap();
497        repo.create(api_key2.clone()).await.unwrap();
498
499        let retrieved = repo
500            .get_by_ids(&[api_key1.id.clone(), api_key2.id.clone()])
501            .await
502            .unwrap();
503        assert!(retrieved.results.len() == 2);
504        assert_eq!(retrieved.results[0].id, api_key1.id);
505        assert_eq!(retrieved.results[1].id, api_key2.id);
506        assert_eq!(retrieved.failed_ids.len(), 0);
507    }
508
509    #[tokio::test]
510    #[ignore = "Requires active Redis instance"]
511    async fn test_list_paginated_api_keys() {
512        let repo = setup_test_repo().await;
513
514        let api_key_id1 = uuid::Uuid::new_v4().to_string();
515        let api_key_id2 = uuid::Uuid::new_v4().to_string();
516        let api_key_id3 = uuid::Uuid::new_v4().to_string();
517        let api_key1 = create_test_api_key(&api_key_id1);
518        let api_key2 = create_test_api_key(&api_key_id2);
519        let api_key3 = create_test_api_key(&api_key_id3);
520
521        repo.create(api_key1.clone()).await.unwrap();
522        repo.create(api_key2.clone()).await.unwrap();
523        repo.create(api_key3.clone()).await.unwrap();
524
525        let query = PaginationQuery {
526            page: 1,
527            per_page: 2,
528        };
529
530        let result = repo.list_paginated(query).await;
531        assert!(result.is_ok());
532        let result = result.unwrap();
533        println!("result: {:?}", result);
534        assert!(result.items.len() == 2);
535    }
536
537    #[tokio::test]
538    #[ignore = "Requires active Redis instance"]
539    async fn test_has_entries() {
540        let repo = setup_test_repo().await;
541        assert!(!repo.has_entries().await.unwrap());
542        repo.create(create_test_api_key("test-api-key"))
543            .await
544            .unwrap();
545        assert!(repo.has_entries().await.unwrap());
546        repo.drop_all_entries().await.unwrap();
547        assert!(!repo.has_entries().await.unwrap());
548    }
549}