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 {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 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 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 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 let mut pipe = redis::pipe();
333 pipe.atomic();
334
335 for plugin_id in &plugin_ids {
337 let plugin_key = self.api_key_key(plugin_id);
338 pipe.del(&plugin_key);
339 }
340
341 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 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}