Skip to content

Commit 429e29e

Browse files
authored
feat: Authentication improvements (#456)
* feat: Add support to plugin list endpoint * docs: Add plugin list to docs * fix: Use paginated list * fix: Lint * feat: Create API keys CRUD methods * fix: Examoles linting * chore: Hide API key feature under feature flag * fix: Rename flag to authV2 * test: Add redis tests + docs * test: Add repository tests * test: Add remaining tests * chore: Update changelog * test: Fix tests * fix: Rollback wiremock change * refactor: Split models into files * fix: Remove feature flag + use SecureString * feat: Serialize and deserialize secret streing * refactor: Use tryfrom for request handling * fix: Coderabbit fixes * feat: Load default key in repo * test: Add routes tests * fix: Avoid deleting API keys
1 parent 07b795a commit 429e29e

33 files changed

Lines changed: 2486 additions & 304 deletions

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,12 +278,32 @@ cargo test properties
278278
cargo test integration
279279
```
280280

281+
281282
> :warning: Debian/Ubuntu: If you encounter OpenSSL build errors, install the required packages:
282283
283284
```bash
284285
sudo apt-get update && sudo apt-get install -y pkg-config libssl-dev
285286
```
286287

288+
#### Run tests against Redis
289+
290+
1. You can start a Redis instance using the following command:
291+
292+
```bash
293+
docker run -d \
294+
--name redis \
295+
-p 6379:6379 \
296+
redis:latest
297+
```
298+
299+
2. Then remove the `#[ignore = "Requires active Redis instance"]` attribute from the tests you want to run.
300+
301+
3. Run the tests using single thread to avoid race conditions within suites:
302+
303+
```bash
304+
cargo test your_test_regex -- --test-threads=1
305+
306+
287307
### Config files
288308

289309
Create `config/config.json` file. You can use `config/config.example.json` as a starting point:

src/api/controllers/api_key.rs

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
//! # Api Key Controller
2+
//!
3+
//! Handles HTTP endpoints for api key operations including:
4+
//! - Create api keys
5+
//! - List api keys
6+
//! - Delete api keys
7+
use crate::{
8+
jobs::JobProducerTrait,
9+
models::{
10+
ApiError, ApiKeyRepoModel, ApiKeyRequest, ApiKeyResponse, ApiResponse, NetworkRepoModel,
11+
NotificationRepoModel, PaginationMeta, PaginationQuery, RelayerRepoModel, SignerRepoModel,
12+
ThinDataAppState, TransactionRepoModel,
13+
},
14+
repositories::{
15+
ApiKeyRepositoryTrait, NetworkRepository, PluginRepositoryTrait, RelayerRepository,
16+
Repository, TransactionCounterTrait, TransactionRepository,
17+
},
18+
};
19+
use actix_web::HttpResponse;
20+
use eyre::Result;
21+
22+
/// Create api key
23+
///
24+
/// # Arguments
25+
///
26+
/// * `api_key_request` - The api key request.
27+
/// * `name` - The name of the api key.
28+
/// * `allowed_origins` - The allowed origins for the api key.
29+
/// * `permissions` - The permissions for the api key.
30+
/// * `state` - The application state containing the api key repository.
31+
///
32+
/// # Returns
33+
///
34+
/// The result of the plugin call.
35+
pub async fn create_api_key<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
36+
api_key_request: ApiKeyRequest,
37+
state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
38+
) -> Result<HttpResponse, ApiError>
39+
where
40+
J: JobProducerTrait + Send + Sync + 'static,
41+
RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
42+
TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
43+
NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
44+
NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
45+
SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
46+
TCR: TransactionCounterTrait + Send + Sync + 'static,
47+
PR: PluginRepositoryTrait + Send + Sync + 'static,
48+
AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
49+
{
50+
let api_key = ApiKeyRepoModel::try_from(api_key_request)?;
51+
52+
let api_key = state.api_key_repository.create(api_key).await?;
53+
54+
Ok(HttpResponse::Created().json(ApiResponse::success(api_key)))
55+
}
56+
57+
/// List api keys
58+
///
59+
/// # Arguments
60+
///
61+
/// * `query` - The pagination query parameters.
62+
/// * `page` - The page number.
63+
/// * `per_page` - The number of items per page.
64+
/// * `state` - The application state containing the api key repository.
65+
///
66+
/// # Returns
67+
///
68+
/// The result of the api key list.
69+
pub async fn list_api_keys<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
70+
query: PaginationQuery,
71+
state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
72+
) -> Result<HttpResponse, ApiError>
73+
where
74+
J: JobProducerTrait + Send + Sync + 'static,
75+
RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
76+
TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
77+
NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
78+
NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
79+
SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
80+
TCR: TransactionCounterTrait + Send + Sync + 'static,
81+
PR: PluginRepositoryTrait + Send + Sync + 'static,
82+
AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
83+
{
84+
let api_keys = state.api_key_repository.list_paginated(query).await?;
85+
86+
let api_key_items: Vec<ApiKeyRepoModel> = api_keys.items.into_iter().collect();
87+
88+
// Subtract the "value" from the api key to avoid exposing it.
89+
let api_key_items: Vec<ApiKeyResponse> = api_key_items
90+
.into_iter()
91+
.map(ApiKeyResponse::try_from)
92+
.collect::<Result<Vec<ApiKeyResponse>, ApiError>>()?;
93+
94+
Ok(HttpResponse::Ok().json(ApiResponse::paginated(
95+
api_key_items,
96+
PaginationMeta {
97+
total_items: api_keys.total,
98+
current_page: api_keys.page,
99+
per_page: api_keys.per_page,
100+
},
101+
)))
102+
}
103+
104+
/// Get api key permissions
105+
///
106+
/// # Arguments
107+
///
108+
/// * `api_key_id` - The id of the api key.
109+
/// * `state` - The application state containing the api key repository.
110+
///
111+
pub async fn get_api_key_permissions<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
112+
api_key_id: String,
113+
state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
114+
) -> Result<HttpResponse, ApiError>
115+
where
116+
J: JobProducerTrait + Send + Sync + 'static,
117+
RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
118+
TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
119+
NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
120+
NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
121+
SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
122+
TCR: TransactionCounterTrait + Send + Sync + 'static,
123+
PR: PluginRepositoryTrait + Send + Sync + 'static,
124+
AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
125+
{
126+
let permissions = state
127+
.api_key_repository
128+
.list_permissions(&api_key_id)
129+
.await?;
130+
131+
Ok(HttpResponse::Ok().json(ApiResponse::success(permissions)))
132+
}
133+
134+
/// Delete api key
135+
///
136+
/// # Arguments
137+
///
138+
/// * `api_key_id` - The id of the api key.
139+
/// * `state` - The application state containing the api key repository.
140+
///
141+
pub async fn delete_api_key<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
142+
_api_key_id: String,
143+
_state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
144+
) -> Result<HttpResponse, ApiError>
145+
where
146+
J: JobProducerTrait + Send + Sync + 'static,
147+
RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
148+
TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
149+
NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
150+
NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
151+
SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
152+
TCR: TransactionCounterTrait + Send + Sync + 'static,
153+
PR: PluginRepositoryTrait + Send + Sync + 'static,
154+
AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
155+
{
156+
// state.api_key_repository.delete_by_id(&api_key_id).await?;
157+
158+
// Ok(HttpResponse::Ok().json(ApiResponse::success(api_key_id)))
159+
Ok(HttpResponse::Ok().json(ApiResponse::<String>::error("Not implemented".to_string())))
160+
}
161+
162+
#[cfg(test)]
163+
mod tests {
164+
use super::*;
165+
use crate::{
166+
models::{ApiKeyRepoModel, PaginationQuery, SecretString},
167+
utils::mocks::mockutils::create_mock_app_state,
168+
};
169+
use actix_web::web::ThinData;
170+
171+
/// Helper function to create a test api key model
172+
fn create_test_api_key_model(id: &str) -> ApiKeyRepoModel {
173+
ApiKeyRepoModel {
174+
id: id.to_string(),
175+
value: SecretString::new("test-api-key-value"),
176+
name: "Test API Key".to_string(),
177+
allowed_origins: vec!["*".to_string()],
178+
permissions: vec!["relayer:all:execute".to_string()],
179+
created_at: "2023-01-01T00:00:00Z".to_string(),
180+
}
181+
}
182+
183+
/// Helper function to create a test api key create request
184+
fn create_test_api_key_create_request(name: &str) -> ApiKeyRequest {
185+
ApiKeyRequest {
186+
name: name.to_string(),
187+
permissions: vec!["relayer:all:execute".to_string()],
188+
allowed_origins: Some(vec!["*".to_string()]),
189+
}
190+
}
191+
192+
#[actix_web::test]
193+
async fn test_create_api_key() {
194+
let app_state = create_mock_app_state(None, None, None, None, None, None).await;
195+
let api_key_request = create_test_api_key_create_request("Test API Key");
196+
197+
let result = create_api_key(api_key_request, ThinData(app_state)).await;
198+
199+
assert!(result.is_ok());
200+
let response = result.unwrap();
201+
assert_eq!(response.status(), 201);
202+
}
203+
204+
#[actix_web::test]
205+
async fn test_list_api_keys_empty() {
206+
let app_state = create_mock_app_state(None, None, None, None, None, None).await;
207+
let query = PaginationQuery {
208+
page: 1,
209+
per_page: 10,
210+
};
211+
212+
let result = list_api_keys(query, ThinData(app_state)).await;
213+
214+
assert!(result.is_ok());
215+
let response = result.unwrap();
216+
assert_eq!(response.status(), 200);
217+
}
218+
219+
#[actix_web::test]
220+
async fn test_list_api_keys_with_data() {
221+
let api_key = create_test_api_key_model("test-api-key-1");
222+
let app_state =
223+
create_mock_app_state(Some(vec![api_key]), None, None, None, None, None).await;
224+
let query = PaginationQuery {
225+
page: 1,
226+
per_page: 10,
227+
};
228+
229+
let result = list_api_keys(query, ThinData(app_state)).await;
230+
231+
assert!(result.is_ok());
232+
let response = result.unwrap();
233+
assert_eq!(response.status(), 200);
234+
}
235+
236+
#[actix_web::test]
237+
async fn test_get_api_key_permissions() {
238+
let api_key = create_test_api_key_model("test-api-key-1");
239+
let api_key_id = api_key.id.clone();
240+
let app_state =
241+
create_mock_app_state(Some(vec![api_key]), None, None, None, None, None).await;
242+
243+
let result = get_api_key_permissions(api_key_id, ThinData(app_state)).await;
244+
245+
assert!(result.is_ok());
246+
let response = result.unwrap();
247+
assert_eq!(response.status(), 200);
248+
}
249+
250+
// #[actix_web::test]
251+
// async fn test_delete_api_key() {
252+
// let api_key = create_test_api_key_model("test-api-key-1");
253+
// let api_key_id = api_key.id.clone();
254+
// let app_state =
255+
// create_mock_app_state(Some(vec![api_key]), None, None, None, None, None).await;
256+
257+
// let result = delete_api_key(api_key_id, ThinData(app_state)).await;
258+
259+
// assert!(result.is_ok());
260+
// let response = result.unwrap();
261+
// assert_eq!(response.status(), 200);
262+
// }
263+
264+
#[actix_web::test]
265+
async fn test_get_permissions_nonexistent_api_key() {
266+
let app_state = create_mock_app_state(None, None, None, None, None, None).await;
267+
268+
let result =
269+
get_api_key_permissions("nonexistent-id".to_string(), ThinData(app_state)).await;
270+
271+
assert!(result.is_err());
272+
}
273+
}

src/api/controllers/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
//!
55
//! ## Controllers
66
//!
7+
//! * `api_key` - API key management endpoints
78
//! * `relayer` - Transaction and relayer management endpoints
89
//! * `plugin` - Plugin endpoints
910
//! * `notifications` - Notification management endpoints
1011
//! * `signers` - Signer management endpoints
1112
13+
pub mod api_key;
1214
pub mod notification;
1315
pub mod plugin;
1416
pub mod relayer;

0 commit comments

Comments
 (0)