mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-07 10:14:35 +00:00
feat(web): add webhook-managed machine access and multi-instance CLI support (#1989)
* feat: add webhook-managed access and multi-instance CLI support * fix(foreign): verify credential of foreign credential peer
This commit is contained in:
@@ -7,8 +7,11 @@ mod users;
|
||||
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use axum::routing::post;
|
||||
use axum::extract::Path;
|
||||
use axum::http::{header, Request, StatusCode};
|
||||
use axum::middleware::{self as axum_mw, Next};
|
||||
use axum::response::Response;
|
||||
use axum::routing::{delete, post};
|
||||
use axum::{extract::State, routing::get, Extension, Json, Router};
|
||||
use axum_login::tower_sessions::{ExpiredDeletion, SessionManagerLayer};
|
||||
use axum_login::{login_required, AuthManagerLayerBuilder, AuthUser, AuthzBackend};
|
||||
@@ -29,6 +32,7 @@ use users::{AuthSession, Backend};
|
||||
use crate::client_manager::storage::StorageToken;
|
||||
use crate::client_manager::ClientManager;
|
||||
use crate::db::Db;
|
||||
use crate::webhook::SharedWebhookConfig;
|
||||
use crate::FeatureFlags;
|
||||
|
||||
/// Embed assets for web dashboard, build frontend first
|
||||
@@ -41,12 +45,9 @@ pub struct RestfulServer {
|
||||
bind_addr: SocketAddr,
|
||||
client_mgr: Arc<ClientManager>,
|
||||
feature_flags: Arc<FeatureFlags>,
|
||||
webhook_config: SharedWebhookConfig,
|
||||
db: Db,
|
||||
oidc_config: oidc::OidcConfig,
|
||||
|
||||
// serve_task: Option<ScopedTask<()>>,
|
||||
// delete_task: Option<ScopedTask<tower_sessions::session_store::Result<()>>>,
|
||||
// network_api: NetworkApi<WebClientManager>,
|
||||
web_router: Option<Router>,
|
||||
}
|
||||
|
||||
@@ -111,20 +112,17 @@ impl RestfulServer {
|
||||
web_router: Option<Router>,
|
||||
feature_flags: Arc<FeatureFlags>,
|
||||
oidc_config: oidc::OidcConfig,
|
||||
webhook_config: SharedWebhookConfig,
|
||||
) -> anyhow::Result<Self> {
|
||||
assert!(client_mgr.is_running());
|
||||
|
||||
// let network_api = NetworkApi::new();
|
||||
|
||||
Ok(RestfulServer {
|
||||
bind_addr,
|
||||
client_mgr,
|
||||
feature_flags,
|
||||
webhook_config,
|
||||
db,
|
||||
oidc_config,
|
||||
// serve_task: None,
|
||||
// delete_task: None,
|
||||
// network_api,
|
||||
web_router,
|
||||
})
|
||||
}
|
||||
@@ -245,7 +243,31 @@ impl RestfulServer {
|
||||
.zstd(true)
|
||||
.quality(tower_http::compression::CompressionLevel::Default);
|
||||
|
||||
let app = Router::new()
|
||||
// Token-authenticated management routes that bypass session auth.
|
||||
let internal_app = if self.webhook_config.has_internal_auth() {
|
||||
let internal_token = self.webhook_config.internal_auth_token.clone().unwrap();
|
||||
let internal_routes = Router::new()
|
||||
.route(
|
||||
"/api/internal/sessions",
|
||||
get(Self::handle_list_all_sessions_internal),
|
||||
)
|
||||
.route(
|
||||
"/api/internal/sessions/:machine-id",
|
||||
delete(Self::handle_disconnect_session_internal),
|
||||
)
|
||||
.merge(NetworkApi::build_route_internal())
|
||||
.merge(rpc::router_internal())
|
||||
.with_state(self.client_mgr.clone())
|
||||
.layer(axum_mw::from_fn(move |req, next| {
|
||||
let token = internal_token.clone();
|
||||
internal_auth_middleware(token, req, next)
|
||||
}));
|
||||
Some(internal_routes)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut app = Router::new()
|
||||
.route("/api/v1/summary", get(Self::handle_get_summary))
|
||||
.route("/api/v1/sessions", get(Self::handle_list_all_sessions))
|
||||
.merge(NetworkApi::build_route())
|
||||
@@ -265,6 +287,10 @@ impl RestfulServer {
|
||||
.layer(tower_http::cors::CorsLayer::very_permissive())
|
||||
.layer(compression_layer);
|
||||
|
||||
if let Some(internal_routes) = internal_app {
|
||||
app = app.merge(internal_routes);
|
||||
}
|
||||
|
||||
#[cfg(feature = "embed")]
|
||||
let app = if let Some(web_router) = self.web_router.take() {
|
||||
app.merge(web_router)
|
||||
@@ -279,4 +305,52 @@ impl RestfulServer {
|
||||
|
||||
Ok((serve_task, delete_task))
|
||||
}
|
||||
|
||||
/// Session listing endpoint for token-authenticated management clients.
|
||||
async fn handle_list_all_sessions_internal(
|
||||
State(client_mgr): AppState,
|
||||
) -> Result<Json<ListSessionJsonResp>, HttpHandleError> {
|
||||
let ret = client_mgr.list_sessions().await;
|
||||
Ok(ListSessionJsonResp(ret).into())
|
||||
}
|
||||
|
||||
async fn handle_disconnect_session_internal(
|
||||
Path(machine_id): Path<uuid::Uuid>,
|
||||
State(client_mgr): AppState,
|
||||
) -> Result<StatusCode, HttpHandleError> {
|
||||
if client_mgr
|
||||
.disconnect_session_by_machine_id_global(&machine_id)
|
||||
.await
|
||||
{
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
} else {
|
||||
Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
other_error("session not found").into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Middleware that validates X-Internal-Auth for token-authenticated routes.
|
||||
async fn internal_auth_middleware(
|
||||
expected_token: String,
|
||||
req: Request<axum::body::Body>,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
let auth_header = req
|
||||
.headers()
|
||||
.get("X-Internal-Auth")
|
||||
.and_then(|v| v.to_str().ok());
|
||||
|
||||
match auth_header {
|
||||
Some(token) if token == expected_token => next.run(req).await,
|
||||
_ => Response::builder()
|
||||
.status(StatusCode::UNAUTHORIZED)
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.body(axum::body::Body::from(
|
||||
r#"{"error":"unauthorized: invalid or missing X-Internal-Auth header"}"#,
|
||||
))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user