Skip to content

Commit 407541b

Browse files
authored
Merge pull request #438 from daree-dev/feature/admin-ip-whitelist
Implement IP whitelist for admin routes
2 parents 8dd5215 + de9ef16 commit 407541b

4 files changed

Lines changed: 25 additions & 5 deletions

File tree

services/api/src/config.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ pub struct Config {
6262
pub base_url: String,
6363
pub api_keys: Vec<String>,
6464
pub admin_whitelist_ips: Vec<IpAddr>,
65+
pub trust_proxy: bool,
6566
pub request_signing_secret: Option<String>,
6667
pub sendgrid_webhook_secret: Option<String>,
6768
pub trusted_proxy_cidrs: Vec<IpNet>,
@@ -198,6 +199,10 @@ impl Config {
198199
.collect()
199200
})
200201
.unwrap_or_default(),
202+
trust_proxy: env::var("TRUST_PROXY")
203+
.ok()
204+
.and_then(|s| s.parse().ok())
205+
.unwrap_or(true),
201206
request_signing_secret: env::var("REQUEST_SIGNING_SECRET").ok(),
202207
sendgrid_webhook_secret: env::var("SENDGRID_WEBHOOK_SECRET").ok(),
203208
trusted_proxy_cidrs,

services/api/src/lib.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ pub async fn run() -> anyhow::Result<()> {
215215
.route("/api/markets/featured", get(handlers::featured_markets))
216216
.route("/api/content", get(handlers::content))
217217
.layer(middleware::from_fn_with_state(
218-
rate_limiter.clone(),
218+
(rate_limiter.clone(), TrustProxy(config.trust_proxy)),
219219
security::global_rate_limit_middleware,
220220
))
221221
.with_state(state.clone());
@@ -242,11 +242,15 @@ pub async fn run() -> anyhow::Result<()> {
242242
axum::routing::delete(handlers::newsletter_gdpr_delete),
243243
)
244244
.layer(middleware::from_fn_with_state(
245-
rate_limiter.clone(),
245+
(rate_limiter.clone(), TrustProxy(config.trust_proxy)),
246246
rate_limit::newsletter_rate_limit_middleware,
247247
))
248248
.with_state(state.clone());
249249

250+
// Admin routes require API key authentication (if configured) and IP whitelisting (if configured).
251+
// When API_KEYS is set, requests must include a valid x-api-key header.
252+
// When ADMIN_WHITELIST_IPS is set, only whitelisted IPs can access admin endpoints.
253+
// Invalid API key returns 401 Unauthorized, non-whitelisted IPs return 403 Forbidden.
250254
let admin_routes = Router::new()
251255
.route(
252256
"/api/markets/:market_id/resolve",

services/api/src/security.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ impl ApiKeyAuth {
262262
}
263263

264264
pub fn verify(&self, key: &str) -> bool {
265-
self.valid_keys.iter().any(|k| k == key)
265+
self.valid_keys.is_empty() || self.valid_keys.iter().any(|k| k == key)
266266
}
267267
}
268268

@@ -315,6 +315,9 @@ impl IpWhitelist {
315315
}
316316

317317
pub fn is_allowed(&self, ip: &str) -> bool {
318+
if self.allowed_ips.is_empty() {
319+
return true;
320+
}
318321
if let Ok(addr) = ip.parse::<IpAddr>() {
319322
return self.allowed_ips.contains(&addr);
320323
}

services/api/tests/security_tests.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ mod tests {
1010
Router,
1111
};
1212
use predictiq_api::security::{
13-
ip_whitelist_middleware, sanitize, signing, IpWhitelist, RateLimitConfig, RateLimiter,
13+
ip_whitelist_middleware, sanitize, signing, IpWhitelist, RateLimitConfig, RateLimiter, TrustProxy,
1414
};
1515
use tower::ServiceExt;
1616

@@ -264,7 +264,7 @@ mod tests {
264264
Router::new()
265265
.route("/admin", get(|| async { "ok" }))
266266
.layer(middleware::from_fn_with_state(
267-
wl,
267+
(wl, TrustProxy(true)),
268268
ip_whitelist_middleware,
269269
))
270270
}
@@ -314,6 +314,14 @@ mod tests {
314314
assert_eq!(status, StatusCode::FORBIDDEN);
315315
}
316316

317+
#[tokio::test]
318+
async fn middleware_allows_when_whitelist_empty() {
319+
let wl = Arc::new(IpWhitelist::new(vec![]));
320+
let req = Request::builder().uri("/admin").body(Body::empty()).unwrap();
321+
let status = whitelist_app(wl).oneshot(req).await.unwrap().status();
322+
assert_eq!(status, StatusCode::OK);
323+
}
324+
317325
// -------------------------------------------------------------------------
318326
// sanitize::contains_sql_injection — expanded corpus
319327
// -------------------------------------------------------------------------

0 commit comments

Comments
 (0)