mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
Add admin proxy install session creation
This commit is contained in:
@@ -288,6 +288,19 @@ pub(super) fn classify_admin_operations_family_route(
|
|||||||
"admin:proxy_nodes",
|
"admin:proxy_nodes",
|
||||||
false,
|
false,
|
||||||
))
|
))
|
||||||
|
} else if method == http::Method::POST
|
||||||
|
&& matches!(
|
||||||
|
normalized_path,
|
||||||
|
"/api/admin/proxy-nodes/install-sessions" | "/api/admin/proxy-nodes/install-sessions/"
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Some(classified(
|
||||||
|
"admin_proxy",
|
||||||
|
"proxy_nodes_manage",
|
||||||
|
"create_proxy_node_install_session",
|
||||||
|
"admin:proxy_nodes",
|
||||||
|
false,
|
||||||
|
))
|
||||||
} else if method == http::Method::POST
|
} else if method == http::Method::POST
|
||||||
&& matches!(
|
&& matches!(
|
||||||
normalized_path,
|
normalized_path,
|
||||||
|
|||||||
@@ -82,6 +82,15 @@ fn classifies_admin_proxy_nodes_manual_create_as_admin_proxy_route() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn classifies_admin_proxy_nodes_install_session_create_as_admin_proxy_route() {
|
||||||
|
assert_proxy_nodes_admin_route(
|
||||||
|
http::Method::POST,
|
||||||
|
"/api/admin/proxy-nodes/install-sessions",
|
||||||
|
"create_proxy_node_install_session",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn classifies_admin_proxy_nodes_manual_update_as_admin_proxy_route() {
|
fn classifies_admin_proxy_nodes_manual_update_as_admin_proxy_route() {
|
||||||
assert_proxy_nodes_admin_route(
|
assert_proxy_nodes_admin_route(
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ use aether_admin::system::{
|
|||||||
use aether_contracts::tunnel::{
|
use aether_contracts::tunnel::{
|
||||||
TUNNEL_RELAY_FORWARDED_BY_HEADER, TUNNEL_RELAY_OWNER_INSTANCE_HEADER,
|
TUNNEL_RELAY_FORWARDED_BY_HEADER, TUNNEL_RELAY_OWNER_INSTANCE_HEADER,
|
||||||
};
|
};
|
||||||
|
use aether_data::repository::management_tokens::{
|
||||||
|
CreateManagementTokenRecord, StoredManagementTokenUserSummary,
|
||||||
|
};
|
||||||
use aether_data::repository::proxy_nodes::{ProxyNodeEventQuery, ProxyNodeMetricsStep};
|
use aether_data::repository::proxy_nodes::{ProxyNodeEventQuery, ProxyNodeMetricsStep};
|
||||||
use axum::{
|
use axum::{
|
||||||
body::{Body, Bytes},
|
body::{Body, Bytes},
|
||||||
@@ -27,6 +30,12 @@ use axum::{
|
|||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::handlers::public::build_proxy_node_install_session_response;
|
||||||
|
use crate::handlers::shared::generate_gateway_secret_plaintext;
|
||||||
|
use crate::LocalMutationOutcome;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct ProxyNodeRegisterRequest {
|
struct ProxyNodeRegisterRequest {
|
||||||
@@ -119,6 +128,11 @@ struct ProxyNodeTestUrlRequest {
|
|||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct ProxyNodeInstallSessionCreateRequest {
|
||||||
|
node_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct ProxyNodeBatchUpgradeRequest {
|
struct ProxyNodeBatchUpgradeRequest {
|
||||||
version: String,
|
version: String,
|
||||||
@@ -208,6 +222,7 @@ impl Drop for ProxyConnectivityProbeUrlOverrideGuard {
|
|||||||
pub(crate) async fn maybe_build_local_admin_proxy_nodes_response(
|
pub(crate) async fn maybe_build_local_admin_proxy_nodes_response(
|
||||||
state: &AdminAppState<'_>,
|
state: &AdminAppState<'_>,
|
||||||
request_context: &AdminRequestContext<'_>,
|
request_context: &AdminRequestContext<'_>,
|
||||||
|
headers: &http::HeaderMap,
|
||||||
request_body: Option<&Bytes>,
|
request_body: Option<&Bytes>,
|
||||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||||
let Some(decision) = request_context.decision() else {
|
let Some(decision) = request_context.decision() else {
|
||||||
@@ -432,6 +447,37 @@ pub(crate) async fn maybe_build_local_admin_proxy_nodes_response(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if decision.route_kind.as_deref() == Some("create_proxy_node_install_session")
|
||||||
|
&& request_context.method() == http::Method::POST
|
||||||
|
{
|
||||||
|
if !state.app().has_management_token_writer() {
|
||||||
|
return Ok(Some(build_admin_proxy_nodes_data_unavailable_response()));
|
||||||
|
}
|
||||||
|
let input = match parse_json_body::<ProxyNodeInstallSessionCreateRequest>(request_body) {
|
||||||
|
Ok(input) => input,
|
||||||
|
Err(response) => return Ok(Some(response)),
|
||||||
|
};
|
||||||
|
let node_name = match validate_proxy_install_node_name(&input.node_name) {
|
||||||
|
Ok(node_name) => node_name,
|
||||||
|
Err(response) => return Ok(Some(response)),
|
||||||
|
};
|
||||||
|
let raw_token =
|
||||||
|
match create_proxy_install_management_token(state, request_context, &node_name).await {
|
||||||
|
Ok(token) => token,
|
||||||
|
Err(response) => return Ok(Some(response)),
|
||||||
|
};
|
||||||
|
return Ok(Some(
|
||||||
|
build_proxy_node_install_session_response(
|
||||||
|
state.app(),
|
||||||
|
request_context.public(),
|
||||||
|
headers,
|
||||||
|
node_name,
|
||||||
|
raw_token,
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
if decision.route_kind.as_deref() == Some("update_manual_node")
|
if decision.route_kind.as_deref() == Some("update_manual_node")
|
||||||
&& request_context.method() == http::Method::PATCH
|
&& request_context.method() == http::Method::PATCH
|
||||||
{
|
{
|
||||||
@@ -2021,6 +2067,121 @@ fn validate_optional_object(value: Option<&Value>, field: &str) -> Result<(), Re
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate_proxy_install_node_name(value: &str) -> Result<String, Response<Body>> {
|
||||||
|
let trimmed = value.trim();
|
||||||
|
if trimmed.is_empty() || trimmed.chars().count() > 100 {
|
||||||
|
return Err(bad_request_response(
|
||||||
|
"节点名称不能为空,且不能超过 100 个字符",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(trimmed.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_proxy_install_management_token_plaintext() -> String {
|
||||||
|
generate_gateway_secret_plaintext("ae", "-")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hash_proxy_install_management_token(value: &str) -> String {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(value.as_bytes());
|
||||||
|
format!("{:x}", hasher.finalize())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn proxy_install_management_token_prefix(value: &str) -> Option<String> {
|
||||||
|
(!value.is_empty()).then(|| value[..value.len().min(12)].to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_proxy_install_management_token(
|
||||||
|
state: &AdminAppState<'_>,
|
||||||
|
request_context: &AdminRequestContext<'_>,
|
||||||
|
node_name: &str,
|
||||||
|
) -> Result<String, Response<Body>> {
|
||||||
|
let Some(principal) = request_context
|
||||||
|
.decision()
|
||||||
|
.and_then(|decision| decision.admin_principal.as_ref())
|
||||||
|
else {
|
||||||
|
return Err((
|
||||||
|
http::StatusCode::UNAUTHORIZED,
|
||||||
|
Json(json!({ "detail": "未认证管理员" })),
|
||||||
|
)
|
||||||
|
.into_response());
|
||||||
|
};
|
||||||
|
|
||||||
|
let user = match state.app().find_user_auth_by_id(&principal.user_id).await {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(err) => {
|
||||||
|
return Err((
|
||||||
|
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(json!({ "detail": format!("admin user lookup failed: {err:?}") })),
|
||||||
|
)
|
||||||
|
.into_response())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let user = user
|
||||||
|
.map(|user| {
|
||||||
|
StoredManagementTokenUserSummary::new(
|
||||||
|
user.id,
|
||||||
|
user.email,
|
||||||
|
user.username,
|
||||||
|
user.role,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
StoredManagementTokenUserSummary::new(
|
||||||
|
principal.user_id.clone(),
|
||||||
|
None,
|
||||||
|
principal.user_id.clone(),
|
||||||
|
principal.user_role.clone(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map_err(|err| {
|
||||||
|
(
|
||||||
|
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(json!({ "detail": format!("management token user summary build failed: {err:?}") })),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let raw_token = generate_proxy_install_management_token_plaintext();
|
||||||
|
let short_id = Uuid::new_v4()
|
||||||
|
.simple()
|
||||||
|
.to_string()
|
||||||
|
.chars()
|
||||||
|
.take(8)
|
||||||
|
.collect::<String>();
|
||||||
|
let record = CreateManagementTokenRecord {
|
||||||
|
id: Uuid::new_v4().to_string(),
|
||||||
|
user_id: user.id.clone(),
|
||||||
|
user,
|
||||||
|
token_hash: hash_proxy_install_management_token(&raw_token),
|
||||||
|
token_prefix: proxy_install_management_token_prefix(&raw_token),
|
||||||
|
name: format!("aether-proxy {node_name} {short_id}"),
|
||||||
|
description: Some("Created by proxy node one-click installer".to_string()),
|
||||||
|
allowed_ips: None,
|
||||||
|
permissions: Some(json!(["admin:proxy_nodes:write"])),
|
||||||
|
expires_at_unix_secs: None,
|
||||||
|
is_active: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
match state.app().create_management_token(&record).await {
|
||||||
|
Ok(LocalMutationOutcome::Applied(_)) => Ok(raw_token),
|
||||||
|
Ok(LocalMutationOutcome::Invalid(detail)) => Err(bad_request_response(detail)),
|
||||||
|
Ok(LocalMutationOutcome::Unavailable) => {
|
||||||
|
Err(build_admin_proxy_nodes_data_unavailable_response())
|
||||||
|
}
|
||||||
|
Ok(LocalMutationOutcome::NotFound) => Err((
|
||||||
|
http::StatusCode::NOT_FOUND,
|
||||||
|
Json(json!({ "detail": "管理员不存在" })),
|
||||||
|
)
|
||||||
|
.into_response()),
|
||||||
|
Err(err) => Err((
|
||||||
|
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(json!({ "detail": format!("management token create failed: {err:?}") })),
|
||||||
|
)
|
||||||
|
.into_response()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_proxy_node_event_query(
|
fn parse_proxy_node_event_query(
|
||||||
query: Option<&str>,
|
query: Option<&str>,
|
||||||
) -> Result<ProxyNodeEventQuery, Response<Body>> {
|
) -> Result<ProxyNodeEventQuery, Response<Body>> {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ pub(crate) async fn maybe_build_local_admin_system_response(
|
|||||||
if let Some(response) = proxy_nodes::maybe_build_local_admin_proxy_nodes_response(
|
if let Some(response) = proxy_nodes::maybe_build_local_admin_proxy_nodes_response(
|
||||||
&request.state(),
|
&request.state(),
|
||||||
&request.request_context(),
|
&request.request_context(),
|
||||||
|
request.request_headers(),
|
||||||
request.request_body(),
|
request.request_body(),
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
|
|||||||
@@ -316,6 +316,11 @@ pub(crate) fn admin_proxy_local_requires_buffered_body(
|
|||||||
| (Some("api_keys_manage"), http::Method::PATCH, Some("toggle_api_key"))
|
| (Some("api_keys_manage"), http::Method::PATCH, Some("toggle_api_key"))
|
||||||
| (Some("adaptive_manage"), http::Method::PATCH, Some("toggle_mode"))
|
| (Some("adaptive_manage"), http::Method::PATCH, Some("toggle_mode"))
|
||||||
| (Some("proxy_nodes_manage"), http::Method::POST, Some("create_manual_node"))
|
| (Some("proxy_nodes_manage"), http::Method::POST, Some("create_manual_node"))
|
||||||
|
| (
|
||||||
|
Some("proxy_nodes_manage"),
|
||||||
|
http::Method::POST,
|
||||||
|
Some("create_proxy_node_install_session"),
|
||||||
|
)
|
||||||
| (Some("proxy_nodes_manage"), http::Method::POST, Some("register_node"))
|
| (Some("proxy_nodes_manage"), http::Method::POST, Some("register_node"))
|
||||||
| (Some("proxy_nodes_manage"), http::Method::POST, Some("heartbeat_node"))
|
| (Some("proxy_nodes_manage"), http::Method::POST, Some("heartbeat_node"))
|
||||||
| (Some("proxy_nodes_manage"), http::Method::POST, Some("unregister_node"))
|
| (Some("proxy_nodes_manage"), http::Method::POST, Some("unregister_node"))
|
||||||
|
|||||||
Reference in New Issue
Block a user