openzeppelin_relayer/repositories/api_key/
api_key_redis.rs1use 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 fn api_key_key(&self, api_key_id: &str) -> String {
43 format!("{}:{}:{}", self.key_prefix, API_KEY_PREFIX, api_key_id)
44 }
45
46 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 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 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 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 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 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 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 let mut pipe = redis::pipe();
335 pipe.atomic();
336
337 for plugin_id in &plugin_ids {
339 let plugin_key = self.api_key_key(plugin_id);
340 pipe.del(&plugin_key);
341 }
342
343 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 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}