mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
feat(admin): unify system data management and aggregate import/export
This commit is contained in:
@@ -63,6 +63,14 @@ pub(super) fn classify_admin_system_family_route(
|
||||
"admin:system",
|
||||
false,
|
||||
))
|
||||
} else if method == http::Method::GET && normalized_path == "/api/admin/system/data/export" {
|
||||
Some(classified(
|
||||
"admin_proxy",
|
||||
"system_manage",
|
||||
"data_export",
|
||||
"admin:system",
|
||||
false,
|
||||
))
|
||||
} else if method == http::Method::POST && normalized_path == "/api/admin/system/config/import" {
|
||||
Some(classified(
|
||||
"admin_proxy",
|
||||
@@ -79,6 +87,14 @@ pub(super) fn classify_admin_system_family_route(
|
||||
"admin:system",
|
||||
false,
|
||||
))
|
||||
} else if method == http::Method::POST && normalized_path == "/api/admin/system/data/import" {
|
||||
Some(classified(
|
||||
"admin_proxy",
|
||||
"system_manage",
|
||||
"data_import",
|
||||
"admin:system",
|
||||
false,
|
||||
))
|
||||
} else if method == http::Method::POST && normalized_path == "/api/admin/system/smtp/test" {
|
||||
Some(classified(
|
||||
"admin_proxy",
|
||||
|
||||
@@ -156,12 +156,32 @@ fn classifies_admin_system_users_export_as_admin_proxy_route() {
|
||||
assert!(!decision.is_execution_runtime_candidate());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_admin_system_data_export_as_admin_proxy_route() {
|
||||
let headers = headers(&[]);
|
||||
let uri: Uri = "/api/admin/system/data/export"
|
||||
.parse()
|
||||
.expect("uri should parse");
|
||||
let decision =
|
||||
classify_control_route(&http::Method::GET, &uri, &headers).expect("route should classify");
|
||||
|
||||
assert_eq!(decision.route_class.as_deref(), Some("admin_proxy"));
|
||||
assert_eq!(decision.route_family.as_deref(), Some("system_manage"));
|
||||
assert_eq!(decision.route_kind.as_deref(), Some("data_export"));
|
||||
assert_eq!(
|
||||
decision.auth_endpoint_signature.as_deref(),
|
||||
Some("admin:system")
|
||||
);
|
||||
assert!(!decision.is_execution_runtime_candidate());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_admin_system_maintenance_write_routes_as_admin_proxy_route() {
|
||||
let headers = headers(&[]);
|
||||
let cases = [
|
||||
("/api/admin/system/config/import", "config_import"),
|
||||
("/api/admin/system/users/import", "users_import"),
|
||||
("/api/admin/system/data/import", "data_import"),
|
||||
("/api/admin/system/smtp/test", "smtp_test"),
|
||||
("/api/admin/system/cleanup", "cleanup"),
|
||||
("/api/admin/system/purge/config", "purge_config"),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::ADMIN_SYSTEM_DATA_EXPORT_VERSION;
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::handlers::admin::system::shared::configs::is_sensitive_admin_system_config_key;
|
||||
use crate::handlers::admin::system::shared::export::{
|
||||
@@ -308,6 +309,20 @@ impl<'a> AdminAppState<'a> {
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_system_data_export_payload(
|
||||
&self,
|
||||
) -> Result<serde_json::Value, GatewayError> {
|
||||
let config_data = self.build_admin_system_config_export_payload().await?;
|
||||
let user_data = self.build_admin_system_users_export_payload().await?;
|
||||
|
||||
Ok(json!({
|
||||
"version": ADMIN_SYSTEM_DATA_EXPORT_VERSION,
|
||||
"exported_at": Utc::now().to_rfc3339(),
|
||||
"config_data": config_data,
|
||||
"user_data": user_data,
|
||||
}))
|
||||
}
|
||||
|
||||
fn build_admin_system_users_export_api_key_payload(
|
||||
&self,
|
||||
key: &aether_data::repository::auth::StoredAuthApiKeyExportRecord,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use super::AdminAppState;
|
||||
use super::{
|
||||
AdminAppState, ADMIN_SYSTEM_DATA_EXPORT_VERSION, ADMIN_SYSTEM_DATA_IMPORT_MAX_SIZE_BYTES,
|
||||
};
|
||||
use crate::api::ai::admin_endpoint_signature_parts;
|
||||
use crate::handlers::admin::provider::endpoints_admin::payloads::AdminProviderEndpointUpdatePatch;
|
||||
use crate::handlers::admin::provider::shared::payloads::{
|
||||
@@ -56,6 +58,26 @@ fn invalid_request(detail: impl Into<String>) -> (http::StatusCode, Value) {
|
||||
)
|
||||
}
|
||||
|
||||
fn build_admin_system_data_import_part_body(
|
||||
root: &Map<String, Value>,
|
||||
field_name: &str,
|
||||
merge_mode: AdminImportMergeMode,
|
||||
) -> Result<Bytes, (http::StatusCode, Value)> {
|
||||
let mut part = match root.get(field_name) {
|
||||
Some(Value::Object(map)) => map.clone(),
|
||||
Some(_) => return Err(invalid_request(format!("{field_name} 必须是对象"))),
|
||||
None => return Err(invalid_request(format!("{field_name} 为必填字段"))),
|
||||
};
|
||||
|
||||
let merge_mode_value = serde_json::to_value(merge_mode)
|
||||
.map_err(|err| invalid_request(format!("merge_mode 序列化失败: {err}")))?;
|
||||
part.insert("merge_mode".to_string(), merge_mode_value);
|
||||
|
||||
serde_json::to_vec(&Value::Object(part))
|
||||
.map(Bytes::from)
|
||||
.map_err(|err| invalid_request(format!("{field_name} 序列化失败: {err}")))
|
||||
}
|
||||
|
||||
fn trim_required(value: &str, field_name: &str) -> Result<String, String> {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
@@ -893,6 +915,91 @@ fn normalize_imported_wallet_target(
|
||||
}
|
||||
|
||||
impl<'a> AdminAppState<'a> {
|
||||
pub(crate) async fn import_admin_system_data(
|
||||
&self,
|
||||
request_body: &Bytes,
|
||||
operator_id: Option<&str>,
|
||||
) -> Result<Result<Value, (http::StatusCode, Value)>, GatewayError> {
|
||||
if !self.has_global_model_data_reader()
|
||||
|| !self.has_global_model_data_writer()
|
||||
|| !self.has_provider_catalog_data_reader()
|
||||
|| !self.has_provider_catalog_data_writer()
|
||||
|| !self.has_auth_user_write_capability()
|
||||
|| !self.has_auth_wallet_write_capability()
|
||||
|| !self.has_auth_api_key_writer()
|
||||
{
|
||||
return Ok(Err((
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
json!({ "detail": "Admin system data unavailable" }),
|
||||
)));
|
||||
}
|
||||
|
||||
if request_body.len() > ADMIN_SYSTEM_DATA_IMPORT_MAX_SIZE_BYTES {
|
||||
return Ok(Err(invalid_request("请求体大小不能超过 20MB")));
|
||||
}
|
||||
|
||||
let root = match serde_json::from_slice::<Value>(request_body) {
|
||||
Ok(Value::Object(map)) => map,
|
||||
_ => return Ok(Err(invalid_request("请求数据验证失败"))),
|
||||
};
|
||||
|
||||
let version = root
|
||||
.get("version")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| invalid_request("version 为必填字段"));
|
||||
let version = match version {
|
||||
Ok(value) => value,
|
||||
Err(err) => return Ok(Err(err)),
|
||||
};
|
||||
if version != ADMIN_SYSTEM_DATA_EXPORT_VERSION {
|
||||
return Ok(Err(invalid_request(format!(
|
||||
"不支持的聚合数据版本: {version},支持的版本: {ADMIN_SYSTEM_DATA_EXPORT_VERSION}"
|
||||
))));
|
||||
}
|
||||
|
||||
let merge_mode = match serde_json::from_value::<AdminImportMergeMode>(
|
||||
root.get("merge_mode").cloned().unwrap_or(Value::Null),
|
||||
) {
|
||||
Ok(value) => value,
|
||||
Err(_) => {
|
||||
return Ok(Err(invalid_request(
|
||||
"merge_mode 仅支持 skip / overwrite / error",
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
let config_body =
|
||||
match build_admin_system_data_import_part_body(&root, "config_data", merge_mode) {
|
||||
Ok(value) => value,
|
||||
Err(err) => return Ok(Err(err)),
|
||||
};
|
||||
let users_body =
|
||||
match build_admin_system_data_import_part_body(&root, "user_data", merge_mode) {
|
||||
Ok(value) => value,
|
||||
Err(err) => return Ok(Err(err)),
|
||||
};
|
||||
|
||||
let config_result = match self.import_admin_system_config(&config_body).await? {
|
||||
Ok(payload) => payload,
|
||||
Err(err) => return Ok(Err(err)),
|
||||
};
|
||||
let users_result = match self
|
||||
.import_admin_system_users(&users_body, operator_id)
|
||||
.await?
|
||||
{
|
||||
Ok(payload) => payload,
|
||||
Err(err) => return Ok(Err(err)),
|
||||
};
|
||||
|
||||
Ok(Ok(json!({
|
||||
"message": "聚合数据导入成功",
|
||||
"config": config_result,
|
||||
"users": users_result,
|
||||
})))
|
||||
}
|
||||
|
||||
pub(crate) async fn import_admin_system_config(
|
||||
&self,
|
||||
request_body: &Bytes,
|
||||
|
||||
@@ -8,6 +8,9 @@ mod modules;
|
||||
mod proxy_nodes;
|
||||
mod templates;
|
||||
|
||||
const ADMIN_SYSTEM_DATA_EXPORT_VERSION: &str = "1.0";
|
||||
const ADMIN_SYSTEM_DATA_IMPORT_MAX_SIZE_BYTES: usize = 20 * 1024 * 1024;
|
||||
|
||||
impl<'a> AdminAppState<'a> {
|
||||
pub(crate) async fn upsert_system_config_json_value(
|
||||
&self,
|
||||
|
||||
@@ -180,6 +180,55 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
|
||||
));
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("data_export")
|
||||
&& request_method == http::Method::GET
|
||||
&& request_path == "/api/admin/system/data/export"
|
||||
{
|
||||
return Ok(Some(attach_admin_audit_response(
|
||||
Json(state.build_admin_system_data_export_payload().await?).into_response(),
|
||||
"admin_system_data_exported",
|
||||
"export_system_data",
|
||||
"system_data_export",
|
||||
"global",
|
||||
)));
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("data_import")
|
||||
&& request_method == http::Method::POST
|
||||
&& request_path == "/api/admin/system/data/import"
|
||||
{
|
||||
let Some(request_body) = request_body else {
|
||||
return Ok(Some(
|
||||
(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
Json(json!({ "detail": "请求数据验证失败" })),
|
||||
)
|
||||
.into_response(),
|
||||
));
|
||||
};
|
||||
return Ok(Some(
|
||||
match state
|
||||
.import_admin_system_data(
|
||||
request_body,
|
||||
decision
|
||||
.admin_principal
|
||||
.as_ref()
|
||||
.map(|principal| principal.user_id.as_str()),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Ok(payload) => attach_admin_audit_response(
|
||||
Json(payload).into_response(),
|
||||
"admin_system_data_imported",
|
||||
"import_system_data",
|
||||
"system_data_import",
|
||||
"global",
|
||||
),
|
||||
Err((status, payload)) => (status, Json(payload)).into_response(),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("smtp_test")
|
||||
&& request_method == http::Method::POST
|
||||
&& request_path == "/api/admin/system/smtp/test"
|
||||
|
||||
@@ -249,6 +249,7 @@ pub(crate) fn admin_proxy_local_requires_buffered_body(
|
||||
| (Some("provider_oauth_manage"), http::Method::POST, Some("device_poll"))
|
||||
| (Some("system_manage"), http::Method::POST, Some("config_import"))
|
||||
| (Some("system_manage"), http::Method::POST, Some("users_import"))
|
||||
| (Some("system_manage"), http::Method::POST, Some("data_import"))
|
||||
| (Some("system_manage"), http::Method::PUT, Some("settings_set"))
|
||||
| (Some("system_manage"), http::Method::PUT, Some("config_set"))
|
||||
| (Some("system_manage"), http::Method::PUT, Some("email_template_set"))
|
||||
|
||||
@@ -259,6 +259,7 @@ fn admin_system_owns_system_route_helpers() {
|
||||
"build_admin_api_formats_payload",
|
||||
"build_admin_system_config_export_payload",
|
||||
"build_admin_system_users_export_payload",
|
||||
"build_admin_system_data_export_payload",
|
||||
"build_admin_system_configs_payload",
|
||||
"build_admin_system_config_detail_payload",
|
||||
"apply_admin_system_config_update",
|
||||
@@ -287,6 +288,7 @@ fn admin_system_owns_system_route_helpers() {
|
||||
"build_admin_api_formats_payload",
|
||||
"build_admin_system_config_export_payload",
|
||||
"build_admin_system_users_export_payload",
|
||||
"build_admin_system_data_export_payload",
|
||||
"build_admin_system_configs_payload",
|
||||
"build_admin_system_config_detail_payload",
|
||||
"apply_admin_system_config_update",
|
||||
@@ -315,6 +317,7 @@ fn admin_system_owns_system_route_helpers() {
|
||||
"pub(crate) async fn build_admin_system_settings_payload",
|
||||
"pub(crate) async fn build_admin_system_config_export_payload",
|
||||
"pub(crate) async fn build_admin_system_users_export_payload",
|
||||
"pub(crate) async fn build_admin_system_data_export_payload",
|
||||
"pub(crate) fn build_admin_system_configs_payload",
|
||||
"pub(crate) async fn build_admin_system_config_detail_payload",
|
||||
"pub(crate) async fn apply_admin_system_config_update",
|
||||
@@ -447,6 +450,7 @@ fn admin_system_owns_system_route_helpers() {
|
||||
"pub(crate) async fn reset_admin_email_template",
|
||||
"pub(crate) async fn build_admin_system_config_export_payload",
|
||||
"pub(crate) async fn build_admin_system_users_export_payload",
|
||||
"pub(crate) async fn build_admin_system_data_export_payload",
|
||||
] {
|
||||
assert!(
|
||||
request_system.contains(pattern),
|
||||
@@ -511,6 +515,7 @@ fn admin_system_shared_configs_split_export_owners() {
|
||||
for pattern in [
|
||||
"pub(crate) async fn build_admin_system_config_export_payload(",
|
||||
"pub(crate) async fn build_admin_system_users_export_payload(",
|
||||
"pub(crate) async fn build_admin_system_data_export_payload(",
|
||||
] {
|
||||
assert!(
|
||||
request_system.contains(pattern),
|
||||
|
||||
@@ -839,6 +839,7 @@ async fn gateway_handles_admin_system_unavailable_write_routes_locally_with_trus
|
||||
let unavailable_paths = [
|
||||
"/api/admin/system/config/import",
|
||||
"/api/admin/system/users/import",
|
||||
"/api/admin/system/data/import",
|
||||
];
|
||||
let local_paths = [
|
||||
"/api/admin/system/cleanup",
|
||||
@@ -862,7 +863,11 @@ async fn gateway_handles_admin_system_unavailable_write_routes_locally_with_trus
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
|
||||
assert_eq!(
|
||||
response.status(),
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
"path={path}"
|
||||
);
|
||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||
assert_eq!(payload["detail"], DETAIL, "path={path}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user