openzeppelin_relayer/repositories/signer/
mod.rs

1//! Signer Repository Module
2//!
3//! This module provides the signer repository layer for the OpenZeppelin Relayer service.
4//! It implements the Repository pattern to abstract signer data persistence operations,
5//! supporting both in-memory and Redis-backed storage implementations.
6//!
7//! ## Features
8//!
9//! - **CRUD Operations**: Create, read, update, and delete signer configurations
10//! - **Multi-Provider Support**: Handle various signer types (Local, AWS KMS, Google Cloud KMS, etc.)
11//! - **Secure Storage**: Proper handling of cryptographic keys and secrets
12//! - **Pagination Support**: Efficient paginated listing of signers
13//! - **Configuration Management**: Convert between file configs and repository models
14//!
15//! ## Repository Implementations
16//!
17//! - [`InMemorySignerRepository`]: Fast in-memory storage for testing/development
18//! - [`RedisSignerRepository`]: Redis-backed storage for production environments
19//!
20//! ## Supported Signer Types
21//!
22//! - **Local Signers**: Direct private key management
23//! - **AWS KMS**: AWS Key Management Service integration
24//! - **Google Cloud KMS**: Google Cloud Key Management Service integration
25//! - **Turnkey**: Turnkey service integration
26//! - **Vault**: HashiCorp Vault integration
27
28mod signer_in_memory;
29mod signer_redis;
30
31pub use signer_in_memory::*;
32pub use signer_redis::*;
33
34use crate::{
35    models::{RepositoryError, SignerRepoModel},
36    repositories::{PaginatedResult, PaginationQuery, Repository},
37};
38use async_trait::async_trait;
39use redis::aio::ConnectionManager;
40use std::sync::Arc;
41
42/// Enum wrapper for different signer repository implementations
43#[derive(Debug, Clone)]
44pub enum SignerRepositoryStorage {
45    InMemory(InMemorySignerRepository),
46    Redis(RedisSignerRepository),
47}
48
49impl SignerRepositoryStorage {
50    pub fn new_in_memory() -> Self {
51        Self::InMemory(InMemorySignerRepository::new())
52    }
53
54    pub fn new_redis(
55        connection_manager: Arc<ConnectionManager>,
56        key_prefix: String,
57    ) -> Result<Self, RepositoryError> {
58        let redis_repo = RedisSignerRepository::new(connection_manager, key_prefix)?;
59        Ok(Self::Redis(redis_repo))
60    }
61}
62
63#[async_trait]
64impl Repository<SignerRepoModel, String> for SignerRepositoryStorage {
65    async fn create(&self, entity: SignerRepoModel) -> Result<SignerRepoModel, RepositoryError> {
66        match self {
67            SignerRepositoryStorage::InMemory(repo) => repo.create(entity).await,
68            SignerRepositoryStorage::Redis(repo) => repo.create(entity).await,
69        }
70    }
71
72    async fn get_by_id(&self, id: String) -> Result<SignerRepoModel, RepositoryError> {
73        match self {
74            SignerRepositoryStorage::InMemory(repo) => repo.get_by_id(id).await,
75            SignerRepositoryStorage::Redis(repo) => repo.get_by_id(id).await,
76        }
77    }
78
79    async fn list_all(&self) -> Result<Vec<SignerRepoModel>, RepositoryError> {
80        match self {
81            SignerRepositoryStorage::InMemory(repo) => repo.list_all().await,
82            SignerRepositoryStorage::Redis(repo) => repo.list_all().await,
83        }
84    }
85
86    async fn list_paginated(
87        &self,
88        query: PaginationQuery,
89    ) -> Result<PaginatedResult<SignerRepoModel>, RepositoryError> {
90        match self {
91            SignerRepositoryStorage::InMemory(repo) => repo.list_paginated(query).await,
92            SignerRepositoryStorage::Redis(repo) => repo.list_paginated(query).await,
93        }
94    }
95
96    async fn update(
97        &self,
98        id: String,
99        entity: SignerRepoModel,
100    ) -> Result<SignerRepoModel, RepositoryError> {
101        match self {
102            SignerRepositoryStorage::InMemory(repo) => repo.update(id, entity).await,
103            SignerRepositoryStorage::Redis(repo) => repo.update(id, entity).await,
104        }
105    }
106
107    async fn delete_by_id(&self, id: String) -> Result<(), RepositoryError> {
108        match self {
109            SignerRepositoryStorage::InMemory(repo) => repo.delete_by_id(id).await,
110            SignerRepositoryStorage::Redis(repo) => repo.delete_by_id(id).await,
111        }
112    }
113
114    async fn count(&self) -> Result<usize, RepositoryError> {
115        match self {
116            SignerRepositoryStorage::InMemory(repo) => repo.count().await,
117            SignerRepositoryStorage::Redis(repo) => repo.count().await,
118        }
119    }
120
121    async fn has_entries(&self) -> Result<bool, RepositoryError> {
122        match self {
123            SignerRepositoryStorage::InMemory(repo) => repo.has_entries().await,
124            SignerRepositoryStorage::Redis(repo) => repo.has_entries().await,
125        }
126    }
127
128    async fn drop_all_entries(&self) -> Result<(), RepositoryError> {
129        match self {
130            SignerRepositoryStorage::InMemory(repo) => repo.drop_all_entries().await,
131            SignerRepositoryStorage::Redis(repo) => repo.drop_all_entries().await,
132        }
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use crate::models::{LocalSignerConfigStorage, SignerConfigStorage};
140    use secrets::SecretVec;
141
142    fn create_local_signer(id: String) -> SignerRepoModel {
143        SignerRepoModel {
144            id: id.clone(),
145            config: SignerConfigStorage::Local(LocalSignerConfigStorage {
146                raw_key: SecretVec::new(32, |v| v.copy_from_slice(&[1; 32])),
147            }),
148        }
149    }
150
151    #[actix_web::test]
152    async fn test_in_memory_impl_creation() {
153        let impl_repo = SignerRepositoryStorage::new_in_memory();
154        match impl_repo {
155            SignerRepositoryStorage::InMemory(_) => (),
156            _ => panic!("Expected InMemory variant"),
157        }
158    }
159
160    #[actix_web::test]
161    async fn test_in_memory_impl_operations() {
162        let impl_repo = SignerRepositoryStorage::new_in_memory();
163        let signer = create_local_signer("test-signer".to_string());
164
165        // Test create
166        let created = impl_repo.create(signer.clone()).await.unwrap();
167        assert_eq!(created.id, signer.id);
168
169        // Test get
170        let retrieved = impl_repo
171            .get_by_id("test-signer".to_string())
172            .await
173            .unwrap();
174        assert_eq!(retrieved.id, signer.id);
175
176        // Test count
177        let count = impl_repo.count().await.unwrap();
178        assert!(count >= 1);
179
180        // Test list_all
181        let all_signers = impl_repo.list_all().await.unwrap();
182        assert!(!all_signers.is_empty());
183
184        // Test pagination
185        let query = PaginationQuery {
186            page: 1,
187            per_page: 10,
188        };
189        let paginated = impl_repo.list_paginated(query).await.unwrap();
190        assert!(!paginated.items.is_empty());
191    }
192
193    #[actix_web::test]
194    async fn test_impl_error_handling() {
195        let impl_repo = SignerRepositoryStorage::new_in_memory();
196
197        // Test getting non-existent signer
198        let result = impl_repo.get_by_id("non-existent".to_string()).await;
199        assert!(result.is_err());
200        assert!(matches!(result.unwrap_err(), RepositoryError::NotFound(_)));
201    }
202
203    #[actix_web::test]
204    async fn test_impl_debug() {
205        let impl_repo = SignerRepositoryStorage::new_in_memory();
206        let debug_string = format!("{:?}", impl_repo);
207        assert!(debug_string.contains("InMemory"));
208    }
209
210    #[actix_web::test]
211    async fn test_duplicate_creation_error() {
212        let impl_repo = SignerRepositoryStorage::new_in_memory();
213        let signer = create_local_signer("duplicate-test".to_string());
214
215        // Create the signer first time
216        impl_repo.create(signer.clone()).await.unwrap();
217
218        // Try to create again - should fail
219        let result = impl_repo.create(signer).await;
220        assert!(result.is_err());
221        assert!(matches!(
222            result.unwrap_err(),
223            RepositoryError::ConstraintViolation(_)
224        ));
225    }
226
227    #[actix_web::test]
228    async fn test_update_operations() {
229        let impl_repo = SignerRepositoryStorage::new_in_memory();
230        let signer = create_local_signer("update-test".to_string());
231
232        // Create the signer first
233        impl_repo.create(signer.clone()).await.unwrap();
234
235        // Update with different config
236        let updated_signer = SignerRepoModel {
237            id: "update-test".to_string(),
238            config: SignerConfigStorage::Local(LocalSignerConfigStorage {
239                raw_key: SecretVec::new(32, |v| v.copy_from_slice(&[2; 32])),
240            }),
241        };
242
243        let result = impl_repo
244            .update("update-test".to_string(), updated_signer)
245            .await;
246        assert!(result.is_ok());
247
248        // Test updating non-existent signer
249        let non_existent_signer = SignerRepoModel {
250            id: "non-existent".to_string(),
251            config: SignerConfigStorage::Local(LocalSignerConfigStorage {
252                raw_key: SecretVec::new(32, |v| v.copy_from_slice(&[3; 32])),
253            }),
254        };
255
256        let result = impl_repo
257            .update("non-existent".to_string(), non_existent_signer)
258            .await;
259        assert!(result.is_err());
260        assert!(matches!(result.unwrap_err(), RepositoryError::NotFound(_)));
261    }
262
263    #[actix_web::test]
264    async fn test_delete_operations() {
265        let impl_repo = SignerRepositoryStorage::new_in_memory();
266        let signer = create_local_signer("delete-test".to_string());
267
268        // Create the signer first
269        impl_repo.create(signer).await.unwrap();
270
271        // Delete the signer
272        let result = impl_repo.delete_by_id("delete-test".to_string()).await;
273        assert!(result.is_ok());
274
275        // Verify it's gone
276        let get_result = impl_repo.get_by_id("delete-test".to_string()).await;
277        assert!(get_result.is_err());
278        assert!(matches!(
279            get_result.unwrap_err(),
280            RepositoryError::NotFound(_)
281        ));
282
283        // Test deleting non-existent signer
284        let result = impl_repo.delete_by_id("non-existent".to_string()).await;
285        assert!(result.is_err());
286        assert!(matches!(result.unwrap_err(), RepositoryError::NotFound(_)));
287    }
288
289    #[actix_web::test]
290    async fn test_has_entries() {
291        let repo = InMemorySignerRepository::new();
292        assert!(!repo.has_entries().await.unwrap());
293
294        let signer = create_local_signer("test".to_string());
295        repo.create(signer.clone()).await.unwrap();
296        assert!(repo.has_entries().await.unwrap());
297    }
298
299    #[actix_web::test]
300    async fn test_drop_all_entries() {
301        let repo = InMemorySignerRepository::new();
302        let signer = create_local_signer("test".to_string());
303        repo.create(signer.clone()).await.unwrap();
304        assert!(repo.has_entries().await.unwrap());
305
306        repo.drop_all_entries().await.unwrap();
307        assert!(!repo.has_entries().await.unwrap());
308    }
309}
310
311#[cfg(test)]
312mockall::mock! {
313    pub SignerRepository {}
314
315    #[async_trait]
316    impl Repository<SignerRepoModel, String> for SignerRepository {
317        async fn create(&self, entity: SignerRepoModel) -> Result<SignerRepoModel, RepositoryError>;
318        async fn get_by_id(&self, id: String) -> Result<SignerRepoModel, RepositoryError>;
319        async fn list_all(&self) -> Result<Vec<SignerRepoModel>, RepositoryError>;
320        async fn list_paginated(&self, query: PaginationQuery) -> Result<PaginatedResult<SignerRepoModel>, RepositoryError>;
321        async fn update(&self, id: String, entity: SignerRepoModel) -> Result<SignerRepoModel, RepositoryError>;
322        async fn delete_by_id(&self, id: String) -> Result<(), RepositoryError>;
323        async fn count(&self) -> Result<usize, RepositoryError>;
324        async fn has_entries(&self) -> Result<bool, RepositoryError>;
325        async fn drop_all_entries(&self) -> Result<(), RepositoryError>;
326    }
327}