mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-06 09:27:46 +08:00
feat(security): harden client IP and admin controls
This commit is contained in:
@@ -49,12 +49,13 @@ fn build_admin_security_not_found_response(detail: impl Into<String>) -> Respons
|
||||
}
|
||||
|
||||
fn admin_security_blacklist_ip_from_path(request_path: &str) -> Option<String> {
|
||||
let value = request_path
|
||||
.strip_prefix("/api/admin/security/ip/blacklist/")?
|
||||
.trim()
|
||||
.trim_matches('/')
|
||||
.to_string();
|
||||
if value.is_empty() || value.contains('/') {
|
||||
let value = decode_admin_security_path_value(
|
||||
request_path
|
||||
.strip_prefix("/api/admin/security/ip/blacklist/")?
|
||||
.trim()
|
||||
.trim_matches('/'),
|
||||
)?;
|
||||
if value.parse::<std::net::IpAddr>().is_err() {
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
@@ -62,18 +63,49 @@ fn admin_security_blacklist_ip_from_path(request_path: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
fn admin_security_whitelist_ip_from_path(request_path: &str) -> Option<String> {
|
||||
let value = request_path
|
||||
.strip_prefix("/api/admin/security/ip/whitelist/")?
|
||||
.trim()
|
||||
.trim_matches('/')
|
||||
.to_string();
|
||||
if value.is_empty() || value.contains('/') {
|
||||
let value = decode_admin_security_path_value(
|
||||
request_path
|
||||
.strip_prefix("/api/admin/security/ip/whitelist/")?
|
||||
.trim()
|
||||
.trim_matches('/'),
|
||||
)?;
|
||||
if !admin_security_validate_ip_or_cidr(&value) {
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_admin_security_path_value(value: &str) -> Option<String> {
|
||||
if value.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let bytes = value.as_bytes();
|
||||
let mut decoded = Vec::with_capacity(bytes.len());
|
||||
let mut index = 0;
|
||||
while index < bytes.len() {
|
||||
if bytes[index] == b'%' {
|
||||
let high = *bytes.get(index + 1)?;
|
||||
let low = *bytes.get(index + 2)?;
|
||||
decoded.push((decode_hex_digit(high)? << 4) | decode_hex_digit(low)?);
|
||||
index += 3;
|
||||
} else {
|
||||
decoded.push(bytes[index]);
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
String::from_utf8(decoded).ok()
|
||||
}
|
||||
|
||||
fn decode_hex_digit(value: u8) -> Option<u8> {
|
||||
match value {
|
||||
b'0'..=b'9' => Some(value - b'0'),
|
||||
b'a'..=b'f' => Some(value - b'a' + 10),
|
||||
b'A'..=b'F' => Some(value - b'A' + 10),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn admin_security_validate_ip_or_cidr(value: &str) -> bool {
|
||||
let value = value.trim();
|
||||
if value.is_empty() {
|
||||
@@ -107,7 +139,12 @@ async fn build_admin_security_blacklist_add_response(
|
||||
));
|
||||
};
|
||||
let payload = match serde_json::from_slice::<AdminSecurityBlacklistAddRequest>(request_body) {
|
||||
Ok(value) if !value.ip_address.trim().is_empty() && !value.reason.trim().is_empty() => {
|
||||
Ok(value)
|
||||
if value.ip_address.trim().parse::<std::net::IpAddr>().is_ok()
|
||||
&& !value.reason.trim().is_empty()
|
||||
&& value.reason.trim().chars().count() <= 200
|
||||
&& value.ttl.is_none_or(|ttl| ttl > 0) =>
|
||||
{
|
||||
value
|
||||
}
|
||||
_ => {
|
||||
|
||||
@@ -643,7 +643,11 @@ fn count_active_admin_demotions(
|
||||
mutation: &AdminUserBatchMutation,
|
||||
items: &[AdminUserSelectionItem],
|
||||
) -> usize {
|
||||
if mutation.role.as_deref() != Some("user") {
|
||||
if mutation
|
||||
.role
|
||||
.as_deref()
|
||||
.is_none_or(crate::roles::is_full_admin_role)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
items
|
||||
@@ -659,7 +663,10 @@ fn batch_role_demotion_failure_reason(
|
||||
active_admin_demotions: usize,
|
||||
current_admin_user_id: Option<&str>,
|
||||
) -> Option<&'static str> {
|
||||
if mutation.role.as_deref() != Some("user")
|
||||
if mutation
|
||||
.role
|
||||
.as_deref()
|
||||
.is_none_or(crate::roles::is_full_admin_role)
|
||||
|| !item.is_active
|
||||
|| !item.role.eq_ignore_ascii_case("admin")
|
||||
{
|
||||
|
||||
@@ -150,6 +150,19 @@ pub(in super::super) async fn build_admin_update_user_response(
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
if existing_user.is_active
|
||||
&& crate::roles::is_full_admin_role(&existing_user.role)
|
||||
&& role
|
||||
.as_deref()
|
||||
.is_some_and(|role| !crate::roles::is_full_admin_role(role))
|
||||
&& state.count_active_admin_users().await? <= 1
|
||||
{
|
||||
return Ok((
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
Json(json!({ "detail": "不能降级最后一个管理员账户" })),
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
let effective_role = role.as_deref().unwrap_or(existing_user.role.as_str());
|
||||
let group_ids = if field_presence.contains("group_ids") {
|
||||
Some(normalize_admin_user_group_ids(payload.group_ids))
|
||||
|
||||
Reference in New Issue
Block a user