mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-10 03:09:50 +08:00
fix(admin): classify bulk wallet adjustment failures
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
use super::{
|
||||
build_admin_users_bad_request_response, build_admin_users_permission_denied_response,
|
||||
build_admin_users_read_only_response, disabled_user_policy_detail, disabled_user_policy_field,
|
||||
build_admin_users_read_only_response, build_admin_users_wallet_permission_denied_response,
|
||||
disabled_user_policy_detail, disabled_user_policy_field,
|
||||
management_token_may_adjust_admin_wallet_balance,
|
||||
management_token_may_administer_user_accounts, normalize_admin_user_role,
|
||||
};
|
||||
@@ -111,6 +112,11 @@ enum AdminUserWalletBalanceOperation {
|
||||
Deduct,
|
||||
}
|
||||
|
||||
enum AdminBatchWalletBalanceAdjustmentError {
|
||||
WalletLookup,
|
||||
BalanceAdjustment,
|
||||
}
|
||||
|
||||
pub(in super::super) async fn build_admin_resolve_user_selection_response(
|
||||
state: &AdminAppState<'_>,
|
||||
_request_context: &AdminRequestContext<'_>,
|
||||
@@ -177,7 +183,7 @@ pub(in super::super) async fn build_admin_user_batch_action_response(
|
||||
if mutation.wallet_balance_adjustment.is_some()
|
||||
&& !management_token_may_adjust_admin_wallet_balance(request_context)
|
||||
{
|
||||
return Ok(build_admin_users_permission_denied_response(
|
||||
return Ok(build_admin_users_wallet_permission_denied_response(
|
||||
request_context,
|
||||
));
|
||||
}
|
||||
@@ -290,7 +296,20 @@ pub(in super::super) async fn build_admin_user_batch_action_response(
|
||||
}));
|
||||
continue;
|
||||
}
|
||||
Err(_) => {
|
||||
Err(AdminBatchWalletBalanceAdjustmentError::WalletLookup) => {
|
||||
record_batch_action_interruption(
|
||||
&resolved.items,
|
||||
item_index,
|
||||
false,
|
||||
"读取用户钱包失败,批次已中止,该用户未执行",
|
||||
&mut failures,
|
||||
&mut uncertain_user_ids,
|
||||
&mut unprocessed_user_ids,
|
||||
);
|
||||
interrupted = true;
|
||||
break;
|
||||
}
|
||||
Err(AdminBatchWalletBalanceAdjustmentError::BalanceAdjustment) => {
|
||||
record_batch_action_interruption(
|
||||
&resolved.items,
|
||||
item_index,
|
||||
@@ -903,13 +922,14 @@ async fn apply_batch_user_wallet_balance_adjustment(
|
||||
user_id: &str,
|
||||
adjustment: AdminUserWalletBalanceAdjustment,
|
||||
operator_id: Option<&str>,
|
||||
) -> Result<bool, GatewayError> {
|
||||
) -> Result<bool, AdminBatchWalletBalanceAdjustmentError> {
|
||||
// Resolve only the wallet ID; the repository clamps the deduction under its row lock.
|
||||
let Some(wallet) = state
|
||||
.find_wallet(aether_data::repository::wallet::WalletLookupKey::UserId(
|
||||
user_id,
|
||||
))
|
||||
.await?
|
||||
.await
|
||||
.map_err(|_| AdminBatchWalletBalanceAdjustmentError::WalletLookup)?
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
@@ -918,7 +938,7 @@ async fn apply_batch_user_wallet_balance_adjustment(
|
||||
AdminUserWalletBalanceOperation::Deduct => -adjustment.amount,
|
||||
};
|
||||
|
||||
Ok(state
|
||||
state
|
||||
.admin_adjust_wallet_balance(
|
||||
&wallet.id,
|
||||
amount,
|
||||
@@ -927,8 +947,9 @@ async fn apply_batch_user_wallet_balance_adjustment(
|
||||
Some("管理员批量调整用户余额"),
|
||||
true,
|
||||
)
|
||||
.await?
|
||||
.is_some())
|
||||
.await
|
||||
.map_err(|_| AdminBatchWalletBalanceAdjustmentError::BalanceAdjustment)
|
||||
.map(|result| result.is_some())
|
||||
}
|
||||
|
||||
fn build_admin_user_batch_bad_request_response(detail: String) -> Response<Body> {
|
||||
|
||||
@@ -49,9 +49,10 @@ use self::shared::AdminUpdateUserPatch;
|
||||
use self::shared::{
|
||||
admin_default_user_initial_gift, build_admin_users_bad_request_response,
|
||||
build_admin_users_data_unavailable_response, build_admin_users_permission_denied_response,
|
||||
build_admin_users_read_only_response, disabled_user_policy_detail, disabled_user_policy_field,
|
||||
format_optional_datetime_iso8601, legacy_admin_list_policy_mode,
|
||||
legacy_admin_rate_limit_policy_mode, management_token_may_adjust_admin_wallet_balance,
|
||||
build_admin_users_read_only_response, build_admin_users_wallet_permission_denied_response,
|
||||
disabled_user_policy_detail, disabled_user_policy_field, format_optional_datetime_iso8601,
|
||||
legacy_admin_list_policy_mode, legacy_admin_rate_limit_policy_mode,
|
||||
management_token_may_adjust_admin_wallet_balance,
|
||||
management_token_may_administer_user_accounts, normalize_admin_optional_user_email,
|
||||
normalize_admin_user_group_ids, normalize_admin_user_role, normalize_admin_username,
|
||||
validate_admin_user_password, AdminCreateUserApiKeyRequest, AdminCreateUserRequest,
|
||||
|
||||
@@ -204,6 +204,34 @@ pub(super) fn build_admin_users_permission_denied_response(
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn build_admin_users_wallet_permission_denied_response(
|
||||
request_context: &crate::handlers::admin::request::AdminRequestContext<'_>,
|
||||
) -> Response<Body> {
|
||||
let actor_id = request_context
|
||||
.decision()
|
||||
.and_then(|decision| decision.admin_principal.as_ref())
|
||||
.and_then(|principal| principal.management_token_id.as_deref())
|
||||
.unwrap_or("unknown");
|
||||
crate::handlers::admin::shared::attach_admin_audit_response(
|
||||
(
|
||||
http::StatusCode::FORBIDDEN,
|
||||
Json(json!({
|
||||
"detail": "management token permission denied",
|
||||
"required_permissions": ["admin:wallets:write", "admin:wallets:admin"],
|
||||
"permission_mode": "any_of",
|
||||
"route_family": request_context.route_family(),
|
||||
"route_kind": request_context.route_kind(),
|
||||
"request_path": request_context.path(),
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
"admin_user_wallet_balance_permission_denied",
|
||||
"permission_denied",
|
||||
"admin_user_wallet_balance",
|
||||
actor_id,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn normalize_admin_optional_user_email(
|
||||
value: Option<&str>,
|
||||
) -> Result<Option<String>, String> {
|
||||
@@ -409,9 +437,52 @@ pub(super) fn format_optional_datetime_iso8601(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{normalize_admin_user_api_formats, AdminUpdateUserApiKeyRequest};
|
||||
use super::{
|
||||
build_admin_users_wallet_permission_denied_response, normalize_admin_user_api_formats,
|
||||
AdminUpdateUserApiKeyRequest,
|
||||
};
|
||||
use crate::control::{GatewayControlDecision, GatewayPublicRequestContext};
|
||||
use crate::handlers::admin::request::AdminRequestContext;
|
||||
use axum::http::{HeaderMap, Method, Uri};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn wallet_permission_denial_uses_wallet_audit_category() {
|
||||
let uri: Uri = "/api/admin/users/batch-action"
|
||||
.parse()
|
||||
.expect("uri should parse");
|
||||
let method = Method::POST;
|
||||
let headers = HeaderMap::new();
|
||||
let decision = GatewayControlDecision::synthetic(
|
||||
uri.path(),
|
||||
Some("admin_proxy".to_string()),
|
||||
Some("users_manage".to_string()),
|
||||
Some("batch_user_action".to_string()),
|
||||
Some("admin:users".to_string()),
|
||||
);
|
||||
let context = GatewayPublicRequestContext::from_request_parts(
|
||||
"trace-wallet-permission-denied",
|
||||
&method,
|
||||
&uri,
|
||||
&headers,
|
||||
Some(decision),
|
||||
);
|
||||
let request_context = AdminRequestContext::new(&context);
|
||||
|
||||
let response = build_admin_users_wallet_permission_denied_response(&request_context);
|
||||
let event = response
|
||||
.extensions()
|
||||
.get::<crate::audit::AdminAuditEvent>()
|
||||
.expect("wallet denial should attach an audit event");
|
||||
|
||||
assert_eq!(
|
||||
event.event_name,
|
||||
"admin_user_wallet_balance_permission_denied"
|
||||
);
|
||||
assert_eq!(event.action, "permission_denied");
|
||||
assert_eq!(event.target_type, "admin_user_wallet_balance");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admin_user_api_formats_accept_current_canonical_signatures() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user