openzeppelin_relayer/repositories/api_key/
mod.rs

1//! API Key Repository Module
2//!
3//! This module provides the api key repository for the OpenZeppelin Relayer service.
4//! It implements a specialized repository pattern for managing api key configurations,
5//! supporting both in-memory and Redis-backed storage implementations.
6//!
7//! ## Repository Implementations
8//!
9//! - [`InMemoryApiKeyRepository`]: In-memory storage for testing/development
10//! - [`RedisApiKeyRepository`]: Redis-backed storage for production environments
11//!
12//! ## API Keys
13//!
14//! The api key system allows extending relayer authorization scheme through api keys.
15//! Each api key is identified by a unique ID and contains a list of permissions that
16//! restrict the api key's access to the server.
17//!
18use async_trait::async_trait;
19use redis::aio::ConnectionManager;
20use std::sync::Arc;
21
22pub mod api_key_in_memory;
23pub mod api_key_redis;
24
25pub use api_key_in_memory::*;
26pub use api_key_redis::*;
27
28#[cfg(test)]
29use mockall::automock;
30
31use crate::{
32    models::{ApiKeyRepoModel, PaginationQuery, RepositoryError},
33    repositories::PaginatedResult,
34};
35
36#[async_trait]
37#[allow(dead_code)]
38#[cfg_attr(test, automock)]
39pub trait ApiKeyRepositoryTrait {
40    async fn get_by_id(&self, id: &str) -> Result<Option<ApiKeyRepoModel>, RepositoryError>;
41    async fn create(&self, api_key: ApiKeyRepoModel) -> Result<ApiKeyRepoModel, RepositoryError>;
42    async fn list_paginated(
43        &self,
44        query: PaginationQuery,
45    ) -> Result<PaginatedResult<ApiKeyRepoModel>, RepositoryError>;
46    async fn count(&self) -> Result<usize, RepositoryError>;
47    async fn list_permissions(&self, api_key_id: &str) -> Result<Vec<String>, RepositoryError>;
48    async fn delete_by_id(&self, api_key_id: &str) -> Result<(), RepositoryError>;
49    async fn has_entries(&self) -> Result<bool, RepositoryError>;
50    async fn drop_all_entries(&self) -> Result<(), RepositoryError>;
51}
52
53/// Enum wrapper for different plugin repository implementations
54#[derive(Debug, Clone)]
55pub enum ApiKeyRepositoryStorage {
56    InMemory(InMemoryApiKeyRepository),
57    Redis(RedisApiKeyRepository),
58}
59
60impl ApiKeyRepositoryStorage {
61    pub fn new_in_memory() -> Self {
62        Self::InMemory(InMemoryApiKeyRepository::new())
63    }
64
65    pub fn new_redis(
66        connection_manager: Arc<ConnectionManager>,
67        key_prefix: String,
68    ) -> Result<Self, RepositoryError> {
69        let redis_repo = RedisApiKeyRepository::new(connection_manager, key_prefix)?;
70        Ok(Self::Redis(redis_repo))
71    }
72}
73
74#[async_trait]
75impl ApiKeyRepositoryTrait for ApiKeyRepositoryStorage {
76    async fn get_by_id(&self, id: &str) -> Result<Option<ApiKeyRepoModel>, RepositoryError> {
77        match self {
78            ApiKeyRepositoryStorage::InMemory(repo) => repo.get_by_id(id).await,
79            ApiKeyRepositoryStorage::Redis(repo) => repo.get_by_id(id).await,
80        }
81    }
82
83    async fn create(&self, api_key: ApiKeyRepoModel) -> Result<ApiKeyRepoModel, RepositoryError> {
84        match self {
85            ApiKeyRepositoryStorage::InMemory(repo) => repo.create(api_key).await,
86            ApiKeyRepositoryStorage::Redis(repo) => repo.create(api_key).await,
87        }
88    }
89
90    async fn list_permissions(&self, api_key_id: &str) -> Result<Vec<String>, RepositoryError> {
91        match self {
92            ApiKeyRepositoryStorage::InMemory(repo) => repo.list_permissions(api_key_id).await,
93            ApiKeyRepositoryStorage::Redis(repo) => repo.list_permissions(api_key_id).await,
94        }
95    }
96
97    async fn delete_by_id(&self, api_key_id: &str) -> Result<(), RepositoryError> {
98        match self {
99            ApiKeyRepositoryStorage::InMemory(repo) => repo.delete_by_id(api_key_id).await,
100            ApiKeyRepositoryStorage::Redis(repo) => repo.delete_by_id(api_key_id).await,
101        }
102    }
103
104    async fn list_paginated(
105        &self,
106        query: PaginationQuery,
107    ) -> Result<PaginatedResult<ApiKeyRepoModel>, RepositoryError> {
108        match self {
109            ApiKeyRepositoryStorage::InMemory(repo) => repo.list_paginated(query).await,
110            ApiKeyRepositoryStorage::Redis(repo) => repo.list_paginated(query).await,
111        }
112    }
113
114    async fn count(&self) -> Result<usize, RepositoryError> {
115        match self {
116            ApiKeyRepositoryStorage::InMemory(repo) => repo.count().await,
117            ApiKeyRepositoryStorage::Redis(repo) => repo.count().await,
118        }
119    }
120
121    async fn has_entries(&self) -> Result<bool, RepositoryError> {
122        match self {
123            ApiKeyRepositoryStorage::InMemory(repo) => repo.has_entries().await,
124            ApiKeyRepositoryStorage::Redis(repo) => repo.has_entries().await,
125        }
126    }
127
128    async fn drop_all_entries(&self) -> Result<(), RepositoryError> {
129        match self {
130            ApiKeyRepositoryStorage::InMemory(repo) => repo.drop_all_entries().await,
131            ApiKeyRepositoryStorage::Redis(repo) => repo.drop_all_entries().await,
132        }
133    }
134}
135
136impl PartialEq for ApiKeyRepoModel {
137    fn eq(&self, other: &Self) -> bool {
138        self.id == other.id
139            && self.name == other.name
140            && self.allowed_origins == other.allowed_origins
141            && self.permissions == other.permissions
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use crate::models::SecretString;
148
149    use super::*;
150
151    use chrono::Utc;
152
153    // Helper function to create a test api key
154    fn create_test_api_key(
155        id: &str,
156        name: &str,
157        value: &str,
158        allowed_origins: &[&str],
159        permissions: &[&str],
160    ) -> ApiKeyRepoModel {
161        ApiKeyRepoModel {
162            id: id.to_string(),
163            name: name.to_string(),
164            value: SecretString::new(value),
165            allowed_origins: allowed_origins.iter().map(|s| s.to_string()).collect(),
166            permissions: permissions.iter().map(|s| s.to_string()).collect(),
167            created_at: Utc::now().to_string(),
168        }
169    }
170
171    #[tokio::test]
172    async fn test_api_key_repository_storage_get_by_id_existing() {
173        let storage = ApiKeyRepositoryStorage::new_in_memory();
174        let api_key = create_test_api_key(
175            "test-api-key",
176            "test-name",
177            "test-value",
178            &["*"],
179            &["relayer:all:execute"],
180        );
181
182        // Add the api key first
183        storage.create(api_key.clone()).await.unwrap();
184
185        // Get the api key
186        let result = storage.get_by_id("test-api-key").await.unwrap();
187        assert_eq!(result, Some(api_key));
188    }
189
190    #[tokio::test]
191    async fn test_api_key_repository_storage_get_by_id_non_existing() {
192        let storage = ApiKeyRepositoryStorage::new_in_memory();
193
194        let result = storage.get_by_id("non-existent").await.unwrap();
195        assert_eq!(result, None);
196    }
197
198    #[tokio::test]
199    async fn test_api_key_repository_storage_add_success() {
200        let storage = ApiKeyRepositoryStorage::new_in_memory();
201        let api_key = create_test_api_key(
202            "test-api-key",
203            "test-name",
204            "test-value",
205            &["*"],
206            &["relayer:all:execute"],
207        );
208
209        let result = storage.create(api_key).await;
210        assert!(result.is_ok());
211    }
212
213    #[tokio::test]
214    async fn test_api_key_repository_storage_add_duplicate() {
215        let storage = ApiKeyRepositoryStorage::new_in_memory();
216        let api_key = create_test_api_key(
217            "test-api-key",
218            "test-name",
219            "test-value",
220            &["*"],
221            &["relayer:all:execute"],
222        );
223
224        // Add the api key first time
225        storage.create(api_key.clone()).await.unwrap();
226
227        // Try to add the same api key again - should succeed (overwrite)
228        let result = storage.create(api_key).await;
229        assert!(result.is_ok());
230    }
231
232    #[tokio::test]
233    async fn test_api_key_repository_storage_count_empty() {
234        let storage = ApiKeyRepositoryStorage::new_in_memory();
235
236        let count = storage.count().await.unwrap();
237        assert_eq!(count, 0);
238    }
239
240    #[tokio::test]
241    async fn test_api_key_repository_storage_count_with_api_keys() {
242        let storage = ApiKeyRepositoryStorage::new_in_memory();
243
244        // Add multiple plugins
245        storage
246            .create(create_test_api_key(
247                "api-key1",
248                "test-name1",
249                "test-value1",
250                &["*"],
251                &["relayer:all:execute"],
252            ))
253            .await
254            .unwrap();
255        storage
256            .create(create_test_api_key(
257                "api-key2",
258                "test-name2",
259                "test-value2",
260                &["*"],
261                &["relayer:all:execute"],
262            ))
263            .await
264            .unwrap();
265        storage
266            .create(create_test_api_key(
267                "api-key3",
268                "test-name3",
269                "test-value3",
270                &["*"],
271                &["relayer:all:execute"],
272            ))
273            .await
274            .unwrap();
275
276        let count = storage.count().await.unwrap();
277        assert_eq!(count, 3);
278    }
279
280    #[tokio::test]
281    async fn test_api_key_repository_storage_has_entries_empty() {
282        let storage = ApiKeyRepositoryStorage::new_in_memory();
283
284        let has_entries = storage.has_entries().await.unwrap();
285        assert!(!has_entries);
286    }
287
288    #[tokio::test]
289    async fn test_api_key_repository_storage_has_entries_with_api_keys() {
290        let storage = ApiKeyRepositoryStorage::new_in_memory();
291
292        storage
293            .create(create_test_api_key(
294                "api-key1",
295                "test-name1",
296                "test-value1",
297                &["*"],
298                &["relayer:all:execute"],
299            ))
300            .await
301            .unwrap();
302
303        let has_entries = storage.has_entries().await.unwrap();
304        assert!(has_entries);
305    }
306
307    #[tokio::test]
308    async fn test_api_key_repository_storage_drop_all_entries_empty() {
309        let storage = ApiKeyRepositoryStorage::new_in_memory();
310
311        let result = storage.drop_all_entries().await;
312        assert!(result.is_ok());
313
314        let count = storage.count().await.unwrap();
315        assert_eq!(count, 0);
316    }
317
318    #[tokio::test]
319    async fn test_api_key_repository_storage_drop_all_entries_with_api_keys() {
320        let storage = ApiKeyRepositoryStorage::new_in_memory();
321
322        // Add multiple plugins
323        storage
324            .create(create_test_api_key(
325                "api-key1",
326                "test-name1",
327                "test-value1",
328                &["*"],
329                &["relayer:all:execute"],
330            ))
331            .await
332            .unwrap();
333        storage
334            .create(create_test_api_key(
335                "api-key2",
336                "test-name2",
337                "test-value2",
338                &["*"],
339                &["relayer:all:execute"],
340            ))
341            .await
342            .unwrap();
343
344        let result = storage.drop_all_entries().await;
345        assert!(result.is_ok());
346
347        let count = storage.count().await.unwrap();
348        assert_eq!(count, 0);
349
350        let has_entries = storage.has_entries().await.unwrap();
351        assert!(!has_entries);
352    }
353
354    #[tokio::test]
355    async fn test_api_key_repository_storage_list_paginated_empty() {
356        let storage = ApiKeyRepositoryStorage::new_in_memory();
357
358        let query = PaginationQuery {
359            page: 1,
360            per_page: 10,
361        };
362        let result = storage.list_paginated(query).await.unwrap();
363
364        assert_eq!(result.items.len(), 0);
365        assert_eq!(result.total, 0);
366        assert_eq!(result.page, 1);
367        assert_eq!(result.per_page, 10);
368    }
369
370    #[tokio::test]
371    async fn test_api_key_repository_storage_list_paginated_with_api_keys() {
372        let storage = ApiKeyRepositoryStorage::new_in_memory();
373
374        // Add multiple plugins
375        storage
376            .create(create_test_api_key(
377                "api-key1",
378                "test-name1",
379                "test-value1",
380                &["*"],
381                &["relayer:all:execute"],
382            ))
383            .await
384            .unwrap();
385        storage
386            .create(create_test_api_key(
387                "api-key2",
388                "test-name2",
389                "test-value2",
390                &["*"],
391                &["relayer:all:execute"],
392            ))
393            .await
394            .unwrap();
395        storage
396            .create(create_test_api_key(
397                "api-key3",
398                "test-name3",
399                "test-value3",
400                &["*"],
401                &["relayer:all:execute"],
402            ))
403            .await
404            .unwrap();
405
406        let query = PaginationQuery {
407            page: 1,
408            per_page: 2,
409        };
410        let result = storage.list_paginated(query).await.unwrap();
411
412        assert_eq!(result.items.len(), 2);
413        assert_eq!(result.total, 3);
414        assert_eq!(result.page, 1);
415        assert_eq!(result.per_page, 2);
416    }
417
418    #[tokio::test]
419    async fn test_api_key_repository_storage_workflow() {
420        let storage = ApiKeyRepositoryStorage::new_in_memory();
421
422        // Initially empty
423        assert!(!storage.has_entries().await.unwrap());
424        assert_eq!(storage.count().await.unwrap(), 0);
425
426        // Add plugins
427        let api_key1 = create_test_api_key(
428            "api-key1",
429            "test-name1",
430            "test-value1",
431            &["*"],
432            &["relayer:all:execute"],
433        );
434        let api_key2 = create_test_api_key(
435            "api-key2",
436            "test-name2",
437            "test-value2",
438            &["*"],
439            &["relayer:all:execute"],
440        );
441
442        storage.create(api_key1.clone()).await.unwrap();
443        storage.create(api_key2.clone()).await.unwrap();
444
445        // Check state
446        assert!(storage.has_entries().await.unwrap());
447        assert_eq!(storage.count().await.unwrap(), 2);
448
449        // Retrieve specific plugin
450        let retrieved = storage.get_by_id("api-key1").await.unwrap();
451        assert_eq!(retrieved, Some(api_key1));
452
453        // List all plugins
454        let query = PaginationQuery {
455            page: 1,
456            per_page: 10,
457        };
458        let result = storage.list_paginated(query).await.unwrap();
459        assert_eq!(result.items.len(), 2);
460        assert_eq!(result.total, 2);
461
462        // Clear all plugins
463        storage.drop_all_entries().await.unwrap();
464        assert!(!storage.has_entries().await.unwrap());
465        assert_eq!(storage.count().await.unwrap(), 0);
466    }
467}