mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-05 17:07:46 +08:00
feat(security): harden client IP and admin controls
This commit is contained in:
@@ -4,8 +4,10 @@ use axum::body::{Body, Bytes};
|
||||
use axum::routing::any;
|
||||
use axum::{extract::Request, Router};
|
||||
use http::{HeaderMap, HeaderValue, StatusCode};
|
||||
use http_body_util::BodyExt;
|
||||
use serde_json::json;
|
||||
|
||||
use super::super::super::send_request;
|
||||
use super::super::{build_router_with_state, start_server, AppState};
|
||||
use crate::admin_api::{
|
||||
maybe_build_local_admin_security_response, AdminAppState, AdminRequestContext,
|
||||
@@ -17,6 +19,72 @@ use crate::constants::{
|
||||
};
|
||||
use crate::control::resolve_public_request_context;
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_blocks_blacklisted_ip_before_routing() {
|
||||
let gateway = build_router_with_state(
|
||||
AppState::new()
|
||||
.expect("gateway should build")
|
||||
.with_admin_security_blacklist_for_tests([(
|
||||
"127.0.0.1".to_string(),
|
||||
"blocked".to_string(),
|
||||
)]),
|
||||
);
|
||||
let request = Request::builder()
|
||||
.uri("/api/public/system")
|
||||
.body(Body::empty())
|
||||
.expect("request should build");
|
||||
|
||||
let response = send_request(gateway, request).await;
|
||||
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
let payload = response
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.expect("body should collect")
|
||||
.to_bytes();
|
||||
let payload: serde_json::Value =
|
||||
serde_json::from_slice(&payload).expect("response should be json");
|
||||
assert_eq!(payload["error"]["message"], "当前 IP 已被禁止访问");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_blocks_forwarded_ip_from_trusted_proxy() {
|
||||
let gateway = build_router_with_state(
|
||||
AppState::new()
|
||||
.expect("gateway should build")
|
||||
.with_admin_security_blacklist_for_tests([(
|
||||
"203.0.113.8".to_string(),
|
||||
"blocked".to_string(),
|
||||
)]),
|
||||
);
|
||||
let request = Request::builder()
|
||||
.uri("/api/public/system")
|
||||
.header("x-real-ip", "203.0.113.8")
|
||||
.body(Body::empty())
|
||||
.expect("request should build");
|
||||
|
||||
let response = send_request(gateway, request).await;
|
||||
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn admin_security_whitelist_matches_cidr() {
|
||||
let state = AppState::new()
|
||||
.expect("gateway should build")
|
||||
.with_admin_security_whitelist_for_tests(["203.0.113.0/24".to_string()]);
|
||||
|
||||
assert!(state
|
||||
.admin_security_ip_whitelisted("203.0.113.8".parse().expect("valid ip"))
|
||||
.await
|
||||
.expect("whitelist check should succeed"));
|
||||
assert!(!state
|
||||
.admin_security_ip_whitelisted("198.51.100.8".parse().expect("valid ip"))
|
||||
.await
|
||||
.expect("whitelist check should succeed"));
|
||||
}
|
||||
|
||||
async fn send_admin_security_request(
|
||||
gateway: Router,
|
||||
method: reqwest::Method,
|
||||
@@ -130,6 +198,26 @@ async fn gateway_handles_admin_security_blacklist_add_locally_with_trusted_admin
|
||||
assert_eq!(upstream_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_rejects_invalid_admin_security_blacklist_ip() {
|
||||
let gateway = build_router_with_state(AppState::new().expect("gateway should build"));
|
||||
|
||||
let (status, payload, upstream_count) = send_admin_security_request(
|
||||
gateway,
|
||||
reqwest::Method::POST,
|
||||
"/api/admin/security/ip/blacklist",
|
||||
Some(json!({
|
||||
"ip_address": "not-an-ip",
|
||||
"reason": "invalid"
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(status, StatusCode::BAD_REQUEST);
|
||||
assert_eq!(payload["detail"], "请求数据验证失败");
|
||||
assert_eq!(upstream_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_admin_security_blacklist_add_attaches_explicit_audit() {
|
||||
let state = AppState::new().expect("gateway should build");
|
||||
@@ -311,6 +399,28 @@ async fn gateway_handles_admin_security_whitelist_remove_locally_with_trusted_ad
|
||||
assert_eq!(upstream_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_removes_percent_encoded_whitelist_cidr() {
|
||||
let gateway = build_router_with_state(
|
||||
AppState::new()
|
||||
.expect("gateway should build")
|
||||
.with_admin_security_whitelist_for_tests(["10.0.0.0/24".to_string()]),
|
||||
);
|
||||
|
||||
let (status, payload, upstream_count) = send_admin_security_request(
|
||||
gateway,
|
||||
reqwest::Method::DELETE,
|
||||
"/api/admin/security/ip/whitelist/10.0.0.0%2F24",
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(status, StatusCode::OK);
|
||||
assert_eq!(payload["success"], true);
|
||||
assert_eq!(payload["message"], "IP 10.0.0.0/24 已从白名单移除");
|
||||
assert_eq!(upstream_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_rejects_admin_security_whitelist_remove_without_ip_address() {
|
||||
let gateway = build_router_with_state(AppState::new().expect("gateway should build"));
|
||||
|
||||
@@ -1114,6 +1114,36 @@ async fn gateway_handles_admin_user_batch_actions_locally() {
|
||||
"不能降级最后一个管理员账户"
|
||||
);
|
||||
|
||||
let last_admin_audit_demotion_response = client
|
||||
.post(format!("{gateway_url}/api/admin/users/batch-action"))
|
||||
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
|
||||
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
|
||||
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
|
||||
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
|
||||
.json(&json!({
|
||||
"selection": {
|
||||
"user_ids": ["user-1"]
|
||||
},
|
||||
"action": "update_role",
|
||||
"payload": {
|
||||
"role": "audit_admin"
|
||||
}
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
assert_eq!(last_admin_audit_demotion_response.status(), StatusCode::OK);
|
||||
let last_admin_audit_demotion_payload: serde_json::Value = last_admin_audit_demotion_response
|
||||
.json()
|
||||
.await
|
||||
.expect("json body should parse");
|
||||
assert_eq!(last_admin_audit_demotion_payload["success"], 0);
|
||||
assert_eq!(last_admin_audit_demotion_payload["failed"], 1);
|
||||
assert_eq!(
|
||||
last_admin_audit_demotion_payload["failures"][0]["reason"],
|
||||
"不能降级最后一个管理员账户"
|
||||
);
|
||||
|
||||
let detail_response = client
|
||||
.get(format!("{gateway_url}/api/admin/users/user-1"))
|
||||
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
|
||||
@@ -1299,6 +1329,40 @@ async fn gateway_handles_admin_user_detail_routes_locally_with_trusted_admin_pri
|
||||
upstream_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_rejects_demoting_the_last_active_admin() {
|
||||
let gateway = build_router_with_state(
|
||||
AppState::new()
|
||||
.expect("gateway should build")
|
||||
.with_auth_users_for_tests([sample_admin_user_with_role(
|
||||
"admin-1",
|
||||
"admin",
|
||||
"[email protected]",
|
||||
"admin",
|
||||
)]),
|
||||
);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
for role in ["user", "audit_admin"] {
|
||||
let response = client
|
||||
.put(format!("{gateway_url}/api/admin/users/admin-1"))
|
||||
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
|
||||
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
|
||||
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
|
||||
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
|
||||
.json(&json!({ "role": role }))
|
||||
.send()
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||
assert_eq!(payload["detail"], "不能降级最后一个管理员账户");
|
||||
}
|
||||
|
||||
gateway_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_handles_admin_user_detail_locally_with_trusted_admin_principal() {
|
||||
let upstream_hits = Arc::new(Mutex::new(0usize));
|
||||
|
||||
Reference in New Issue
Block a user