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 {api_key_id} not found"
244            ))),
245        }
246    }
247
248    async fn delete_by_id(&self, id: &str) -> Result<(), RepositoryError> {
249        if id.is_empty() {
250            return Err(RepositoryError::InvalidData(
251                "API Key ID cannot be empty".to_string(),
252            ));
253        }
254
255        let key = self.api_key_key(id);
256        let api_key_list_key = self.api_key_list_key();
257        let mut conn = self.client.as_ref().clone();
258
259        debug!("Deleting api key with ID: {}", id);
260
261        // Check if api key exists
262        let existing: Option<String> = conn
263            .get(&key)
264            .await
265            .map_err(|e| self.map_redis_error(e, "delete_api_key_check"))?;
266
267        if existing.is_none() {
268            return Err(RepositoryError::NotFound(format!(
269                "Api key with ID {id} not found"
270            )));
271        }
272
273        // Use atomic pipeline to ensure consistency
274        let mut pipe = redis::pipe();
275        pipe.atomic();
276        pipe.del(&key);
277        pipe.srem(&api_key_list_key, id);
278
279        pipe.exec_async(&mut conn)
280            .await
281            .map_err(|e| self.map_redis_error(e, "delete_api_key"))?;
282
283        debug!("Successfully deleted api key {}", id);
284        Ok(())
285    }
286
287    async fn count(&self) -> Result<usize, RepositoryError> {
288        let mut conn = self.client.as_ref().clone();
289        let api_key_list_key = self.api_key_list_key();
290
291        let count: u64 = conn
292            .scard(&api_key_list_key)
293            .await
294            .map_err(|e| self.map_redis_error(e, "count_api_keys"))?;
295
296        Ok(count as usize)
297    }
298
299    async fn has_entries(&self) -> Result<bool, RepositoryError> {
300        let mut conn = self.client.as_ref().clone();
301        let plugin_list_key = self.api_key_list_key();
302
303        debug!("Checking if plugin entries exist");
304
305        let exists: bool = conn
306            .exists(&plugin_list_key)
307            .await
308            .map_err(|e| self.map_redis_error(e, "has_entries_check"))?;
309
310        debug!("Plugin entries exist: {}", exists);
311        Ok(exists)
312    }
313
314    async fn drop_all_entries(&self) -> Result<(), RepositoryError> {
315        let mut conn = self.client.as_ref().clone();
316        let plugin_list_key = self.api_key_list_key();
317
318        debug!("Dropping all plugin entries");
319
320        // Get all plugin IDs first
321        let plugin_ids: Vec<String> = conn
322            .smembers(&plugin_list_key)
323            .await
324            .map_err(|e| self.map_redis_error(e, "drop_all_entries_get_ids"))?;
325
326        if plugin_ids.is_empty() {
327            debug!("No plugin entries to drop");
328            return Ok(());
329        }
330
331        // Use pipeline for atomic operations
332        let mut pipe = redis::pipe();
333        pipe.atomic();
334
335        // Delete all individual plugin entries
336        for plugin_id in &plugin_ids {
337            let plugin_key = self.api_key_key(plugin_id);
338            pipe.del(&plugin_key);
339        }
340
341        // Delete the plugin list key
342        pipe.del(&plugin_list_key);
343
344        pipe.exec_async(&mut conn)
345            .await
346            .map_err(|e| self.map_redis_error(e, "drop_all_entries_pipeline"))?;
347
348        debug!("Dropped {} plugin entries", plugin_ids.len());
349        Ok(())
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use crate::models::SecretString;
356
357    use super::*;
358    use chrono::Utc;
359
360    fn create_test_api_key(id: &str) -> ApiKeyRepoModel {
361        ApiKeyRepoModel {
362            id: id.to_string(),
363            value: SecretString::new("test-value"),
364            name: "test-name".to_string(),
365            allowed_origins: vec!["*".to_string()],
366            permissions: vec!["relayer:all:execute".to_string()],
367            created_at: Utc::now().to_string(),
368        }
369    }
370
371    async fn setup_test_repo() -> RedisApiKeyRepository {
372        let redis_url =
373            std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379/".to_string());
374        let client = redis::Client::open(redis_url).expect("Failed to create Redis client");
375        let mut connection_manager = ConnectionManager::new(client)
376            .await
377            .expect("Failed to create Redis connection manager");
378
379        // Clear the api key list
380        connection_manager
381            .del::<&str, ()>("test_api_key:apikey_list")
382            .await
383            .unwrap();
384
385        RedisApiKeyRepository::new(Arc::new(connection_manager), "test_api_key".to_string())
386            .expect("Failed to create Redis api key repository")
387    }
388
389    #[tokio::test]
390    #[ignore = "Requires active Redis instance"]
391    async fn test_new_repository_creation() {
392        let repo = setup_test_repo().await;
393        assert_eq!(repo.key_prefix, "test_api_key");
394    }
395
396    #[tokio::test]
397    #[ignore = "Requires active Redis instance"]
398    async fn test_new_repository_empty_prefix_fails() {
399        let client =
400            redis::Client::open("redis://127.0.0.1:6379/").expect("Failed to create Redis client");
401        let connection_manager = redis::aio::ConnectionManager::new(client)
402            .await
403            .expect("Failed to create Redis connection manager");
404
405        let result = RedisApiKeyRepository::new(Arc::new(connection_manager), "".to_string());
406        assert!(result.is_err());
407        assert!(result
408            .unwrap_err()
409            .to_string()
410            .contains("key prefix cannot be empty"));
411    }
412
413    #[tokio::test]
414    #[ignore = "Requires active Redis instance"]
415    async fn test_key_generation() {
416        let repo = setup_test_repo().await;
417
418        let api_key_key = repo.api_key_key("test-api-key");
419        assert_eq!(api_key_key, "test_api_key:apikey:test-api-key");
420
421        let list_key = repo.api_key_list_key();
422        assert_eq!(list_key, "test_api_key:apikey_list");
423    }
424
425    #[tokio::test]
426    #[ignore = "Requires active Redis instance"]
427    async fn test_serialize_deserialize_api_key() {
428        let repo = setup_test_repo().await;
429        let api_key = create_test_api_key("test-api-key");
430
431        let json = repo
432            .serialize_entity(&api_key, |a| &a.id, "apikey")
433            .unwrap();
434        let deserialized: ApiKeyRepoModel = repo
435            .deserialize_entity(&json, &api_key.id, "apikey")
436            .unwrap();
437
438        assert_eq!(api_key.id, deserialized.id);
439        assert_eq!(api_key.value, deserialized.value);
440        assert_eq!(api_key.name, deserialized.name);
441        assert_eq!(api_key.allowed_origins, deserialized.allowed_origins);
442        assert_eq!(api_key.permissions, deserialized.permissions);
443        assert_eq!(api_key.created_at, deserialized.created_at);
444    }
445
446    #[tokio::test]
447    #[ignore = "Requires active Redis instance"]
448    async fn test_create_api_key() {
449        let repo = setup_test_repo().await;
450        let api_key_id = uuid::Uuid::new_v4().to_string();
451        let api_key = create_test_api_key(&api_key_id);
452
453        let result = repo.create(api_key.clone()).await;
454        assert!(result.is_ok());
455
456        let retrieved = repo.get_by_id(&api_key_id).await.unwrap();
457        assert!(retrieved.is_some());
458        let retrieved = retrieved.unwrap();
459        assert_eq!(retrieved.id, api_key.id);
460        assert_eq!(retrieved.value, api_key.value);
461    }
462
463    #[tokio::test]
464    #[ignore = "Requires active Redis instance"]
465    async fn test_get_nonexistent_api_key() {
466        let repo = setup_test_repo().await;
467
468        let result = repo.get_by_id("nonexistent-api-key").await;
469        assert!(matches!(result, Ok(None)));
470    }
471
472    #[tokio::test]
473    #[ignore = "Requires active Redis instance"]
474    async fn test_error_handling_empty_id() {
475        let repo = setup_test_repo().await;
476
477        let result = repo.get_by_id("").await;
478        assert!(result.is_err());
479        assert!(result
480            .unwrap_err()
481            .to_string()
482            .contains("ID cannot be empty"));
483    }
484
485    #[tokio::test]
486    #[ignore = "Requires active Redis instance"]
487    async fn test_get_by_ids_api_keys() {
488        let repo = setup_test_repo().await;
489        let api_key_id1 = uuid::Uuid::new_v4().to_string();
490        let api_key_id2 = uuid::Uuid::new_v4().to_string();
491        let api_key1 = create_test_api_key(&api_key_id1);
492        let api_key2 = create_test_api_key(&api_key_id2);
493
494        repo.create(api_key1.clone()).await.unwrap();
495        repo.create(api_key2.clone()).await.unwrap();
496
497        let retrieved = repo
498            .get_by_ids(&[api_key1.id.clone(), api_key2.id.clone()])
499            .await
500            .unwrap();
501        assert!(retrieved.results.len() == 2);
502        assert_eq!(retrieved.results[0].id, api_key1.id);
503        assert_eq!(retrieved.results[1].id, api_key2.id);
504        assert_eq!(retrieved.failed_ids.len(), 0);
505    }
506
507    #[tokio::test]
508    #[ignore = "Requires active Redis instance"]
509    async fn test_list_paginated_api_keys() {
510        let repo = setup_test_repo().await;
511
512        let api_key_id1 = uuid::Uuid::new_v4().to_string();
513        let api_key_id2 = uuid::Uuid::new_v4().to_string();
514        let api_key_id3 = uuid::Uuid::new_v4().to_string();
515        let api_key1 = create_test_api_key(&api_key_id1);
516        let api_key2 = create_test_api_key(&api_key_id2);
517        let api_key3 = create_test_api_key(&api_key_id3);
518
519        repo.create(api_key1.clone()).await.unwrap();
520        repo.create(api_key2.clone()).await.unwrap();
521        repo.create(api_key3.clone()).await.unwrap();
522
523        let query = PaginationQuery {
524            page: 1,
525            per_page: 2,
526        };
527
528        let result = repo.list_paginated(query).await;
529        assert!(result.is_ok());
530        let result = result.unwrap();
531        println!("result: {:?}", result);
532        assert!(result.items.len() == 2);
533    }
534
535    #[tokio::test]
536    #[ignore = "Requires active Redis instance"]
537    async fn test_has_entries() {
538        let repo = setup_test_repo().await;
539        assert!(!repo.has_entries().await.unwrap());
540        repo.create(create_test_api_key("test-api-key"))
541            .await
542            .unwrap();
543        assert!(repo.has_entries().await.unwrap());
544        repo.drop_all_entries().await.unwrap();
545        assert!(!repo.has_entries().await.unwrap());
546    }
547}