mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
Fix admin import export version checks
This commit is contained in:
@@ -10,6 +10,7 @@ use aether_admin::system::{
|
|||||||
serialize_admin_system_users_export_wallet, AdminSystemConfigDocument, AdminSystemConfigEntry,
|
serialize_admin_system_users_export_wallet, AdminSystemConfigDocument, AdminSystemConfigEntry,
|
||||||
AdminSystemConfigGlobalModel, AdminSystemConfigLdap, AdminSystemConfigOAuthProvider,
|
AdminSystemConfigGlobalModel, AdminSystemConfigLdap, AdminSystemConfigOAuthProvider,
|
||||||
AdminSystemConfigProxyNode, ADMIN_SYSTEM_CONFIG_EXPORT_VERSION,
|
AdminSystemConfigProxyNode, ADMIN_SYSTEM_CONFIG_EXPORT_VERSION,
|
||||||
|
ADMIN_SYSTEM_USERS_EXPORT_VERSION,
|
||||||
};
|
};
|
||||||
use aether_data_contracts::repository::global_models::AdminGlobalModelListQuery;
|
use aether_data_contracts::repository::global_models::AdminGlobalModelListQuery;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
@@ -165,8 +166,6 @@ impl<'a> AdminAppState<'a> {
|
|||||||
pub(crate) async fn build_admin_system_users_export_payload(
|
pub(crate) async fn build_admin_system_users_export_payload(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<serde_json::Value, GatewayError> {
|
) -> Result<serde_json::Value, GatewayError> {
|
||||||
const ADMIN_SYSTEM_USERS_EXPORT_VERSION: &str = "1.3";
|
|
||||||
|
|
||||||
let users = self.list_non_admin_export_users().await?;
|
let users = self.list_non_admin_export_users().await?;
|
||||||
let user_ids = users.iter().map(|user| user.id.clone()).collect::<Vec<_>>();
|
let user_ids = users.iter().map(|user| user.id.clone()).collect::<Vec<_>>();
|
||||||
let user_wallets = self.list_wallet_snapshots_by_user_ids(&user_ids).await?;
|
let user_wallets = self.list_wallet_snapshots_by_user_ids(&user_ids).await?;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ use aether_admin::system::{
|
|||||||
AdminSystemConfigProviderKey as ImportedProviderKey,
|
AdminSystemConfigProviderKey as ImportedProviderKey,
|
||||||
AdminSystemConfigProviderModel as ImportedProviderModel,
|
AdminSystemConfigProviderModel as ImportedProviderModel,
|
||||||
AdminSystemConfigProxyNode as ImportedProxyNode,
|
AdminSystemConfigProxyNode as ImportedProxyNode,
|
||||||
ADMIN_SYSTEM_PROVIDER_OPS_SENSITIVE_CREDENTIAL_FIELDS,
|
ADMIN_SYSTEM_PROVIDER_OPS_SENSITIVE_CREDENTIAL_FIELDS, ADMIN_SYSTEM_USERS_SUPPORTED_VERSIONS,
|
||||||
};
|
};
|
||||||
use aether_data::repository::auth_modules::StoredLdapModuleConfig;
|
use aether_data::repository::auth_modules::StoredLdapModuleConfig;
|
||||||
use aether_data::repository::oauth_providers::{
|
use aether_data::repository::oauth_providers::{
|
||||||
@@ -46,7 +46,6 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
const ADMIN_SYSTEM_IMPORT_MAX_SIZE_BYTES: usize = 10 * 1024 * 1024;
|
const ADMIN_SYSTEM_IMPORT_MAX_SIZE_BYTES: usize = 10 * 1024 * 1024;
|
||||||
const MIN_ADMIN_SYSTEM_IMPORT_VERSION: (u32, u32) = (2, 2);
|
|
||||||
|
|
||||||
fn invalid_request(detail: impl Into<String>) -> (http::StatusCode, Value) {
|
fn invalid_request(detail: impl Into<String>) -> (http::StatusCode, Value) {
|
||||||
(
|
(
|
||||||
@@ -436,12 +435,19 @@ fn imported_system_export_version(version: Option<&Value>) -> Result<(u32, u32),
|
|||||||
Ok((major, minor))
|
Ok((major, minor))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_imported_system_export_version(version: Option<&Value>) -> Result<(), String> {
|
fn validate_imported_system_users_export_version(version: Option<&Value>) -> Result<(), String> {
|
||||||
let parsed = imported_system_export_version(version)?;
|
let Some(Value::String(raw_version)) = version else {
|
||||||
if parsed < MIN_ADMIN_SYSTEM_IMPORT_VERSION {
|
return Err("version 必须是 x.y 字符串".to_string());
|
||||||
|
};
|
||||||
|
let normalized = raw_version.trim();
|
||||||
|
if normalized.is_empty() {
|
||||||
|
return Err("version 必须是 x.y 字符串".to_string());
|
||||||
|
}
|
||||||
|
let _ = imported_system_export_version(version)?;
|
||||||
|
if !ADMIN_SYSTEM_USERS_SUPPORTED_VERSIONS.contains(&normalized) {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"version {}.{} 已不再支持;仅支持 2.2+ 导出格式",
|
"不支持的用户数据版本: {normalized},支持的版本: {}",
|
||||||
parsed.0, parsed.1
|
ADMIN_SYSTEM_USERS_SUPPORTED_VERSIONS.join(", ")
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1687,7 +1693,9 @@ impl<'a> AdminAppState<'a> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
invalid_value!(validate_imported_system_export_version(root.get("version")));
|
invalid_value!(validate_imported_system_users_export_version(
|
||||||
|
root.get("version")
|
||||||
|
));
|
||||||
|
|
||||||
let mut stats = AdminSystemUsersImportStats::default();
|
let mut stats = AdminSystemUsersImportStats::default();
|
||||||
|
|
||||||
@@ -2428,18 +2436,18 @@ mod tests {
|
|||||||
use super::{
|
use super::{
|
||||||
imported_optional_bool, imported_optional_f64, imported_optional_i32,
|
imported_optional_bool, imported_optional_f64, imported_optional_i32,
|
||||||
imported_optional_u64, imported_string_list_from_value,
|
imported_optional_u64, imported_string_list_from_value,
|
||||||
validate_imported_system_export_version,
|
validate_imported_system_users_export_version,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn import_requires_supported_export_version() {
|
fn users_import_requires_supported_export_version() {
|
||||||
assert!(validate_imported_system_export_version(Some(&json!("2.2"))).is_ok());
|
assert!(validate_imported_system_users_export_version(Some(&json!("1.3"))).is_ok());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
validate_imported_system_export_version(Some(&json!("2.1"))).unwrap_err(),
|
validate_imported_system_users_export_version(Some(&json!("2.2"))).unwrap_err(),
|
||||||
"version 2.1 已不再支持;仅支持 2.2+ 导出格式"
|
"不支持的用户数据版本: 2.2,支持的版本: 1.3"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
validate_imported_system_export_version(Some(&json!(null))).unwrap_err(),
|
validate_imported_system_users_export_version(Some(&json!(null))).unwrap_err(),
|
||||||
"version 必须是 x.y 字符串"
|
"version 必须是 x.y 字符串"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -575,7 +575,7 @@ async fn gateway_imports_admin_system_users_locally_and_persists_data() {
|
|||||||
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
|
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
|
||||||
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
|
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
|
||||||
.json(&json!({
|
.json(&json!({
|
||||||
"version": "2.2",
|
"version": "1.3",
|
||||||
"merge_mode": "overwrite",
|
"merge_mode": "overwrite",
|
||||||
"users": [{
|
"users": [{
|
||||||
"email": "alice@example.com",
|
"email": "alice@example.com",
|
||||||
@@ -867,7 +867,7 @@ async fn gateway_rejects_legacy_user_import_string_bool_field() {
|
|||||||
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
|
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
|
||||||
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
|
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
|
||||||
.json(&json!({
|
.json(&json!({
|
||||||
"version": "2.2",
|
"version": "1.3",
|
||||||
"merge_mode": "overwrite",
|
"merge_mode": "overwrite",
|
||||||
"users": [{
|
"users": [{
|
||||||
"email": "legacy@example.com",
|
"email": "legacy@example.com",
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ pub struct AdminEmailTemplateUpdate {
|
|||||||
|
|
||||||
pub const ADMIN_SYSTEM_CONFIG_EXPORT_VERSION: &str = "2.2";
|
pub const ADMIN_SYSTEM_CONFIG_EXPORT_VERSION: &str = "2.2";
|
||||||
pub const ADMIN_SYSTEM_CONFIG_SUPPORTED_VERSIONS: &[&str] = &[ADMIN_SYSTEM_CONFIG_EXPORT_VERSION];
|
pub const ADMIN_SYSTEM_CONFIG_SUPPORTED_VERSIONS: &[&str] = &[ADMIN_SYSTEM_CONFIG_EXPORT_VERSION];
|
||||||
|
pub const ADMIN_SYSTEM_USERS_EXPORT_VERSION: &str = "1.3";
|
||||||
|
pub const ADMIN_SYSTEM_USERS_SUPPORTED_VERSIONS: &[&str] = &[ADMIN_SYSTEM_USERS_EXPORT_VERSION];
|
||||||
pub const ADMIN_SYSTEM_PROVIDER_OPS_SENSITIVE_CREDENTIAL_FIELDS: &[&str] = &[
|
pub const ADMIN_SYSTEM_PROVIDER_OPS_SENSITIVE_CREDENTIAL_FIELDS: &[&str] = &[
|
||||||
"api_key",
|
"api_key",
|
||||||
"password",
|
"password",
|
||||||
|
|||||||
@@ -14,6 +14,31 @@ import type { SystemConfig } from './useSystemConfig'
|
|||||||
// 文件大小限制 (10MB)
|
// 文件大小限制 (10MB)
|
||||||
const MAX_FILE_SIZE = 10 * 1024 * 1024
|
const MAX_FILE_SIZE = 10 * 1024 * 1024
|
||||||
|
|
||||||
|
type JsonObject = Record<string, unknown>
|
||||||
|
|
||||||
|
function asJsonObject(value: unknown): JsonObject | null {
|
||||||
|
return value && typeof value === 'object' && !Array.isArray(value)
|
||||||
|
? value as JsonObject
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasArrayField(value: JsonObject, key: string): boolean {
|
||||||
|
return Array.isArray(value[key])
|
||||||
|
}
|
||||||
|
|
||||||
|
function looksLikeConfigExport(value: JsonObject): boolean {
|
||||||
|
return hasArrayField(value, 'global_models')
|
||||||
|
|| hasArrayField(value, 'providers')
|
||||||
|
|| hasArrayField(value, 'proxy_nodes')
|
||||||
|
|| hasArrayField(value, 'oauth_providers')
|
||||||
|
|| hasArrayField(value, 'system_configs')
|
||||||
|
|| Object.prototype.hasOwnProperty.call(value, 'ldap_config')
|
||||||
|
}
|
||||||
|
|
||||||
|
function looksLikeUsersExport(value: JsonObject): boolean {
|
||||||
|
return hasArrayField(value, 'users') || hasArrayField(value, 'standalone_keys')
|
||||||
|
}
|
||||||
|
|
||||||
export function useConfigExportImport(systemConfig: { value: SystemConfig }) {
|
export function useConfigExportImport(systemConfig: { value: SystemConfig }) {
|
||||||
const { success, error } = useToast()
|
const { success, error } = useToast()
|
||||||
|
|
||||||
@@ -83,13 +108,28 @@ export function useConfigExportImport(systemConfig: { value: SystemConfig }) {
|
|||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
try {
|
try {
|
||||||
const content = e.target?.result as string
|
const content = e.target?.result as string
|
||||||
const data = JSON.parse(content) as ConfigExportData
|
const root = asJsonObject(JSON.parse(content))
|
||||||
|
if (!root) {
|
||||||
|
error('无效的配置文件:JSON 顶层必须是对象')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!data.version) {
|
if (looksLikeUsersExport(root) && !looksLikeConfigExport(root)) {
|
||||||
|
error('这是用户数据导出文件,请使用“导入用户数据”')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!root.version) {
|
||||||
error('无效的配置文件:缺少版本信息')
|
error('无效的配置文件:缺少版本信息')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!looksLikeConfigExport(root)) {
|
||||||
|
error('无效的配置文件:未找到配置导出内容')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = root as unknown as ConfigExportData
|
||||||
importPreview.value = data
|
importPreview.value = data
|
||||||
mergeMode.value = 'skip'
|
mergeMode.value = 'skip'
|
||||||
importDialogOpen.value = true
|
importDialogOpen.value = true
|
||||||
@@ -170,8 +210,33 @@ export function useConfigExportImport(systemConfig: { value: SystemConfig }) {
|
|||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
try {
|
try {
|
||||||
const content = e.target?.result as string
|
const content = e.target?.result as string
|
||||||
const data = JSON.parse(content) as UsersExportData
|
const root = asJsonObject(JSON.parse(content))
|
||||||
|
if (!root) {
|
||||||
|
error('无效的用户数据文件:JSON 顶层必须是对象')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (looksLikeConfigExport(root) && !looksLikeUsersExport(root)) {
|
||||||
|
error('这是配置导出文件,请使用“导入配置”')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!root.version) {
|
||||||
|
error('无效的用户数据文件:缺少版本信息')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(root.users)) {
|
||||||
|
error('无效的用户数据文件:缺少 users 数组')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.standalone_keys != null && !Array.isArray(root.standalone_keys)) {
|
||||||
|
error('无效的用户数据文件:standalone_keys 必须是数组')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = root as unknown as UsersExportData
|
||||||
importUsersPreview.value = data
|
importUsersPreview.value = data
|
||||||
usersMergeMode.value = 'skip'
|
usersMergeMode.value = 'skip'
|
||||||
importUsersDialogOpen.value = true
|
importUsersDialogOpen.value = true
|
||||||
|
|||||||
Reference in New Issue
Block a user