refactor: 大规模模块拆分与重组,新增 aether-admin crate

- 新建独立 aether-admin crate 承载 admin 相关共享契约与纯辅助函数
- 拆分 ai_pipeline 下 kiro/private_envelope/conversion/planner 等大文件为子模块目录
- 重组 admin handlers 各业务域(billing/oauth/provider/system/users 等)为目录结构,移除 shared.rs/builders.rs 等反模式
- 移除 ai_pipeline runtime adapters 旧实现(claude/openai/gemini/kiro/vertex/antigravity 等),改由 provider transport 统一承载
- 移除 control_facade/execution_facade/auth_snapshot_facade 等冗余 facade 层
- 拆分 query/billing 与 query/monitoring 模块、state/runtime/payments 与 security 模块
- 扩展架构测试覆盖 admin_billing/admin_model/admin_users 等新模块
- 删除 docs/architecture/refactor-execution-plan.md 已完成的执行计划文档
This commit is contained in:
fawney19
2026-04-09 00:10:38 +08:00
parent 4fb9882b54
commit 4fc95adfb9
663 changed files with 48471 additions and 40232 deletions
@@ -4,13 +4,12 @@ use super::super::users::{
normalize_admin_optional_api_key_name, normalize_admin_user_api_formats,
normalize_admin_user_string_list,
};
use crate::control::GatewayPublicRequestContext;
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
use crate::handlers::admin::shared::{
decrypt_catalog_secret_with_fallbacks, encrypt_catalog_secret_with_fallbacks, query_param_bool,
query_param_optional_bool, query_param_value,
};
use crate::handlers::admin::system::shared::configs::serialize_admin_system_users_export_wallet;
use crate::{AppState, GatewayError};
use crate::GatewayError;
use axum::{
body::Body,
http,
@@ -19,8 +18,6 @@ use axum::{
};
use serde_json::json;
const ADMIN_API_KEYS_DATA_UNAVAILABLE_DETAIL: &str = "Admin standalone API key data unavailable";
mod mutation_routes;
mod read_routes;
mod routes;
@@ -41,8 +38,8 @@ use self::shared::{
};
pub(crate) async fn maybe_build_local_admin_api_keys_response(
state: &AppState,
request_context: &GatewayPublicRequestContext,
state: &AdminAppState<'_>,
request_context: &AdminRequestContext<'_>,
request_body: Option<&axum::body::Bytes>,
) -> Result<Option<Response<Body>>, GatewayError> {
routes::maybe_build_local_admin_api_keys_routes_response(state, request_context, request_body)
@@ -5,15 +5,15 @@ use super::shared::{
AdminStandaloneApiKeyCreateRequest, AdminStandaloneApiKeyFieldPresence,
AdminStandaloneApiKeyToggleRequest, AdminStandaloneApiKeyUpdateRequest,
};
use super::{
default_admin_user_api_key_name, encrypt_catalog_secret_with_fallbacks,
format_optional_unix_secs_iso8601, generate_admin_user_api_key_plaintext,
hash_admin_user_api_key, masked_user_api_key_display, normalize_admin_optional_api_key_name,
normalize_admin_user_api_formats, normalize_admin_user_string_list,
};
use crate::control::GatewayPublicRequestContext;
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
use crate::handlers::admin::shared::attach_admin_audit_response;
use crate::{AppState, GatewayError};
use crate::handlers::admin::users::{
default_admin_user_api_key_name, format_optional_unix_secs_iso8601,
generate_admin_user_api_key_plaintext, hash_admin_user_api_key, masked_user_api_key_display,
normalize_admin_optional_api_key_name, normalize_admin_user_api_formats,
normalize_admin_user_string_list,
};
use crate::GatewayError;
use axum::{
body::Body,
http,
@@ -23,11 +23,11 @@ use axum::{
use serde_json::json;
pub(super) async fn build_admin_create_api_key_response(
state: &AppState,
request_context: &GatewayPublicRequestContext,
state: &AdminAppState<'_>,
request_context: &AdminRequestContext<'_>,
request_body: Option<&axum::body::Bytes>,
) -> Result<Response<Body>, GatewayError> {
if !state.data.has_auth_api_key_writer() {
if !state.has_auth_api_key_writer() {
return Ok(build_admin_api_keys_data_unavailable_response());
}
@@ -44,7 +44,7 @@ pub(super) async fn build_admin_create_api_key_response(
Err(_) => {
return Ok(build_admin_api_keys_bad_request_response(
"请求数据验证失败",
))
));
}
};
if payload.initial_balance_usd.is_some()
@@ -85,7 +85,7 @@ pub(super) async fn build_admin_create_api_key_response(
}
let plaintext_key = generate_admin_user_api_key_plaintext();
let Some(key_encrypted) = encrypt_catalog_secret_with_fallbacks(state, &plaintext_key) else {
let Some(key_encrypted) = state.encrypt_catalog_secret_with_fallbacks(&plaintext_key) else {
return Ok((
http::StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "detail": "API密钥加密失败" })),
@@ -138,15 +138,15 @@ pub(super) async fn build_admin_create_api_key_response(
}
pub(super) async fn build_admin_update_api_key_response(
state: &AppState,
request_context: &GatewayPublicRequestContext,
state: &AdminAppState<'_>,
request_context: &AdminRequestContext<'_>,
request_body: Option<&axum::body::Bytes>,
) -> Result<Response<Body>, GatewayError> {
if !state.data.has_auth_api_key_writer() {
if !state.has_auth_api_key_writer() {
return Ok(build_admin_api_keys_data_unavailable_response());
}
let Some(api_key_id) = admin_api_keys_id_from_path(&request_context.request_path) else {
let Some(api_key_id) = admin_api_keys_id_from_path(request_context.path()) else {
return Ok(build_admin_api_keys_data_unavailable_response());
};
let Some(request_body) = request_body else {
@@ -159,7 +159,7 @@ pub(super) async fn build_admin_update_api_key_response(
_ => {
return Ok(build_admin_api_keys_bad_request_response(
"请求数据验证失败",
))
));
}
};
let field_presence = AdminStandaloneApiKeyFieldPresence {
@@ -174,7 +174,7 @@ pub(super) async fn build_admin_update_api_key_response(
Err(_) => {
return Ok(build_admin_api_keys_bad_request_response(
"请求数据验证失败",
))
));
}
};
if payload.initial_balance_usd.is_some()
@@ -262,15 +262,15 @@ pub(super) async fn build_admin_update_api_key_response(
}
pub(super) async fn build_admin_toggle_api_key_response(
state: &AppState,
request_context: &GatewayPublicRequestContext,
state: &AdminAppState<'_>,
request_context: &AdminRequestContext<'_>,
request_body: Option<&axum::body::Bytes>,
) -> Result<Response<Body>, GatewayError> {
if !state.data.has_auth_api_key_writer() {
if !state.has_auth_api_key_writer() {
return Ok(build_admin_api_keys_data_unavailable_response());
}
let Some(api_key_id) = admin_api_keys_id_from_path(&request_context.request_path) else {
let Some(api_key_id) = admin_api_keys_id_from_path(request_context.path()) else {
return Ok(build_admin_api_keys_data_unavailable_response());
};
@@ -283,17 +283,15 @@ pub(super) async fn build_admin_toggle_api_key_response(
Err(_) => {
return Ok(build_admin_api_keys_bad_request_response(
"请求数据验证失败",
))
));
}
}
}
};
let Some(snapshot) = state
.data
.list_auth_api_key_snapshots_by_ids(std::slice::from_ref(&api_key_id))
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?
.await?
.into_iter()
.find(|snapshot| snapshot.api_key_id == api_key_id)
else {
@@ -326,14 +324,14 @@ pub(super) async fn build_admin_toggle_api_key_response(
}
pub(super) async fn build_admin_delete_api_key_response(
state: &AppState,
request_context: &GatewayPublicRequestContext,
state: &AdminAppState<'_>,
request_context: &AdminRequestContext<'_>,
) -> Result<Response<Body>, GatewayError> {
if !state.data.has_auth_api_key_writer() {
if !state.has_auth_api_key_writer() {
return Ok(build_admin_api_keys_data_unavailable_response());
}
let Some(api_key_id) = admin_api_keys_id_from_path(&request_context.request_path) else {
let Some(api_key_id) = admin_api_keys_id_from_path(request_context.path()) else {
return Ok(build_admin_api_keys_data_unavailable_response());
};
@@ -5,9 +5,9 @@ use super::shared::{
build_admin_api_keys_data_unavailable_response, build_admin_api_keys_not_found_response,
};
use super::{decrypt_catalog_secret_with_fallbacks, query_param_bool, query_param_optional_bool};
use crate::control::GatewayPublicRequestContext;
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
use crate::handlers::admin::shared::attach_admin_audit_response;
use crate::{AppState, GatewayError};
use crate::GatewayError;
use axum::{
body::Body,
http,
@@ -19,11 +19,11 @@ use std::time::Instant;
use tracing::info;
pub(super) async fn build_admin_list_api_keys_response(
state: &AppState,
request_context: &GatewayPublicRequestContext,
state: &AdminAppState<'_>,
request_context: &AdminRequestContext<'_>,
) -> Result<Response<Body>, GatewayError> {
let handler_started_at = Instant::now();
let query = request_context.request_query_string.as_deref();
let query = request_context.query_string();
let skip = match admin_api_keys_parse_skip(query) {
Ok(value) => value,
Err(detail) => return Ok(build_admin_api_keys_bad_request_response(detail)),
@@ -109,18 +109,16 @@ pub(super) async fn build_admin_list_api_keys_response(
}
pub(super) async fn build_admin_api_key_detail_response(
state: &AppState,
request_context: &GatewayPublicRequestContext,
state: &AdminAppState<'_>,
request_context: &AdminRequestContext<'_>,
) -> Result<Response<Body>, GatewayError> {
let Some(api_key_id) = admin_api_keys_id_from_path(&request_context.request_path) else {
let Some(api_key_id) = admin_api_keys_id_from_path(request_context.path()) else {
return Ok(build_admin_api_keys_data_unavailable_response());
};
if state
.data
.list_auth_api_key_snapshots_by_ids(std::slice::from_ref(&api_key_id))
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?
.await?
.into_iter()
.any(|snapshot| snapshot.api_key_id == api_key_id && !snapshot.api_key_is_standalone)
{
@@ -136,11 +134,7 @@ pub(super) async fn build_admin_api_key_detail_response(
return Ok(build_admin_api_keys_not_found_response());
};
if query_param_bool(
request_context.request_query_string.as_deref(),
"include_key",
false,
) {
if query_param_bool(request_context.query_string(), "include_key", false) {
let Some(ciphertext) = record
.key_encrypted
.as_deref()
@@ -4,16 +4,16 @@ use super::mutation_routes::{
};
use super::read_routes::{build_admin_api_key_detail_response, build_admin_list_api_keys_response};
use super::shared::build_admin_api_keys_data_unavailable_response;
use crate::control::GatewayPublicRequestContext;
use crate::{AppState, GatewayError};
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
use crate::GatewayError;
use axum::{body::Body, http, response::Response};
pub(super) async fn maybe_build_local_admin_api_keys_routes_response(
state: &AppState,
request_context: &GatewayPublicRequestContext,
state: &AdminAppState<'_>,
request_context: &AdminRequestContext<'_>,
request_body: Option<&axum::body::Bytes>,
) -> Result<Option<Response<Body>>, GatewayError> {
let Some(decision) = request_context.control_decision.as_ref() else {
let Some(decision) = request_context.decision() else {
return Ok(None);
};
@@ -21,7 +21,7 @@ pub(super) async fn maybe_build_local_admin_api_keys_routes_response(
return Ok(None);
}
let path = request_context.request_path.as_str();
let path = request_context.path();
let is_api_keys_route = matches!(path, "/api/admin/api-keys" | "/api/admin/api-keys/")
|| (path.starts_with("/api/admin/api-keys/") && path.matches('/').count() == 4);
@@ -31,7 +31,7 @@ pub(super) async fn maybe_build_local_admin_api_keys_routes_response(
match decision.route_kind.as_deref() {
Some("list_api_keys")
if request_context.request_method == http::Method::GET
if request_context.method() == http::Method::GET
&& matches!(path, "/api/admin/api-keys" | "/api/admin/api-keys/") =>
{
Ok(Some(
@@ -39,7 +39,7 @@ pub(super) async fn maybe_build_local_admin_api_keys_routes_response(
))
}
Some("api_key_detail")
if request_context.request_method == http::Method::GET
if request_context.method() == http::Method::GET
&& path.starts_with("/api/admin/api-keys/") =>
{
Ok(Some(
@@ -47,7 +47,7 @@ pub(super) async fn maybe_build_local_admin_api_keys_routes_response(
))
}
Some("create_api_key")
if request_context.request_method == http::Method::POST
if request_context.method() == http::Method::POST
&& matches!(path, "/api/admin/api-keys" | "/api/admin/api-keys/") =>
{
Ok(Some(
@@ -55,7 +55,7 @@ pub(super) async fn maybe_build_local_admin_api_keys_routes_response(
))
}
Some("update_api_key")
if request_context.request_method == http::Method::PUT
if request_context.method() == http::Method::PUT
&& path.starts_with("/api/admin/api-keys/") =>
{
Ok(Some(
@@ -63,7 +63,7 @@ pub(super) async fn maybe_build_local_admin_api_keys_routes_response(
))
}
Some("toggle_api_key")
if request_context.request_method == http::Method::PATCH
if request_context.method() == http::Method::PATCH
&& path.starts_with("/api/admin/api-keys/") =>
{
Ok(Some(
@@ -71,7 +71,7 @@ pub(super) async fn maybe_build_local_admin_api_keys_routes_response(
))
}
Some("delete_api_key")
if request_context.request_method == http::Method::DELETE
if request_context.method() == http::Method::DELETE
&& path.starts_with("/api/admin/api-keys/") =>
{
Ok(Some(
@@ -1,9 +1,19 @@
use super::ADMIN_API_KEYS_DATA_UNAVAILABLE_DETAIL;
use super::{
format_optional_unix_secs_iso8601, http, json, masked_user_api_key_display, query_param_value,
serialize_admin_system_users_export_wallet, AppState, Body, GatewayError,
GatewayPublicRequestContext, IntoResponse, Json, Response,
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
use crate::handlers::admin::shared::query_param_value;
use crate::handlers::admin::users::{
format_optional_unix_secs_iso8601, masked_user_api_key_display,
};
use crate::GatewayError;
use aether_admin::system::serialize_admin_system_users_export_wallet;
use axum::{
body::Body,
http,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
const ADMIN_API_KEYS_DATA_UNAVAILABLE_DETAIL: &str = "Admin standalone API key data unavailable";
#[derive(Debug, Default, serde::Deserialize)]
pub(super) struct AdminStandaloneApiKeyCreateRequest {
@@ -85,11 +95,10 @@ pub(super) fn admin_api_keys_id_from_path(request_path: &str) -> Option<String>
}
pub(super) fn admin_api_keys_operator_id(
request_context: &GatewayPublicRequestContext,
request_context: &AdminRequestContext<'_>,
) -> Option<String> {
request_context
.control_decision
.as_ref()
.decision()
.and_then(|decision| decision.admin_principal.as_ref())
.map(|principal| principal.user_id.clone())
}
@@ -118,8 +127,12 @@ pub(super) fn admin_api_keys_parse_limit(query: Option<&str>) -> Result<usize, S
}
}
fn masked_admin_api_key_display(state: &AdminAppState<'_>, ciphertext: Option<&str>) -> String {
masked_user_api_key_display(state, ciphertext)
}
pub(super) fn build_admin_api_key_list_item_payload(
state: &AppState,
state: &AdminAppState<'_>,
record: &aether_data::repository::auth::StoredAuthApiKeyExportRecord,
total_tokens: Option<u64>,
wallet: Option<&aether_data::repository::wallet::StoredWalletSnapshot>,
@@ -128,7 +141,7 @@ pub(super) fn build_admin_api_key_list_item_payload(
"id": record.api_key_id,
"user_id": record.user_id,
"name": record.name,
"key_display": masked_user_api_key_display(state, record.key_encrypted.as_deref()),
"key_display": masked_admin_api_key_display(state, record.key_encrypted.as_deref()),
"is_active": record.is_active,
"is_standalone": true,
"total_requests": record.total_requests,
@@ -148,7 +161,7 @@ pub(super) fn build_admin_api_key_list_item_payload(
}
pub(super) fn build_admin_api_key_detail_payload(
state: &AppState,
state: &AdminAppState<'_>,
record: &aether_data::repository::auth::StoredAuthApiKeyExportRecord,
total_tokens: u64,
wallet: Option<&aether_data::repository::wallet::StoredWalletSnapshot>,
@@ -157,7 +170,7 @@ pub(super) fn build_admin_api_key_detail_payload(
"id": record.api_key_id,
"user_id": record.user_id,
"name": record.name,
"key_display": masked_user_api_key_display(state, record.key_encrypted.as_deref()),
"key_display": masked_admin_api_key_display(state, record.key_encrypted.as_deref()),
"is_active": record.is_active,
"is_standalone": true,
"total_requests": record.total_requests,
@@ -176,7 +189,7 @@ pub(super) fn build_admin_api_key_detail_payload(
}
pub(super) async fn admin_api_key_total_tokens_by_ids(
state: &AppState,
state: &AdminAppState<'_>,
api_key_ids: &[String],
) -> Result<std::collections::BTreeMap<String, u64>, GatewayError> {
if api_key_ids.is_empty() || !state.has_usage_data_reader() {
@@ -1,8 +1,6 @@
use super::shared::*;
use crate::handlers::admin::shared::{
decrypt_catalog_secret_with_fallbacks, encrypt_catalog_secret_with_fallbacks,
};
use crate::{AppState, GatewayError};
use crate::handlers::admin::request::AdminAppState;
use crate::GatewayError;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
@@ -69,7 +67,7 @@ pub(super) struct AdminLdapConnectionTestConfig {
}
pub(super) async fn build_admin_ldap_update_config(
state: &AppState,
state: &AdminAppState<'_>,
payload: AdminLdapConfigUpdateRequest,
) -> Result<aether_data::repository::auth_modules::StoredLdapModuleConfig, String> {
let server_url = admin_ldap_trim_required(payload.server_url, "LDAP 服务器地址不能为空")?;
@@ -139,7 +137,7 @@ pub(super) async fn build_admin_ldap_update_config(
let bind_password_encrypted = match bind_password {
Some(value) if value.is_empty() => None,
Some(value) => encrypt_catalog_secret_with_fallbacks(state, &value),
Some(value) => state.encrypt_catalog_secret_with_fallbacks(&value),
None => existing.and_then(|config| config.bind_password_encrypted),
};
if bind_password_update_requested && bind_password_encrypted.is_none() {
@@ -165,7 +163,7 @@ pub(super) async fn build_admin_ldap_update_config(
}
pub(super) async fn build_admin_ldap_test_config(
state: &AppState,
state: &AdminAppState<'_>,
payload: AdminLdapConfigTestRequest,
) -> Result<Option<AdminLdapConnectionTestConfig>, String> {
if let Some(value) = payload.user_search_filter.as_deref() {
@@ -337,7 +335,7 @@ fn admin_ldap_validate_search_filter(value: &str) -> Result<(), String> {
}
fn admin_ldap_read_saved_bind_password(
state: &AppState,
state: &AdminAppState<'_>,
config: &aether_data::repository::auth_modules::StoredLdapModuleConfig,
) -> Option<String> {
config
@@ -346,7 +344,8 @@ fn admin_ldap_read_saved_bind_password(
.map(str::trim)
.filter(|value| !value.is_empty())
.and_then(|value| {
decrypt_catalog_secret_with_fallbacks(state.encryption_key(), value)
state
.decrypt_catalog_secret_with_fallbacks(value)
.or_else(|| Some(value.to_string()))
})
.filter(|value| !value.trim().is_empty())
@@ -1,5 +1,5 @@
use crate::control::GatewayPublicRequestContext;
use crate::{AppState, GatewayError};
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
use crate::GatewayError;
use axum::{
body::{Body, Bytes},
response::Response,
@@ -10,8 +10,8 @@ mod routes;
mod shared;
pub(crate) async fn maybe_build_local_admin_ldap_response(
state: &AppState,
request_context: &GatewayPublicRequestContext,
state: &AdminAppState<'_>,
request_context: &AdminRequestContext<'_>,
request_body: Option<&Bytes>,
) -> Result<Option<Response<Body>>, GatewayError> {
routes::maybe_build_local_admin_ldap_response(state, request_context, request_body).await
@@ -3,9 +3,9 @@ use super::builders::{
AdminLdapConfigTestRequest, AdminLdapConfigUpdateRequest,
};
use super::shared::*;
use crate::control::GatewayPublicRequestContext;
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
use crate::handlers::admin::shared::attach_admin_audit_response;
use crate::{AppState, GatewayError};
use crate::GatewayError;
use axum::{
body::{Body, Bytes},
http,
@@ -15,11 +15,11 @@ use axum::{
use serde_json::json;
pub(super) async fn maybe_build_local_admin_ldap_response(
state: &AppState,
request_context: &GatewayPublicRequestContext,
state: &AdminAppState<'_>,
request_context: &AdminRequestContext<'_>,
request_body: Option<&Bytes>,
) -> Result<Option<Response<Body>>, GatewayError> {
let Some(decision) = request_context.control_decision.as_ref() else {
let Some(decision) = request_context.decision() else {
return Ok(None);
};
if decision.route_family.as_deref() != Some("ldap_manage") {
@@ -28,8 +28,8 @@ pub(super) async fn maybe_build_local_admin_ldap_response(
match decision.route_kind.as_deref() {
Some("get_config")
if request_context.request_method == http::Method::GET
&& is_admin_ldap_config_root(&request_context.request_path) =>
if request_context.method() == http::Method::GET
&& is_admin_ldap_config_root(request_context.path()) =>
{
return Ok(Some(attach_admin_audit_response(
Json(build_admin_ldap_config_payload(
@@ -43,8 +43,8 @@ pub(super) async fn maybe_build_local_admin_ldap_response(
)));
}
Some("set_config")
if request_context.request_method == http::Method::PUT
&& is_admin_ldap_config_root(&request_context.request_path) =>
if request_context.method() == http::Method::PUT
&& is_admin_ldap_config_root(request_context.path()) =>
{
if !state.has_auth_module_writer() {
return Ok(Some(admin_ldap_unavailable_response()));
@@ -70,8 +70,8 @@ pub(super) async fn maybe_build_local_admin_ldap_response(
));
}
Some("test_connection")
if request_context.request_method == http::Method::POST
&& is_admin_ldap_test_root(&request_context.request_path) =>
if request_context.method() == http::Method::POST
&& is_admin_ldap_test_root(request_context.path()) =>
{
let payload = match request_body {
Some(body) if !body.is_empty() => match serde_json::from_slice::<
@@ -2,13 +2,11 @@ mod api_keys;
mod ldap;
mod oauth_config;
mod oauth_routes;
mod routes;
mod security;
pub(crate) use self::api_keys::maybe_build_local_admin_api_keys_response;
pub(crate) use self::ldap::maybe_build_local_admin_ldap_response;
pub(crate) use self::oauth_config::{
build_admin_oauth_provider_payload, build_admin_oauth_supported_types_payload,
build_admin_oauth_upsert_record,
};
pub(crate) use self::oauth_routes::maybe_build_local_admin_oauth_response;
pub(super) use self::api_keys::maybe_build_local_admin_api_keys_response;
pub(super) use self::ldap::maybe_build_local_admin_ldap_response;
pub(super) use self::oauth_routes::maybe_build_local_admin_oauth_response;
pub(super) use self::routes::maybe_build_local_admin_auth_response;
pub(crate) use self::security::maybe_build_local_admin_security_response;
@@ -1,5 +1,4 @@
use crate::handlers::admin::shared::encrypt_catalog_secret_with_fallbacks;
use crate::AppState;
use crate::handlers::admin::request::AdminAppState;
use aether_data::repository::oauth_providers::{
EncryptedSecretUpdate, UpsertOAuthProviderConfigRecord,
};
@@ -10,31 +9,31 @@ use url::Url;
#[derive(Debug, Deserialize)]
pub(crate) struct AdminOAuthProviderUpsertRequest {
pub(crate) display_name: String,
pub(crate) client_id: String,
pub(super) display_name: String,
pub(super) client_id: String,
#[serde(default)]
pub(crate) client_secret: Option<String>,
pub(super) client_secret: Option<String>,
#[serde(default)]
pub(crate) authorization_url_override: Option<String>,
pub(super) authorization_url_override: Option<String>,
#[serde(default)]
pub(crate) token_url_override: Option<String>,
pub(super) token_url_override: Option<String>,
#[serde(default)]
pub(crate) userinfo_url_override: Option<String>,
pub(super) userinfo_url_override: Option<String>,
#[serde(default)]
pub(crate) scopes: Option<Vec<String>>,
pub(crate) redirect_uri: String,
pub(crate) frontend_callback_url: String,
pub(super) scopes: Option<Vec<String>>,
pub(super) redirect_uri: String,
pub(super) frontend_callback_url: String,
#[serde(default)]
pub(crate) attribute_mapping: Option<serde_json::Value>,
pub(super) attribute_mapping: Option<serde_json::Value>,
#[serde(default)]
pub(crate) extra_config: Option<serde_json::Value>,
pub(super) extra_config: Option<serde_json::Value>,
#[serde(default)]
pub(crate) is_enabled: bool,
pub(super) is_enabled: bool,
#[serde(default)]
pub(crate) force: bool,
pub(super) force: bool,
}
pub(crate) fn build_admin_oauth_supported_types_payload() -> Vec<serde_json::Value> {
pub(super) fn build_admin_oauth_supported_types_payload() -> Vec<serde_json::Value> {
vec![json!({
"provider_type": "linuxdo",
"display_name": "Linux Do",
@@ -45,7 +44,7 @@ pub(crate) fn build_admin_oauth_supported_types_payload() -> Vec<serde_json::Val
})]
}
pub(crate) fn build_admin_oauth_provider_payload(
pub(super) fn build_admin_oauth_provider_payload(
provider: &aether_data::repository::oauth_providers::StoredOAuthProviderConfig,
) -> serde_json::Value {
json!({
@@ -135,8 +134,8 @@ fn validate_admin_oauth_url_override(url: &str, allowed_domains: &[&str]) -> Res
Ok(())
}
pub(crate) fn build_admin_oauth_upsert_record(
state: &AppState,
pub(super) fn build_admin_oauth_upsert_record(
state: &AdminAppState<'_>,
provider_type: &str,
payload: AdminOAuthProviderUpsertRequest,
) -> Result<UpsertOAuthProviderConfigRecord, String> {
@@ -213,7 +212,8 @@ pub(crate) fn build_admin_oauth_upsert_record(
} else if secret.is_empty() {
EncryptedSecretUpdate::Preserve
} else {
let encrypted = encrypt_catalog_secret_with_fallbacks(state, secret)
let encrypted = state
.encrypt_catalog_secret_with_fallbacks(secret)
.ok_or_else(|| "gateway 未配置 OAuth provider 加密密钥".to_string())?;
EncryptedSecretUpdate::Set(encrypted)
}
@@ -3,9 +3,9 @@ use super::oauth_config::{
build_admin_oauth_provider_payload, build_admin_oauth_supported_types_payload,
build_admin_oauth_upsert_record, AdminOAuthProviderUpsertRequest,
};
use crate::control::GatewayPublicRequestContext;
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
use crate::handlers::admin::shared::{attach_admin_audit_response, build_proxy_error_response};
use crate::{AppState, GatewayError};
use crate::GatewayError;
use axum::{
body::{Body, Bytes},
http,
@@ -15,11 +15,11 @@ use axum::{
use serde_json::json;
pub(crate) async fn maybe_build_local_admin_oauth_response(
state: &AppState,
request_context: &GatewayPublicRequestContext,
state: &AdminAppState<'_>,
request_context: &AdminRequestContext<'_>,
request_body: Option<&Bytes>,
) -> Result<Option<Response<Body>>, GatewayError> {
let Some(decision) = request_context.control_decision.as_ref() else {
let Some(decision) = request_context.decision() else {
return Ok(None);
};
if decision.route_family.as_deref() != Some("oauth_manage") {
@@ -27,8 +27,8 @@ pub(crate) async fn maybe_build_local_admin_oauth_response(
}
if decision.route_kind.as_deref() == Some("supported_types")
&& request_context.request_method == http::Method::GET
&& request_context.request_path == "/api/admin/oauth/supported-types"
&& request_context.method() == http::Method::GET
&& request_context.path() == "/api/admin/oauth/supported-types"
{
return Ok(Some(
Json(build_admin_oauth_supported_types_payload()).into_response(),
@@ -36,9 +36,9 @@ pub(crate) async fn maybe_build_local_admin_oauth_response(
}
if decision.route_kind.as_deref() == Some("list_providers")
&& request_context.request_method == http::Method::GET
&& request_context.method() == http::Method::GET
&& matches!(
request_context.request_path.as_str(),
request_context.path(),
"/api/admin/oauth/providers" | "/api/admin/oauth/providers/"
)
{
@@ -59,10 +59,9 @@ pub(crate) async fn maybe_build_local_admin_oauth_response(
}
if decision.route_kind.as_deref() == Some("get_provider")
&& request_context.request_method == http::Method::GET
&& request_context.method() == http::Method::GET
{
let Some(provider_type) =
admin_oauth_provider_type_from_path(&request_context.request_path)
let Some(provider_type) = admin_oauth_provider_type_from_path(request_context.path())
else {
return Ok(Some(
(
@@ -91,10 +90,9 @@ pub(crate) async fn maybe_build_local_admin_oauth_response(
}
if decision.route_kind.as_deref() == Some("upsert_provider")
&& request_context.request_method == http::Method::PUT
&& request_context.method() == http::Method::PUT
{
let Some(provider_type) =
admin_oauth_provider_type_from_path(&request_context.request_path)
let Some(provider_type) = admin_oauth_provider_type_from_path(request_context.path())
else {
return Ok(Some(build_proxy_error_response(
http::StatusCode::BAD_REQUEST,
@@ -172,10 +170,9 @@ pub(crate) async fn maybe_build_local_admin_oauth_response(
}
if decision.route_kind.as_deref() == Some("delete_provider")
&& request_context.request_method == http::Method::DELETE
&& request_context.method() == http::Method::DELETE
{
let Some(provider_type) =
admin_oauth_provider_type_from_path(&request_context.request_path)
let Some(provider_type) = admin_oauth_provider_type_from_path(request_context.path())
else {
return Ok(Some(build_proxy_error_response(
http::StatusCode::BAD_REQUEST,
@@ -229,10 +226,9 @@ pub(crate) async fn maybe_build_local_admin_oauth_response(
}
if decision.route_kind.as_deref() == Some("test_provider")
&& request_context.request_method == http::Method::POST
&& request_context.method() == http::Method::POST
{
let Some(provider_type) =
admin_oauth_test_provider_type_from_path(&request_context.request_path)
let Some(provider_type) = admin_oauth_test_provider_type_from_path(request_context.path())
else {
return Ok(Some(
(
@@ -0,0 +1,48 @@
use super::{api_keys, ldap, oauth_routes, security};
use crate::handlers::admin::request::{AdminRouteRequest, AdminRouteResult};
pub(crate) async fn maybe_build_local_admin_auth_response(
request: AdminRouteRequest<'_>,
) -> AdminRouteResult {
if let Some(response) = security::maybe_build_local_admin_security_response(
&request.state(),
&request.request_context(),
request.request_body(),
)
.await?
{
return Ok(Some(response));
}
if let Some(response) = api_keys::maybe_build_local_admin_api_keys_response(
&request.state(),
&request.request_context(),
request.request_body(),
)
.await?
{
return Ok(Some(response));
}
if let Some(response) = ldap::maybe_build_local_admin_ldap_response(
&request.state(),
&request.request_context(),
request.request_body(),
)
.await?
{
return Ok(Some(response));
}
if let Some(response) = oauth_routes::maybe_build_local_admin_oauth_response(
&request.state(),
&request.request_context(),
request.request_body(),
)
.await?
{
return Ok(Some(response));
}
Ok(None)
}
@@ -1,6 +1,6 @@
use crate::control::GatewayPublicRequestContext;
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
use crate::handlers::admin::shared::attach_admin_audit_response;
use crate::{AppState, GatewayError};
use crate::GatewayError;
use axum::{
body::{Body, Bytes},
http,
@@ -98,7 +98,7 @@ fn admin_security_validate_ip_or_cidr(value: &str) -> bool {
}
async fn build_admin_security_blacklist_add_response(
state: &AppState,
state: &AdminAppState<'_>,
request_body: Option<&Bytes>,
) -> Result<Response<Body>, GatewayError> {
let Some(request_body) = request_body else {
@@ -113,7 +113,7 @@ async fn build_admin_security_blacklist_add_response(
_ => {
return Ok(build_admin_security_bad_request_response(
"请求数据验证失败",
))
));
}
};
@@ -148,11 +148,10 @@ async fn build_admin_security_blacklist_add_response(
}
async fn build_admin_security_blacklist_remove_response(
state: &AppState,
request_context: &GatewayPublicRequestContext,
state: &AdminAppState<'_>,
request_context: &AdminRequestContext<'_>,
) -> Result<Response<Body>, GatewayError> {
let Some(ip_address) = admin_security_blacklist_ip_from_path(&request_context.request_path)
else {
let Some(ip_address) = admin_security_blacklist_ip_from_path(request_context.path()) else {
return Ok(build_admin_security_bad_request_response("缺少 ip_address"));
};
@@ -176,7 +175,7 @@ async fn build_admin_security_blacklist_remove_response(
}
async fn build_admin_security_blacklist_stats_response(
state: &AppState,
state: &AdminAppState<'_>,
) -> Result<Response<Body>, GatewayError> {
let (available, total, error) = state.admin_security_blacklist_stats().await?;
let mut payload = json!({
@@ -196,7 +195,7 @@ async fn build_admin_security_blacklist_stats_response(
}
async fn build_admin_security_blacklist_list_response(
state: &AppState,
state: &AdminAppState<'_>,
) -> Result<Response<Body>, GatewayError> {
let entries = state.list_admin_security_blacklist().await?;
let total = entries.len();
@@ -210,7 +209,7 @@ async fn build_admin_security_blacklist_list_response(
}
async fn build_admin_security_whitelist_add_response(
state: &AppState,
state: &AdminAppState<'_>,
request_body: Option<&Bytes>,
) -> Result<Response<Body>, GatewayError> {
let Some(request_body) = request_body else {
@@ -223,7 +222,7 @@ async fn build_admin_security_whitelist_add_response(
Err(_) => {
return Ok(build_admin_security_bad_request_response(
"请求数据验证失败",
))
));
}
};
let ip_address = payload.ip_address.trim();
@@ -249,11 +248,10 @@ async fn build_admin_security_whitelist_add_response(
}
async fn build_admin_security_whitelist_remove_response(
state: &AppState,
request_context: &GatewayPublicRequestContext,
state: &AdminAppState<'_>,
request_context: &AdminRequestContext<'_>,
) -> Result<Response<Body>, GatewayError> {
let Some(ip_address) = admin_security_whitelist_ip_from_path(&request_context.request_path)
else {
let Some(ip_address) = admin_security_whitelist_ip_from_path(request_context.path()) else {
return Ok(build_admin_security_bad_request_response("缺少 ip_address"));
};
@@ -277,7 +275,7 @@ async fn build_admin_security_whitelist_remove_response(
}
async fn build_admin_security_whitelist_list_response(
state: &AppState,
state: &AdminAppState<'_>,
) -> Result<Response<Body>, GatewayError> {
let whitelist = state.list_admin_security_whitelist().await?;
let total = whitelist.len();
@@ -295,11 +293,11 @@ async fn build_admin_security_whitelist_list_response(
}
pub(crate) async fn maybe_build_local_admin_security_response(
state: &AppState,
request_context: &GatewayPublicRequestContext,
state: &AdminAppState<'_>,
request_context: &AdminRequestContext<'_>,
request_body: Option<&Bytes>,
) -> Result<Option<Response<Body>>, GatewayError> {
let Some(decision) = request_context.control_decision.as_ref() else {
let Some(decision) = request_context.decision() else {
return Ok(None);
};